From 3b0ae3426bab1e5cfeadbb81f28700130201854c Mon Sep 17 00:00:00 2001 From: Laxmikant Sharma Date: Wed, 16 Sep 2026 11:05:36 -0700 Subject: [PATCH 1/4] v0.1.1: signature-set order, low-S ECDSA, delivery_hash over the signed Delivery Standalone repository fixes ahead of -02; the posted -01 text is unchanged. - pactcore: signatures_ordered() (sorted by the Section 9.1 normalized kid, ties by the raw kid, code point order); ECDSA signatures are emitted and accepted only in the low-S form, with the P-256 and P-384 group orders as constants. - facilitator: an unsorted signature set is refused as signatures-unordered (422, Section 13.1) after the signer-identity check, recorded as CHOICES C9; --problems exports the problem-type table for the registry section. - validate: the delivery_hash checks include the Delivery signature, as facilitator.py always did; identifier normalization and the assurance constraint are imported from pactcore (exact decimal, no 1e-9 slack), and each worked bound is checked from both sides one cent apart; V-07 is the draft's own vector (a trailing "/" only; Section 9.1 folds scheme and host, not the path); four checks added: the curve orders proved by computing n*G, and vectors V-21 (unsorted set) and V-22 (high-S). 66 checks become 71. - examples: verdict.json and challenge.json delivery_hash recomputed over the signed Delivery. -01 prints this digest only truncated, and the truncated value matches neither construction. - schemas: bid.schema.json and cfb.schema.json removed; -01 withdrew the sealed-bid procedure and its examples were already quarantined. - Makefile builds -01; both READMEs updated. Gates: tools/validate.py 71 passed; tools/measure.py five paths with money balanced, 24 refusals and 2 acceptances, the third-party-signer case still refused as unexpected-signer. --- Makefile | 1 + README.md | 8 ++- examples/challenge.json | 2 +- examples/verdict.json | 2 +- schemas/bid.schema.json | 16 ----- schemas/cfb.schema.json | 44 ------------ tools/README.md | 15 ++++- tools/facilitator.py | 19 ++++++ tools/pactcore.py | 40 ++++++++++- tools/validate.py | 146 ++++++++++++++++++++++++++++++---------- 10 files changed, 188 insertions(+), 105 deletions(-) delete mode 100644 schemas/bid.schema.json delete mode 100644 schemas/cfb.schema.json diff --git a/Makefile b/Makefile index 1d2f04c..4dd7a27 100644 --- a/Makefile +++ b/Makefile @@ -2,5 +2,6 @@ all: validate draft draft: xml2rfc --text --html draft/draft-laxsharma-pact-00.xml + xml2rfc --text --html draft/draft-laxsharma-pact-01.xml validate: python3 tools/validate.py diff --git a/README.md b/README.md index 63f4ad2..a5f0ed4 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,11 @@ findings and narrows the document to what only PACT can specify: with a `0x00` prefix, nodes with `0x01`, split at the largest power of two less than the count. -The validator runs 66 checks: 7 schema, 2 canonicalization, 10 hash, -10 rule, 9 assurance-constraint, 6 Merkle, and 22 negative vectors from -the draft's conformance table. The rules JSON Schema cannot express are +The validator runs 71 checks: 7 schema, 2 canonicalization, 10 hash, +11 rule, 9 assurance-constraint, 6 Merkle, 22 negative vectors from +the draft's conformance table, and 4 on signature sets and ECDSA +encoding (two prove the curve orders behind the low-S rule, two are +the vectors V-21 and V-22). The rules JSON Schema cannot express are checked in code: parties distinct after normalization, one signature per named party, protected headers carrying `alg`, `kid` and `typ` with an allowed algorithm, and the assurance constraint of -01 Section 7.2 against diff --git a/examples/challenge.json b/examples/challenge.json index 3ae2177..ea9b81e 100644 --- a/examples/challenge.json +++ b/examples/challenge.json @@ -2,7 +2,7 @@ "pact": "0.1", "type": "Challenge", "vtc_id": "vtc_9f2c11", - "delivery_hash": "sha256:c041f6c502786e044aea8aaebc259f82638652467be5ced7b6aba791b18d16af", + "delivery_hash": "sha256:c404026ffa4ec4ae93333206c7581fc0b3c344c70da5e218e1997fdc43799c71", "proof": { "profile": "acceptance", "instrument_hash": "sha256:d9205d4f2922afd55c0a2dc4ab00d8ee5a512343430bcf5e9abf0c76d66c69f7", diff --git a/examples/verdict.json b/examples/verdict.json index d526f2b..784f0bc 100644 --- a/examples/verdict.json +++ b/examples/verdict.json @@ -2,7 +2,7 @@ "pact": "0.1", "type": "Verdict", "vtc_id": "vtc_9f2c11", - "delivery_hash": "sha256:c041f6c502786e044aea8aaebc259f82638652467be5ced7b6aba791b18d16af", + "delivery_hash": "sha256:c404026ffa4ec4ae93333206c7581fc0b3c344c70da5e218e1997fdc43799c71", "outcome": "PASS", "profile": "acceptance", "instrument_hash": "sha256:d9205d4f2922afd55c0a2dc4ab00d8ee5a512343430bcf5e9abf0c76d66c69f7", diff --git a/schemas/bid.schema.json b/schemas/bid.schema.json deleted file mode 100644 index 8b368bd..0000000 --- a/schemas/bid.schema.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://pact-spec.github.io/spec/schemas/bid.schema.json", - "title": "PACT Bid", - "type": "object", - "required": ["pact", "type", "cfb_id", "bidder", "commitment", "signatures"], - "properties": { - "pact": { "type": "string" }, - "type": { "const": "Bid" }, - "cfb_id": { "type": "string" }, - "bidder": { "$ref": "common.schema.json#/$defs/did" }, - "commitment": { "$ref": "common.schema.json#/$defs/hash" }, - "signatures": { "type": "array", "minItems": 1, - "items": { "$ref": "common.schema.json#/$defs/signature" } } - } -} diff --git a/schemas/cfb.schema.json b/schemas/cfb.schema.json deleted file mode 100644 index 4b815e5..0000000 --- a/schemas/cfb.schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://pact-spec.github.io/spec/schemas/cfb.schema.json", - "title": "PACT Call-for-Bids", - "type": "object", - "required": ["pact", "type", "id", "buyer", "task", "max_price", - "verification", "bid_deadline", "signatures"], - "properties": { - "pact": { "type": "string" }, - "type": { "const": "CallForBids" }, - "id": { "type": "string" }, - "buyer": { "$ref": "common.schema.json#/$defs/did" }, - "task": { - "type": "object", - "required": ["spec_hash", "deadline"], - "properties": { - "spec_hash": { "$ref": "common.schema.json#/$defs/hash" }, - "spec": { "$ref": "taskspec.schema.json" }, - "spec_uri": { "type": "string", "format": "uri" }, - "deadline": { "type": "string", "format": "date-time" } - } - }, - "max_price": { - "type": "object", - "required": ["amount", "currency"], - "properties": { - "amount": { "$ref": "common.schema.json#/$defs/money" }, - "currency": { "type": "string" } - } - }, - "verification": { - "type": "object", - "required": ["tier", "criteria_hash"], - "properties": { - "tier": { "$ref": "common.schema.json#/$defs/tier" }, - "criteria_hash": { "$ref": "common.schema.json#/$defs/hash" } - } - }, - "bid_deadline": { "type": "string", "format": "date-time" }, - "challenge": { "$ref": "common.schema.json#/$defs/challenge" }, - "signatures": { "type": "array", "minItems": 1, - "items": { "$ref": "common.schema.json#/$defs/signature" } } - } -} diff --git a/tools/README.md b/tools/README.md index 84412c3..f0b37bb 100644 --- a/tools/README.md +++ b/tools/README.md @@ -5,9 +5,9 @@ implementation of the protocol. | File | What it is | |---|---| -| `validate.py` | The conformance validator: 66 checks over the committed examples and the Section 13.3 vectors. Needs only `jsonschema` and `referencing`. It recomputes digests and Merkle roots but verifies no signature. | +| `validate.py` | The conformance validator: 71 checks over the committed examples, the Section 13.3 vectors, and the two vectors V-21 and V-22 added here ahead of the text (choice 7 below). Needs only `jsonschema` and `referencing`. It recomputes digests and Merkle roots but verifies no signature; the constraint, the normalization and the signature-set rules are imported from `pactcore.py` so the two tools cannot disagree. | | `pactcore.py` | Canonicalization, digests, JWS signing and verification over the transmitted protected header, identifier normalization, the assurance constraint in exact decimal arithmetic, and the RFC 9162 Merkle tree. | -| `facilitator.py` | A reference Facilitator: the six operations of Table 1 over five paths, the Figure 2 state machine including the challenge window and the Figure 6 overturned-PASS path, the Section 7.4 waterfall, schema validation of every posted object, a signed capability document, and RFC 9457 refusals in the draft's own namespace that name the rule. `--rules` prints what it enforces and what it chose. | +| `facilitator.py` | A reference Facilitator: the six operations of Table 1 over five paths, the Figure 2 state machine including the challenge window and the Figure 6 overturned-PASS path, the Section 7.4 waterfall, schema validation of every posted object, a signed capability document, and RFC 9457 refusals in the draft's own namespace that name the rule. `--rules` prints what it enforces and what it chose; `--problems` prints every problem type it emits with its status and section. | | `agents.py` | Buyer, Seller, Verifier and Challenger clients. | | `measure.py` | Drives five contracts through the terminal states on a clock the harness advances, exercises 24 refusals and 2 acceptances each on the rule it is named for, asserts that money balances, checks every minted object and the capability document against the schemas, and reports costs. | @@ -85,6 +85,17 @@ kind of disagreement the experiment exists to surface. bound and never applies it. 6. **PROPOSED is not observable.** With no rail the pools are debited in memory when a co-signed contract is accepted, so the 201 reports FUNDED. +7. **The signature set is sorted and ECDSA is low-S.** Section 6 digests the + contract including its `signatures` array, and the -01 text does not order + that array, so one agreement signed in two orders has two `vtc_hash` values. + This implementation refuses an array not sorted by the Section 9.1 + normalized kid (ties by the raw kid, code point order) as + `signatures-unordered`, and refuses an ECDSA signature whose s is in the high + half of the curve order, for the same reason: a second valid encoding of one + signature is a second digest. Both are checked in `validate.py` and both + are proposed as rules for the next revision. It also computes `delivery_hash` over the Delivery + including its signature, which is what `validate.py` now checks too; the + v0.1.0 validator excluded the signature and the two tools disagreed. ## Measured on 12 September 2026 diff --git a/tools/facilitator.py b/tools/facilitator.py index 3f733ea..edd7372 100644 --- a/tools/facilitator.py +++ b/tools/facilitator.py @@ -96,6 +96,11 @@ C7 Amounts are settled in whole cents; a contract with more decimal places is refused. C8 Not implemented: subcontracts (Section 10), release modes other than on-verification, challenge deposits, committed-sample assurance, network key resolution, any rail. +C9 The `signatures` array is sorted by the Section 9.1 normalized kid, ties by the raw kid, + code point order; an unsorted array is refused as signatures-unordered. The -01 text + does not order the array, so one agreement signed in two orders has two vtc_hash values. + ECDSA signatures are refused unless s is in the low half of the curve order, for the + same reason: a second valid encoding of one signature is a second digest. """ # Section 18.5: identifiers are appended to this prefix, which the draft owns. @@ -134,6 +139,7 @@ "object-conflict": (409, "Section 12.2"), "unknown-contract": (404, "Section 12"), "payload-too-large": (413, "Section 12"), + "signatures-unordered": (422, "Section 13.1"), "internal-error": (500, "Section 12"), } @@ -440,6 +446,12 @@ def propose(self, vtc: dict) -> tuple[int, dict]: raise Refuse("unexpected-signer", "the contract carries a signature from a party that is " "neither its Buyer nor its Seller", signer=kid) + # CHOICES C9, after the signer check so a stranger's signature is refused + # as what it is: the set is sorted, so the Section 6 digest over it + # does not depend on which party signed last. + ok, why = pc.signatures_ordered(vtc) + if not ok: + raise Refuse("signatures-unordered", why) # Section 7.2: evaluated exactly, BEFORE funds lock. q_min = vtc["assurance"]["q_min"] @@ -1064,9 +1076,16 @@ def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--port", type=int, default=8402) ap.add_argument("--verbose", action="store_true") + ap.add_argument("--problems", action="store_true", + help="print every problem type this Facilitator emits, with its " + "HTTP status and the section it cites, then exit") ap.add_argument("--rules", action="store_true", help="print the rules this implementation enforces, and its choices, and exit") args = ap.parse_args() + if args.problems: + for kind, (status, section) in sorted(PROBLEMS.items()): + print(f"{PROBLEM_BASE}{kind}\t{status}\t{section}") + return if args.rules: print(RULES.strip()) print() diff --git a/tools/pactcore.py b/tools/pactcore.py index 4419b49..1d1479f 100644 --- a/tools/pactcore.py +++ b/tools/pactcore.py @@ -40,7 +40,7 @@ from typing import Any, Callable # Ed25519 and P-256 come from `cryptography`. It is an optional dependency: -# validate.py's sixty-six checks do not need it, and this module is only +# validate.py's checks do not need it, and this module is only # imported by the Facilitator and the agents. try: from cryptography.hazmat.primitives.asymmetric.ed25519 import ( @@ -183,6 +183,22 @@ def same_party(a: str, b: str) -> bool: return norm(a) == norm(b) +def signatures_ordered(obj: dict) -> tuple[bool, str]: + """The `signatures` array sorted by kid (facilitator CHOICES C9). + + Two agents that each attach their own entry and then exchange the object + produce two arrays, two vtc_hash values (Section 6 digests the signature + set) and two contracts for one agreement. The order is the Section 9.1 + normalized kid, ties broken by the raw kid, both compared as sequences of + Unicode code points. Returns (ok, reason); an object with fewer than two + entries is trivially ordered. + """ + keys = [(norm(k), k) for k in signer_kids(obj)] + if keys != sorted(keys): + return False, "signatures are not sorted by normalized kid" + return True, "ok" + + # -------------------------------------------------------------------------- # The assurance constraint, Section 7.2 # -------------------------------------------------------------------------- @@ -276,6 +292,9 @@ def sign_bytes(self, data: bytes) -> bytes: curve_hash = hashes.SHA256() if self.alg == "ES256" else hashes.SHA384() der = self.private.sign(data, ec.ECDSA(curve_hash)) r, s = decode_dss_signature(der) + n = CURVE_ORDER[self.alg] + if s > n // 2: # emit the low-S form, see verify_bytes + s = n - s size = 32 if self.alg == "ES256" else 48 return r.to_bytes(size, "big") + s.to_bytes(size, "big") @@ -288,10 +307,29 @@ def verify_bytes(self, sig: bytes, data: bytes) -> None: raise InvalidSignature("bad JWS ECDSA signature length") r = int.from_bytes(sig[:size], "big") s = int.from_bytes(sig[size:], "big") + # RFC 7518 fixes the encoding (raw r||s) but not which of the two valid + # s values a verifier accepts. Accepting both lets anyone holding a + # valid signature mint a second one over the same bytes without the + # key, and a second signature entry changes vtc_hash (Section 6). The + # low half is enforced here ahead of the text; see CHOICES C9. + n = CURVE_ORDER[self.alg] + if s == 0 or s > n // 2: + raise InvalidSignature("ECDSA s is not in the low half of the curve order") curve_hash = hashes.SHA256() if self.alg == "ES256" else hashes.SHA384() self.public.verify(encode_dss_signature(r, s), data, ec.ECDSA(curve_hash)) +# Group orders of P-256 and P-384 (FIPS 186-4 D.1.2.3 and D.1.2.4), for the +# low-S rule in sign_bytes and verify_bytes. tools/validate.py proves both +# constants without a library by computing n * G on each curve and requiring +# the point at infinity. +CURVE_ORDER = { + "ES256": 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, + "ES384": int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF" + "581A0DB248B0A77AECEC196ACCC52973", 16), +} + + class KeyResolver: """Maps a `kid` to a public key. diff --git a/tools/validate.py b/tools/validate.py index 90d7473..49eb6c9 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -3,15 +3,16 @@ Checks performed: 1. Every example validates against its JSON Schema. - 2. cfb.task.spec_hash and vtc.task.spec_hash equal - sha256(JCS(taskspec.json)). - 3. cfb/vtc verification.criteria_hash equals the acceptance-instrument + 2. vtc.task.spec_hash equals sha256(JCS(taskspec.json)). + 3. vtc verification.criteria_hash equals the acceptance-instrument digest: sha256(JCS({relative path: sha256(bytes)})) over examples/acceptance-harness/. 4. taskspec.acceptance.harness_hash equals that same digest, so the instrument is committed from inside the TaskSpec as well as by the - CFB and the VTC. - 5. bid.commitment equals sha256(JCS(bid-reveal.reveal)). + VTC. + 5. delivery_hash in Verdict and Challenge equals sha256(JCS(delivery)) + with the signature member included, matching facilitator.py; the + v0.1.0 validator excluded it and the two tools disagreed. 6. vtc_hash in Delivery, Verdict, Challenge and Attestation equals sha256(JCS(vtc)) with the signatures member included (-01 Section 6). 7. Rules the schemas cannot express: parties are distinct, one @@ -23,6 +24,10 @@ carried in -01 Section 14. 10. Negative vectors, including the Section 13.3 conformance vectors that JSON Schema cannot express. + 11. Signature sets and ECDSA encoding, ahead of the text (facilitator + CHOICES C9): the P-256 and P-384 orders behind the low-S rule are + proved by computing n*G, an unsorted signature set is rejected + (V-21) and a high-S signature is rejected (V-22). Caveat on canonicalization: jcs() below is a restricted implementation of RFC 8785, correct for the value types these examples use (strings, @@ -45,6 +50,7 @@ interoperability with another implementation. """ import json, hashlib, sys, pathlib, base64 +import pactcore as pc # identifier normalization, the exact constraint, signature rules from jsonschema import Draft202012Validator from referencing import Registry, Resource @@ -189,16 +195,14 @@ def validate(example, schema_file, quiet=False): dlv["vtc_hash"] == vtc_hash) check("attestation.vtc_hash == sha256(JCS(vtc, signatures included))", att["vtc_hash"] == vtc_hash) -check("verdict.delivery_hash == sha256(JCS(delivery-sans-signature))", - vdt["delivery_hash"] == h(jcs({k: v for k, v in dlv.items() - if k != "signature"}))) +check("verdict.delivery_hash == sha256(JCS(delivery, signature included))", + vdt["delivery_hash"] == h(jcs(dlv))) check("delivery.evidence.instrument_hash == vtc.criteria_hash", dlv["evidence"]["instrument_hash"] == vtc["verification"]["criteria_hash"]) check("verdict.instrument_hash == vtc.criteria_hash", vdt["instrument_hash"] == vtc["verification"]["criteria_hash"]) -check("challenge.delivery_hash == sha256(JCS(delivery-sans-signature))", - chl["delivery_hash"] == h(jcs({k: v for k, v in dlv.items() - if k != "signature"}))) +check("challenge.delivery_hash == sha256(JCS(delivery, signature included))", + chl["delivery_hash"] == h(jcs(dlv))) check("challenge.proof.instrument_hash == vtc.criteria_hash", chl["proof"]["instrument_hash"] == vtc["verification"]["criteria_hash"]) @@ -260,43 +264,47 @@ def headers_well_formed(obj, typ): # Section 9.1: identifiers are normalized before comparison, and the # normalization folds toward "same party". A trailing separator, a case # variant, or surrounding whitespace must not make one party look like two. -def norm(identifier): - return identifier.strip().rstrip("/.#").lower() +# The function is pactcore's, so this validator and the Facilitator cannot +# disagree about who is who. The copy that lived here folded the whole +# identifier, which merges two did:web paths that differ only in case. +norm = pc.norm check("party comparison normalizes trailing separators and case", norm("did:web:X.example/") == norm("did:web:x.example")) check("normalized parties in the example are still distinct", norm(vtc["parties"]["buyer"]) != norm(vtc["parties"]["seller"])) +check("did:web path case is not folded: Section 9.1 folds scheme and host only", + norm("did:web:x.example:agents:A") != norm("did:web:x.example:agents:a")) print() print("== the assurance constraint (Section 7.2) ==") -def required_bond(price, q, released=0.0): - """B >= P(1-q)/q + E. The facilitator-checkable sufficient form.""" - return price * (1.0 - q) / q + released +# B >= P(1-q)/q + E, evaluated exactly by pactcore.assurance_holds: multiplied +# through by q, so no division and no rounding. The float copy that lived here +# carried a 1e-9 slack and passed a bond a hundredth of a cent short. +def holds(contract, released="0"): + return pc.assurance_holds(contract["price"]["amount"], + contract["liability"]["seller_bond"], + contract["assurance"]["q_min"], released) -def assurance_holds(contract, released=0.0): - P = float(contract["price"]["amount"]) - B = float(contract["liability"]["seller_bond"]) - q = float(contract["assurance"]["q_min"]) - return B + 1e-9 >= required_bond(P, q, released) +check("example contract satisfies the assurance constraint", holds(vtc)) +check("q_min 0.9091 requires B = 18.00 at P=180, so 17.99 fails", + pc.assurance_holds("180.00", "18.00", "0.9091") + and not pc.assurance_holds("180.00", "17.99", "0.9091")) - -check("example contract satisfies the assurance constraint", - assurance_holds(vtc)) - -# Worked figures from Section 14. P = 180.00, B = 18.00, E = 0. +# Worked figures from Section 14. P = 180.00, B = 18.00, E = 0. Each bound is +# checked from both sides, one cent apart. check("q_min 1.00 requires no bond at P=180", - abs(required_bond(180.0, 1.00)) < 1e-9) -check("q_min 0.9091 requires B ~= 18.00 at P=180", - abs(required_bond(180.0, 180.0 / 198.0) - 18.0) < 1e-6) -check("q_min 0.90 requires B = 20.00 at P=180, so 18.00 fails", - abs(required_bond(180.0, 0.90) - 20.0) < 1e-9) -check("q_min 0.50 requires B = 180.00 at P=180", - abs(required_bond(180.0, 0.50) - 180.0) < 1e-9) + pc.assurance_holds("180.00", "0.00", "1.00")) +check("q_min 0.90 requires B = 20.00 at P=180, so 19.99 fails", + pc.assurance_holds("180.00", "20.00", "0.90") + and not pc.assurance_holds("180.00", "19.99", "0.90")) +check("q_min 0.50 requires B = 180.00 at P=180, so 179.99 fails", + pc.assurance_holds("180.00", "180.00", "0.50") + and not pc.assurance_holds("180.00", "179.99", "0.50")) # Section 7.2: open assurance may not be the sole declared source. check("'open' is not the example's sole source of assurance", @@ -425,19 +433,19 @@ def rejects(name, schema_file, mutate): not headers_well_formed(_typ, "application/pact-contract+json")) _alias = json.loads(json.dumps(vtc)) -_alias["parties"]["seller"] = _alias["parties"]["buyer"].upper() + "/" -check("V-07 parties differing only by case and trailing '/' are rejected", +_alias["parties"]["seller"] = _alias["parties"]["buyer"] + "/" +check("V-07 parties differing only by a trailing '/' are rejected", norm(_alias["parties"]["buyer"]) == norm(_alias["parties"]["seller"])) _q = json.loads(json.dumps(vtc)) _q["assurance"] = {"mode": "committed-sample", "q_min": 0.90} check("V-12 B=18.00 at P=180.00 with q_min 0.90 fails the constraint", - not assurance_holds(_q)) + not holds(_q)) _q2 = json.loads(json.dumps(vtc)) _q2["assurance"] = {"mode": "certain", "q_min": 1.00} check("V-13 B=18.00 at P=180.00 with q_min 1.00 satisfies it", - assurance_holds(_q2)) + holds(_q2)) _noev = {k: v for k, v in dlv.items() if k != "evidence"} check("V-14 delivery without evidence is rejected", @@ -487,6 +495,70 @@ def finality_ok(child, parent): check("V-20 object carrying an undefined member is rejected", not validate(_u, "vtc.schema.json", quiet=True)) +print() +print("== signature sets and ECDSA encoding (facilitator CHOICES C9) ==") + +# The low-S rule needs the group order of each curve. These two checks prove +# the constants in pactcore.CURVE_ORDER by computing n * G in affine +# double-and-add, with no library: n * G is the point at infinity exactly when +# n is the order. Curve parameters from FIPS 186-4 D.1.2.3 and D.1.2.4. +P256 = dict(p=2**256 - 2**224 + 2**192 + 2**96 - 1, + gx=0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, + gy=0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5) +P384 = dict(p=2**384 - 2**128 - 2**96 + 2**32 - 1, + gx=int("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A38" + "5502F25DBF55296C3A545E3872760AB7", 16), + gy=int("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C0" + "0A60B1CE1D7E819D7A431D7C90EA0E5F", 16)) + + +def is_group_order(p, gx, gy, n): + a = p - 3 + + def add(P, Q): + if P is None: + return Q + if Q is None: + return P + (x1, y1), (x2, y2) = P, Q + if x1 == x2 and (y1 + y2) % p == 0: + return None + if P == Q: + lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p + else: + lam = (y2 - y1) * pow(x2 - x1, -1, p) % p + x3 = (lam * lam - x1 - x2) % p + return (x3, (lam * (x1 - x3) - y1) % p) + + acc = None + for bit in bin(n)[2:]: + acc = add(acc, acc) + if bit == "1": + acc = add(acc, (gx, gy)) + return acc is None + + +check("P-256 order constant behind the low-S rule is the group order (n*G = O)", + is_group_order(P256["p"], P256["gx"], P256["gy"], pc.CURVE_ORDER["ES256"])) +check("P-384 order constant behind the low-S rule is the group order (n*G = O)", + is_group_order(P384["p"], P384["gx"], P384["gy"], pc.CURVE_ORDER["ES384"])) + +_rev = dict(vtc, signatures=list(reversed(vtc["signatures"]))) +check("V-21 signature set not sorted by normalized kid is rejected", + pc.signatures_ordered(vtc)[0] and not pc.signatures_ordered(_rev)[0]) + +# A high-S encoding is refused before any key is consulted, so the vector +# needs no key material and no `cryptography`. +_k = pc.Key(kid="did:web:v.example#k", alg="ES256", private=None, public=None) +_high = b"\x01" * 32 + (pc.CURVE_ORDER["ES256"] - 1).to_bytes(32, "big") +try: + _k.verify_bytes(_high, b"") + _high_s_refused = False +except pc.InvalidSignature: + _high_s_refused = True +check("V-22 ECDSA signature with s in the high half of the order is rejected", + _high_s_refused) + print() if fails: print(f"{len(fails)} check(s) FAILED") From 7796ed941c57f1c2ec99761205219ab53e1680e8 Mon Sep 17 00:00:00 2001 From: Laxmikant Sharma Date: Wed, 16 Sep 2026 14:26:06 -0700 Subject: [PATCH 2/4] draft-laxsharma-pact-02: the protocol without the terms Revision -02 of the Internet-Draft, built from plan version 2 of 16 September 2026 after the dispatch@ thread. The document now specifies records, digests, signer rules, a state machine over a Facilitator-signed event trace, the five endpoints and the Merkle commitment; settlement terms are carried by reference to a profile (URI, bundle digest, opaque parameters) and the -01 terms become a non-normative example profile in Appendix A. Work Attestation is renamed Outcome Record; every response is a signed Contract Status; contract trees work across Facilitators; media types move to the vendor tree; no registries. Gates: xml2rfc text and HTML build clean (stream warning only); no line over 72 columns; idnits 0 errors, 0 flaws (remaining warnings are the vendor-tree media type names read as FQDNs and aligned columns in artwork); scope lint zero party-directed requirements and zero Facilitator rules about value. Digests in Section 15 and the figures are placeholders until the 0.2 examples exist; the validator count in Section 16 likewise. --- .github/workflows/ci.yml | 1 + Makefile | 1 + draft/draft-laxsharma-pact-02.html | 5924 ++++++++++++++++++++++++++++ draft/draft-laxsharma-pact-02.txt | 4032 +++++++++++++++++++ draft/draft-laxsharma-pact-02.xml | 2964 ++++++++++++++ 5 files changed, 12922 insertions(+) create mode 100644 draft/draft-laxsharma-pact-02.html create mode 100644 draft/draft-laxsharma-pact-02.txt create mode 100644 draft/draft-laxsharma-pact-02.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbc9563..b75138a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,3 +18,4 @@ jobs: - run: pip install xml2rfc - run: xml2rfc --text draft/draft-laxsharma-pact-00.xml - run: xml2rfc --text draft/draft-laxsharma-pact-01.xml + - run: xml2rfc --text draft/draft-laxsharma-pact-02.xml diff --git a/Makefile b/Makefile index 4dd7a27..6013d7c 100644 --- a/Makefile +++ b/Makefile @@ -3,5 +3,6 @@ all: validate draft draft: xml2rfc --text --html draft/draft-laxsharma-pact-00.xml xml2rfc --text --html draft/draft-laxsharma-pact-01.xml + xml2rfc --text --html draft/draft-laxsharma-pact-02.xml validate: python3 tools/validate.py diff --git a/draft/draft-laxsharma-pact-02.html b/draft/draft-laxsharma-pact-02.html new file mode 100644 index 0000000..ad428ef --- /dev/null +++ b/draft/draft-laxsharma-pact-02.html @@ -0,0 +1,5924 @@ + + + + + + +PACT: Co-Signed Task Contracts, Delivery and Verdict Records, and Outcome Records for Autonomous Agents + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Internet-DraftPACTSeptember 2026
SharmaExpires 20 March 2027[Page]
+
+
+
+
Workgroup:
+
Network Working Group
+
Internet-Draft:
+
draft-laxsharma-pact-02
+
Published:
+
+ +
+
Intended Status:
+
Experimental
+
Expires:
+
+
Author:
+
+
+
L. Sharma
+
Independent
+
+
+
+
+

PACT: Co-Signed Task Contracts, Delivery and Verdict Records, and Outcome Records for Autonomous Agents

+
+

Abstract

+

Autonomous agents can already prove who they are, show whose + authority they act under, find one another, call one another, and pay. + What they cannot do with any existing specification is agree on a task + in a form a third party can check, deliver against it, have the + delivery judged by someone other than the performer, and carry away a + record of the outcome that a stranger can verify. This document + specifies PACT, a set of signed JSON records that closes that gap.

+

PACT defines four things: a co-signed task contract whose digest + covers its signature set, so the commitment proves who agreed and not + only what was written; a Verdict record bound by digest to the Delivery + record it judges; a Facilitator-signed event trace and Outcome Record + for every contract, so what happened is recorded once, in one order, + by a party that is not the performer; and a Merkle commitment from a + parent contract's Outcome Record to the Outcome Records of its + subcontracts.

+

Settlement terms are carried by reference to a profile defined + outside this document. This document specifies no escrow, custody or + release of value, and takes no position on the legal effect of any + record it defines.

+
+
+
+

+Status of This Memo +

+

+ This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79.

+

+ Internet-Drafts are working documents of the Internet Engineering Task + Force (IETF). Note that other groups may also distribute working + documents as Internet-Drafts. The list of current Internet-Drafts is + at https://datatracker.ietf.org/drafts/current/.

+

+ Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress."

+

+ This Internet-Draft will expire on 20 March 2027.

+
+
+ +
+
+ ▲

+Table of Contents +

+ +
+
+
+
+

+1. Introduction +

+
+
+

+1.1. Motivation +

+

By late 2026 an autonomous agent can prove who it is, show whose + authority it acts under, discover another agent, call it, record what + happened in a tamper-evident receipt, and pay for the call. Each of + those is the subject of active standardisation, and several are + specified in more detail than this document specifies anything.

+

What none of them provides is interoperability at the level of the + task. Two agents built by different vendors have no common record of + what one asked the other to do, no common form for the result, no way + to have that result judged by a third implementation against criteria + fixed before the work began, and no record of the outcome that a + fourth implementation can verify without trusting any of the first + three. Receipts record that an action occurred. Audit records + establish whether behaviour matched intent. Payment schemes move value + on the payer's instruction. None of them says what was agreed, what + was delivered, or whether the one met the other.

+

That gap is not an oversight in those documents; it is outside + their scope, and correctly so. It is the gap this document + addresses, and only that gap.

+
+
+
+
+

+1.2. What This Document Specifies, and What It Does Not +

+

PACT specifies exactly four things: a co-signed contract record + whose digest covers its signature set (Section 5); a + Delivery record and the Verdict record bound to it by digest + (Section 6, Section 7.2); an event + trace, signed by a Facilitator, from which one Outcome Record per + contract is produced (Section 11, + Section 12); and a Merkle commitment from a parent's + Outcome Record to its children's (Section 10).

+

A contract names its settlement terms by reference: a profile + identifier, a digest over the profile's bytes, and a parameter object + that this document does not read (Section 5.3). What those + terms mean, and everything about who holds or moves value under them, + is the profile's to say. This document specifies the records, their + digests, who signs each one, the order in which a Facilitator records + events, and a commitment across records. That is the whole of it.

+

A deployment relies on other specifications, agreements or + arrangements for: the meaning of the terms a contract names; agent + identity and key distribution; delegation of authority from a human or + organisational principal; agent discovery; transport security beyond + [RFC9325]; an audit or accountability architecture; a + transparency service; a payment rail or settlement network; a + reputation system; and the resolution of any disagreement the records + do not settle.

+

Carrying terms by reference is an old pattern in this series. + ACME [RFC8555] carries a terms-of-service URL and + requires a client to assert agreement to it before an account is + created, without defining a single term. A certificate carries its + policy as an identifier whose rules live outside the IETF + ([RFC5280], Section 4.2.1.4), and the framework for + writing those rules [RFC3647] says it does not aim to + provide legal advice. The Internet Open Trading Protocol + [RFC2801] specified the messages of a trade and left + the trade's terms to the parties. PACT follows that line.

+

Two mechanisms present in the -00 revision remain withdrawn: + contract channels, and the sealed-bid award procedure. The reasons are + recorded in [I-D.laxsharma-pact-01] and are not + repeated. The change from -01 to this revision is listed in + Appendix B.

+
+
+ +
+
+

+1.4. The Experiment +

+

This document is Experimental. The question it tests is stated + over protocol observables only. Given the same sequence of posted + records and the same clock readings, two independent Facilitator + implementations should produce the same event trace + (Section 11). Given the same trace and the same terms + profile, they should produce the same Outcome Record body + (Section 12), byte for byte after canonicalization. + The experiment succeeds if two independent Facilitators, serving + Buyers and Sellers built by different implementers, reach every + terminal state in Figure 2 with Outcome Records + either can verify and that agree. It fails, and that would itself be + worth recording, if the trace turns out to under-determine the + outcome, which is to say if two honest implementations reading the + same records disagree about what happened. Experience should be + reported to the author and to the repository named in + Section 16. The non-normative profile in + Appendix A exists so that the experiment can + be run before any other profile is written.

+
+
+
+
+
+
+

+2. Conventions and Definitions +

+

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only + when, they appear in all capitals, as shown here.

+

Canonical form. Every JSON object defined here is canonicalized with + JCS [RFC8785] before hashing or signing. Implementations + MUST order object keys by UTF-16 code unit as + [RFC8785] Section 3.2.3 requires. Sorting by Unicode code + point is a common substitution; it agrees with the required order + throughout the Basic Multilingual Plane and diverges above it.

+

Object digest. The digest of an object is the string + sha256: followed by the lowercase hexadecimal SHA-256 of the + canonical form of the whole object, including every signature member it + carries. Every hash member in this document that names another object + (vtc_hash, delivery_hash, challenge_hash, + the object member of a trace entry, and the leaves of + Section 12.2) is that object's digest. A digest that + excluded signatures would prove what was written and not who agreed to + it; the -00 revision had that defect and the -01 revision fixed it for + the contract only. This revision applies one construction + everywhere.

+

Signing input. A signature over an object is computed over the + canonical form of the object with the signing member (signature + or signatures) removed, as Section 14.1 + specifies. The digest of an object and the signing input of an object + are therefore different byte strings, and the difference is the + signature set.

+

Version. The pact member carries a version of the form + major.minor; this document defines 0.2. Every object defined here is + hash-committed and signed, so a member an implementation does not + recognise is inside the commitment and cannot be ignored safely. An + implementation MUST reject an object whose pact version it + does not implement, and MUST reject an object carrying a member this + document does not define for it, with one exception: the contents of + terms.parameters (Section 5.3) are defined by the + named profile and this document reads none of them. Extension is by a + new version, not by adding members.

+

Time. Every timestamp is an RFC 3339 date-time + [RFC3339] in UTC with the "Z" designator. The + Facilitator's clock governs every deadline and window in this + document: the instant at which the Facilitator records an event is the + instant that counts, that instant is what the trace carries, and + parties should allow for skew when acting near a boundary. + Section 17.1 says what that clock can and cannot + prove.

+

Amounts. An amount is a decimal string with no exponent and a + fractional part of two to eighteen digits; comparisons are exact and + no rounding is implied. A currency is an asset identifier whose + namespace is defined by the settlement binding named in + price.settlement, and need not be an ISO 4217 code. A network + is a ledger identifier in the form the same binding defines. This + document carries amounts; it does not say what any amount is for. Where + a record produced under this document lists amounts, as + terms_result does (Section 12.1), the meaning + of every entry is the named profile's.

+

Identifiers. A party identifier is a URI. Two identifiers name the + same party when they are equal after the normalization in + Section 9.1, and every comparison of identifiers in + this document is made after that normalization.

+
+
+

+2.1. Terminology +

+

Four words in this document have meanings elsewhere that are + close enough to mislead, and are defined here once.

+
+
Contract:
+
Used in this document for a co-signed JSON + object of the form in Section 5, and for nothing else. + This document takes no position on whether any such object is a + contract in law, in any jurisdiction, and defines no obligation + between the parties that sign one. +
+
+
Verifier, Verdict:
+
A Verifier here is the party that + evaluates a Delivery against the instrument the contract committed + to, and a Verdict is its signed finding. This is not the Verifier + of [RFC9334], which appraises Evidence about an + Attester; the two roles may be played by the same software in a + hardware-attested tier, and are still different roles. +
+
+
Evidence:
+
The evidence member of a Delivery is + the set of artefacts a Verifier evaluates, produced by the Seller. + It is not Evidence in the sense of [RFC9334]. The + member name is kept from -01 because renaming it would change every + committed digest for no gain in clarity that this note does not + provide. +
+
+
Facilitator:
+
The party that runs the state machine of + Section 4 for a contract: it accepts or refuses the + records posted to it, records events in one order on its own clock, + and signs the trace and the Outcome Record. Nothing in this document + says that a Facilitator holds anything of value, and nothing in it + requires that it does. +
+
+
+

The remaining roles are defined by what they sign and receive in + Section 3.10, and the objects by their members in + Section 3.

+
+
+
+
+
+
+

+3. Data Dictionary +

+

This section lists every member this document defines, by the object + that carries it, with its type, whether it is required in that object, + and what it commits to. It is a dictionary and not a rulebook: the rule + that a record omitting a required member, or carrying one this document + does not define for it, does not conform is stated once in + Section 2; the rules a Facilitator applies when it + accepts or refuses a record are in Section 14 and in + the section that defines the record. No sentence in this section + requires anything of any party. Where a member's meaning is the named + terms profile's, the entry says so and says nothing more.

+

Types are JSON types. A digest is a string of the form in + Section 2. An amount is a string of the form in + Section 2. A URI is a string. A timestamp is a + string of the form in Section 2. Cardinality is + written as required or optional.

+
+
+

+3.1. Members Common to Every Record +

+
+
+pact:
+
string, required. The protocol version; + 0.2 for objects defined by this document. +
+
+
+type:
+
string, required. The object's type name: + VerifiableTaskContract, Delivery, + Verdict, Challenge, ContractStatus, + OutcomeRecord, or FacilitatorCapabilities. +
+
+
+signature:
+
object, required in Delivery, + Verdict, Challenge, ContractStatus and the capability document. One + JWS entry of the form in Section 14.1, by the single + signer of that record. +
+
+
+signatures:
+
array of objects, required in the + contract and in the Outcome Record. JWS entries of the form in + Section 14.1, sorted as that section says. Commits, in + the contract, to who agreed; in the Outcome Record, to which + Facilitator issued it. +
+
+
+
+
+
+
+

+3.2. Contract Members +

+

Carried in the Verifiable Task Contract (Section 5), + media type application/vnd.pact.contract+json.

+
+
+id:
+
string, required. Contract identifier, + unique among the contracts of the Facilitator named in + parties.facilitator. +
+
+
+parties:
+
object, required. The identifiers of + the parties, by role: buyer (URI, required), + seller (URI, required), facilitator (URI, + required), verifier (URI, optional). Commits to who plays + each role for this contract. +
+
+
+task:
+
object, required. spec_hash + (digest, required) commits to a TaskSpec (Section 5.2); + spec_uri (URI, optional) says where its bytes may be + fetched; deadline (timestamp, required) is the instant + after which the deadline-passed event may be recorded + (Section 4.2). +
+
+
+price:
+
object, required. amount + (amount, required), currency (string, required), + settlement (URI, required, naming a settlement binding), + network (string, required, in the form the binding + defines). Commits to a figure and a venue that both parties signed. + The meaning of the figure is the named terms profile's. +
+
+
+verification:
+
object, required. tier + (string, required), profile (string or URI, required; + Section 9), criteria_hash (digest, + required; the manifest digest of the acceptance instrument per + Section 5.1), max_verdict_seconds (integer, + required; the longest interval after delivered within + which a first Verdict is recorded before verdict-lapsed + may be), arbiter (URI, optional). Commits to how a + Delivery is judged and by what. +
+
+
+flow:
+
string, required. One of + verdict-first, delivery-first, no-window + (Section 7.1). Selects the shape of the state machine + for this contract. +
+
+
+terms:
+
object, required + (Section 5.3). profile (URI, required) names + a terms profile; profile_hash (digest, required) commits + to the profile's bytes as Section 5.3 says; + parameters (object, required, may be empty) carries the + profile's parameters. This document reads no member of + parameters; every one of them means what the named + profile says. +
+
+
+challenge:
+
object, required. + window_seconds (integer, required, greater than zero) is the + duration of the challenge window; max_dispute_seconds + (integer, required) is the longest interval after a + challenge event within which a Verdict on that Challenge + is recorded before dispute-lapsed may be. +
+
+
+parent:
+
object, optional; present only in a + subcontract (Section 10). vtc_id (string, + required), vtc_hash (digest, required), and + facilitator (URI, required) identify the parent contract + and the Facilitator that holds it. +
+
+
+
+
+
+
+

+3.3. TaskSpec Members +

+

The TaskSpec is the content committed by task.spec_hash + (Section 5.2). It is not transmitted over the + endpoints of this document.

+
+
+description:
+
string, required. A statement of + the work in natural language. +
+
+
+inputs:
+
object, optional. schema_uri + with schema_hash, and where a representative sample is + published, sample_uri with sample_hash; each URI + with its digest over the dereferenced bytes. +
+
+
+deliverable:
+
object, required. format + (string) and schema_uri with schema_hash. +
+
+
+acceptance:
+
object, required. The verification + instrument: harness_uri with harness_hash for + re-execution tiers, enclave and model policy for attestation tiers, + a proof statement with its verifying key for proving tiers, or + rubric_uri with rubric_hash for judgment tiers; + plus thresholds (object) in machine-readable form. + harness_hash equals the contract's + criteria_hash. +
+
+
+constraints:
+
object, optional. Tool + prohibitions, confidentiality and compliance conditions, in a form + this document does not define. +
+
+
+
+
+
+
+

+3.4. Delivery Members +

+

Carried in the Delivery (Section 6), media type + application/vnd.pact.delivery+json.

+
+
+vtc_id, vtc_hash:
+
string and digest, + required. Identify and commit to the contract performed. +
+
+
+work_hash:
+
digest, required. Commits to the + delivered bytes, or to a manifest per Section 5.1 where + the deliverable is a bundle. +
+
+
+work_uri:
+
URI, optional. Where the bytes may be + fetched, subject to Section 17.5. +
+
+
+input_hash:
+
digest, required for tiers whose + fraud proof re-executes. Commits to the production input actually + consumed. +
+
+
+evidence:
+
object, required. Members profiled by + verification.tier and verification.profile; for + the acceptance profile, profile, + instrument_hash, results_hash and + results_uri. Conformance to the profile is a validity + condition of the Delivery, not a judgement on the work. +
+
+
+
+
+
+
+

+3.5. Verdict Members +

+

Carried in the Verdict (Section 7.2), media type + application/vnd.pact.verdict+json.

+
+
+vtc_id:
+
string, required. +
+
+
+delivery_hash:
+
digest, required. Commits to the + Delivery judged, including the Seller's signature over it. +
+
+
+challenge_hash:
+
digest, optional. Present when + the Verdict answers a Challenge; commits to that Challenge. +
+
+
+outcome:
+
string, required. PASS or + FAIL. +
+
+
+profile, instrument_hash:
+
string and + digest, required. The verification profile applied and the digest of + the instrument actually run, which equals the contract's + criteria_hash. +
+
+
+results_hash:
+
digest, required. Commits to the + Verifier's own results. +
+
+
+evaluated_at:
+
timestamp, required. The + Verifier's own clock; informational, since the trace carries the + Facilitator's. +
+
+
+
+
+
+
+

+3.6. Challenge Members +

+

Carried in the Challenge (Section 7.3), media type + application/vnd.pact.challenge+json.

+
+
+vtc_id, delivery_hash:
+
string and + digest, required. Identify the contract and commit to the Delivery + challenged. +
+
+
+proof:
+
object, required. Members profiled by + verification.profile; for the acceptance profile, + profile, instrument_hash, results_hash, + results_uri and failing_checks (array of + strings). +
+
+
+costs:
+
object, optional. amount and + currency: a figure the Challenger asserts for producing the + proof. This document records it in the trace and reads it for + nothing; its meaning is the named terms profile's. +
+
+
+
+
+
+
+

+3.7. Contract Status Members +

+

Carried in the Contract Status (Section 11), media + type application/vnd.pact.status+json, the Facilitator's + signed response to every accepted request.

+
+
+vtc_id, vtc_hash:
+
string and digest, + required. +
+
+
+state:
+
string, required. A state name from + Figure 2. +
+
+
+trace:
+
array of objects, required. The event + trace so far, in the order recorded (Section 4.2). Each + entry carries event (string, required), at + (timestamp, required), object (digest, required where the + event was caused by a posted record), and the event-specific + members listed in Section 4.2. +
+
+
+issued_at:
+
timestamp, required. When this + status was signed. +
+
+
+
+
+
+
+

+3.8. Outcome Record Members +

+

Carried in the Outcome Record (Section 12), media + type application/vnd.pact.outcome+json.

+
+
+vtc_id, vtc_hash:
+
string and digest, + required. +
+
+
+parties:
+
object, required. The contract's + parties object, copied, so that the record names its + subjects and which side of the contract each was on. +
+
+
+outcome:
+
object, required. state + (string, required; FINAL, SETTLED or + ABANDONED) and challenge_upheld (boolean, + required). +
+
+
+work_hash:
+
digest, required where a Delivery was + recorded. Binds the record to what was produced. +
+
+
+trace:
+
array of objects, required. The complete + event trace, ending with the terminal event. +
+
+
+terms_result:
+
object, required + (Section 12.1). profile and + profile_hash (copied from the contract), currency + (string), and transfers (array of objects), each with + from (string), to (string), amount + (amount) and code (string). The entries are the named + profile's output for the trace; this document defines their form + and two arithmetic invariants over them, and nothing about their + meaning. +
+
+
+children_merkle_root:
+
digest, required where the + contract has registered children and absent otherwise + (Section 12.2). +
+
+
+
+
+
+
+

+3.9. Capability Document Members +

+

Carried in the Facilitator capability document + (Section 8), media type + application/vnd.pact.facilitator+json.

+
+
+facilitator:
+
URI, required. The identifier that + appears in parties.facilitator. +
+
+
+settlement_bindings:
+
array of objects, + required. Each with id (URI), networks and + assets (arrays of strings). +
+
+
+flows:
+
array of strings, required. The flows + of Section 7.1 the Facilitator implements. +
+
+
+verification_profiles:
+
array of strings, + required. +
+
+
+terms_profiles:
+
array of objects, required, + with at least one entry. Each with id (URI) and + profile_hash (digest): the terms profiles, at the + revisions named, whose schedules this Facilitator evaluates. +
+
+
+max_contract_value:
+
object, optional. + amount and currency. +
+
+
+challenge_deposit:
+
object, optional. + amount and currency; see + Section 7.3. +
+
+
+endpoints:
+
object, required. Maps each endpoint + name in Section 13 to an absolute URI. +
+
+
+
+
+
+
+

+3.10. Roles +

+

A role is defined by where its identifier appears, what it signs, + and what it receives. Nothing else about a role is defined here.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Table 1: +Roles, by what each signs and receives +
RoleIdentifier appears inSignsReceives
Buyer + parties.buyer +the contract; a child registration + (Section 10.2)Contract Status, Outcome Record
Seller + parties.seller +the contract; the DeliveryContract Status, Outcome Record
Facilitator + parties.facilitator, + parent.facilitator, the capability documentContract Status, Outcome Record, the capability + documentevery posted record
Verifier + parties.verifier, or the + kid of a Verdictthe Verdictthe Delivery and, on a Challenge, the Challenge
Challengerthe kid of a Challengethe ChallengeContract Status
+
+

One identifier may play more than one role across contracts, and + Section 9.1 says which combinations within one + contract a Facilitator refuses.

+
+
+
+
+
+
+

+4. Protocol Overview +

+

A contract passes through four phases. Propose establishes the + record. Agree co-signs it and a Facilitator accepts it. Complete + produces a Delivery and a Verdict on it. Record produces an Outcome + Record. Every step after Agree is an event the Facilitator records on + its own clock, in one order, and the sequence of those events is the + contract's trace. The trace is the protocol's central object: the state + machine is defined over it, every response a Facilitator gives carries + the prefix recorded so far, and the Outcome Record carries the whole of + it.

+
+
+
+
+ Buyer            Facilitator            Seller          Verifier
+   |                   |                    |                |
+   |<==== contract negotiated and co-signed ==>|             |
+   |                   |                    |                |
+   |-- POST contract ->|                    |                |
+   |<-- Status --------|   [ accepted ]     |                |
+   |                   |   [ funded ]       |                |
+   |                   |                    |                |
+   |                   |        [ Seller performs ]          |
+   |                   |<-- POST Delivery --|                |
+   |                   |-- Status --------->|  [ delivered ] |
+   |                   |                    |                |
+   |                   |---- Delivery, criteria_hash ------->|
+   |                   |<--- POST Verdict -------------------|
+   |                   |---- Status ------------------------>|
+   |                   |   [ verdict PASS ]  [ window-opened ]
+   |                   |   [ window-closed ] [ children-final ]
+   |                   |   [ terminal FINAL ]                |
+   |                   |                    |                |
+   |<-- Outcome Record-|-- Outcome Record ->|                |
+
+
+
Figure 1: +Message flow under the verdict-first flow, without a Challenge +
+
+

Every accepted request is answered with a Contract Status + (Section 11), a Facilitator-signed object carrying the + state and the trace so far. Nothing in the figure moves value, and no + arrow in it is named for a movement of value. What a terms profile does + at each bracketed event is the profile's, and it is reported once, in + the Outcome Record, as a list the profile produced and the Facilitator + signed.

+
+
+

+4.1. States +

+
+
+
+
+ ACCEPTED -funded-> FUNDED -delivered-> DELIVERED
+    |                 |                    |
+    | deadline-       | deadline-          | window-opened
+    | passed          | passed             v
+    |                 |          WINDOW_OPEN <------+
+    |                 |            |       |        |
+    |                 |  challenge |       | window | verdict PASS
+    |                 |            v       | closed | on it, or
+    |                 |         DISPUTED----|--------+ dispute-
+    |                 |            |       |          lapsed
+    |                 |    verdict |       |
+    |                 |    FAIL    |       |
+    v                 v            v       v
+  +--------------------------------------------------+
+  |                AWAITING_CHILDREN                 |
+  +--------------------------------------------------+
+                           | children-final, then terminal
+                           v
+           FINAL        SETTLED        ABANDONED
+
+
+
Figure 2: +Contract states +
+
+

The figure omits three arrows that the table carries: a FAIL + Verdict recorded in DELIVERED or in WINDOW_OPEN also leads to + AWAITING_CHILDREN; under the no-window flow DELIVERED leads + there directly; and a Verdict that is late (verdict-lapsed) + opens the window without one. FINAL, SETTLED and ABANDONED are + terminal and each produces exactly one Outcome Record. The -01 + revision named one of these states for a movement of value; no state + here is.

+

The state named PROPOSED in earlier revisions is gone. Between the + parties' signatures and the Facilitator's acceptance a contract exists + only on the parties' side, so no Facilitator could observe that state + and the reference implementation never reported it.

+
+
+
+
+

+4.2. Events +

+

A trace entry is a JSON object with event (one of the + names below), at (the Facilitator's clock when it was + recorded), object where the entry records a posted record + (that record's digest, Section 2), and the members + listed for the event. A Facilitator MUST record the entries of one + contract in the order it recorded them and MUST NOT reorder, remove + or alter an entry once a Status carrying it has been issued + (Section 17.1 says what that rule does and does + not prove).

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Table 2: +Events: the state each is recorded in, the state that follows, and what the entry carries +
EventRecorded in; thenMembers and condition
+ accepted +none; then ACCEPTED + object is vtc_hash. The contract passed Section 13.1.
+ funded +ACCEPTED; then FUNDED + ref (string, optional, in the form the settlement binding defines). Recorded when every account the named terms profile requires shows finality on the settlement binding named in price.settlement; how a Facilitator observes that is the binding's to say, and this is the only sentence in this document that mentions an account.
+ deadline-passed +ACCEPTED or FUNDED; then AWAITING_CHILDREN + task.deadline has passed with no delivered entry.
+ delivered +FUNDED; then DELIVERED + object is the Delivery's digest. The Delivery passed Section 6.
+ window-opened +DELIVERED; then WINDOW_OPENUnder delivery-first, immediately after delivered; under verdict-first, immediately after a PASS verdict or after verdict-lapsed. closes_at (timestamp, required) is at plus challenge.window_seconds.
+ verdict +DELIVERED, WINDOW_OPEN or DISPUTED; then see the condition + object is the Verdict's digest; outcome (PASS or FAIL); answers (digest of the Challenge, when the Verdict carries challenge_hash); supersedes (digest of the Verdict it replaces, when one stood). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending.
+ verdict-lapsed +DELIVERED; then WINDOW_OPENUnder verdict-first, verification.max_verdict_seconds have passed since delivered with no verdict. window-opened follows.
+ challenge +WINDOW_OPEN or DISPUTED; then DISPUTED + object is the Challenge's digest; costs copied from the Challenge when present. The Challenge passed Section 7.3 before closes_at.
+ dispute-lapsed +DISPUTED; then WINDOW_OPEN + object is the Challenge's digest. challenge.max_dispute_seconds have passed since that challenge entry with no Verdict answering it. Leads to WINDOW_OPEN once no Challenge is pending; the earlier Verdict, if any, stands.
+ window-closed +WINDOW_OPEN; then AWAITING_CHILDREN + closes_at has passed and no Challenge is pending. The window is never extended: a dispute that outlasts it delays this entry and does not move closes_at.
+ child-registered +any non-terminal; unchanged + object is the child contract's digest; facilitator (URI). Section 10.2.
+ child-final +any non-terminal; unchanged + object is the child's Outcome Record digest; child (the child contract's digest).
+ child-unresolved +any non-terminal; unchanged + child (the child contract's digest). The child's latest finality instant (Section 10.3) has passed and no Outcome Record for it is held.
+ children-final +AWAITING_CHILDREN; then terminal followsEvery registered child has a child-final or child-unresolved entry. A contract with no registered children records this entry on entering AWAITING_CHILDREN.
+ terminal +AWAITING_CHILDREN; then FINAL, SETTLED or ABANDONED + state (the terminal state) and challenge_upheld (boolean). ABANDONED where deadline-passed was recorded; SETTLED where the standing Verdict is FAIL, with challenge_upheld true when that Verdict answers a Challenge; FINAL otherwise.
+
+

The standing Verdict is the last verdict entry in the + trace that no later entry supersedes. A Challenge is pending from its + challenge entry until a verdict entry answers it or + a dispute-lapsed entry names it.

+

Every instant in the table is read from the Facilitator's clock, + and an entry conditioned on an instant having passed is recorded at + the first opportunity after it, which need not be that instant. Two + Facilitators given the same posted records with the same clock + readings record the same trace; that is the determinism the + experiment in Section 1.4 tests, and the reason + every condition above is stated over the trace and the clock and + nothing else.

+
+
+
+
+
+
+

+5. The Verifiable Task Contract +

+

A VTC is a JSON object, media type + application/vnd.pact.contract+json, with the members in + Section 3.2. A VTC is valid only if every required member + is present, the parties are distinct, and both the Buyer and the Seller + have contributed exactly one signature that verifies against a key bound + to its identifier (Section 14.2). The Facilitator and any + Verifier do not sign the VTC; their assent is expressed by acting on it, + and a Facilitator that will not act on a contract refuses it at + Section 13.1.

+

The settlement identifier, the network and the asset are all + carried inside price so that a co-signed VTC is bound to one + venue. The -00 revision omitted them, which made a signed contract + replayable against any facilitator, chain or token contract.

+
+
+
+
+{
+  "pact": "0.2",
+  "type": "VerifiableTaskContract",
+  "id": "vtc_7f3a91",
+  "parties": {
+    "buyer":       "did:web:acme.example",
+    "seller":      "did:web:dataforge.example",
+    "facilitator": "did:web:settle.example",
+    "verifier":    "did:web:audit.example"
+  },
+  "task": {
+    "spec_hash": "sha256:<spec_hash>",
+    "deadline":  "2026-11-14T00:00:00Z"
+  },
+  "price": {
+    "amount":     "180.00",
+    "currency":   "USDC",
+    "settlement": "https://settle.example/bindings/ledger-1",
+    "network":    "eip155:8453"
+  },
+  "verification": {
+    "tier":                "T0-reexec",
+    "profile":             "acceptance",
+    "criteria_hash":       "sha256:<criteria_hash>",
+    "max_verdict_seconds": 86400
+  },
+  "flow": "verdict-first",
+  "terms": {
+    "profile":
+      "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution",
+    "profile_hash": "sha256:<profile_hash>",
+    "parameters":   { "...": "the profile's; not read here" }
+  },
+  "challenge": {
+    "window_seconds": 3600,
+    "max_dispute_seconds": 86400
+  },
+  "signatures": [ { "protected": "...", "signature": "..." },
+                  { "protected": "...", "signature": "..." } ]
+}
+
+
+
Figure 3: +A Verifiable Task Contract, signatures abbreviated +
+
+

Digests are elided here; the reference repository's values are in + Section 15. The parameters object is shown + elided on purpose: nothing in this document depends on what is in + it.

+
+
+

+5.1. Hash Commitments and Content Conveyance +

+

Every URI carried inside hash-committed content MUST be accompanied + by a sibling hash over the dereferenced bytes. The -00 revision + committed harness_uri as a string while leaving the bytes at + that URI uncommitted, which permitted a Buyer to substitute the + acceptance instrument after signature, run the substituted + instrument, and submit the failure as a valid fraud proof. The -01 + revision stated the rule and its own reference TaskSpec broke it for + three of four URIs; this revision's example carries all four + sibling hashes, and the validator checks each.

+

Where the committed content is a bundle of files rather than a + single octet stream, the commitment MUST be computed as + SHA-256(JCS(M)) where M is an object mapping each file's + path, relative to the bundle root and expressed with "/" separators, + to SHA-256 of its bytes, over every file in the bundle. A + manifest of per-file digests is specified rather than an archive + digest because archive formats carry ordering, timestamp and + permission metadata that is not stable across producers. The same + construction commits to a terms profile (Section 5.3).

+
+
+
+
+

+5.2. The Task Specification +

+

The content committed by spec_hash is a TaskSpec: a JSON + object with the members in Section 3.3, + canonicalized per [RFC8785] before hashing. It is not + transmitted over the endpoints of this document; the parties exchange + it before signing, and spec_uri may say where.

+

The acceptance object MUST carry the members required for + the contract's tier: harness_uri and harness_hash + for re-execution tiers, enclave and model policy for attestation + tiers, a proof statement with its verifying key for proving tiers, or + rubric_uri and rubric_hash for judgment tiers. An + empty acceptance object MUST be rejected. The -00 revision's + schema permitted one, which made every fraud proof impossible.

+

Thresholds MUST be stated so that they cannot be satisfied by + returning almost nothing. A threshold expressed only as a rate over + returned rows is satisfied by returning one correct row out of + millions; a completeness condition relative to the committed input is + therefore required wherever the deliverable is a transformation of + that input.

+
+
+
+
+

+5.3. Terms +

+

The terms member names the settlement terms both parties + signed, by reference. It carries a profile URI, a + profile_hash, and a parameters object. This document + defines no obligation between parties and takes no position on the + legal effect of any object it defines; what the named profile says the + parties have agreed to, and what any of it means between them, is the + profile's and its authors' to say.

+

profile_hash is the manifest digest of + Section 5.1 over the profile's bundle. A bundle usable + with this document contains at least three files: the profile's prose, + parameters.schema.json, a JSON Schema + [I-D.bhutton-json-schema] for the parameters object, and + vectors.json, whose form Section 12.1 + gives. A digest over prose alone would commit the parties to bytes and + not to behaviour; the schema and the vectors are what make two + implementations of the profile checkable against each other.

+

A Facilitator MUST refuse a contract whose terms.profile + and terms.profile_hash do not match an entry in the + terms_profiles array of its own capability document + (Section 8), so that no party signs terms the + Facilitator will not evaluate, and MUST refuse a contract whose + parameters do not validate against the named profile's + parameters.schema.json. It reads parameters for no + other purpose. The rule of Section 2 that an + undefined member is rejected does not apply inside + parameters; the profile's schema governs there.

+

A profile usable with this document defines, in its prose, a + schedule: a total, deterministic function from a contract and a trace + prefix (Section 4.2) to the list of entries the profile + emits at the last event of that prefix, in the form of + Section 12.1. Total means every event in + Table 2 has a defined result, including the ones a + profile author would rather not think about: a lapsed dispute, an + unresolved child, a contract abandoned before it was funded. + Deterministic means the result depends on the contract, the trace and + nothing else, so that any party holding those can recompute it. The + prose also names the accounts the schedule uses and how each one's + opening amount is computed from the contract. This document does not + register profiles and defines none normatively; + Appendix A carries one for the experiment.

+

Everything the -01 revision said in its Section 5.3, and everything + it said in its Section 7 about what is posted, released, + returned or forfeited and when, is now the content of a profile. The + member that carried those figures inside the contract is gone; the + figures a profile needs are in parameters, and the -01 + figures in particular are the parameters of the profile in + Appendix A.

+
+
+
+
+
+
+

+6. The Delivery Record +

+

The Delivery is the record a contract is judged against. It is a + JSON object, media type application/vnd.pact.delivery+json, + with the members in Section 3.4, signed once by the + Seller.

+

A Facilitator MUST refuse a Delivery, with the problem type named, + when: its vtc_hash does not match the contract + (object-conflict); the contract is not in FUNDED + (wrong-state); its signature does not verify against a key + bound to parties.seller (signature-invalid, + unexpected-signer); its evidence member is absent or + does not conform to the verification profile named in the contract + (evidence-nonconformant); or input_hash is absent + where the tier re-executes (evidence-nonconformant). A refused + Delivery is recorded in no trace; the contract stays in FUNDED and a + conformant Delivery may follow before the deadline. The -01 revision + treated a nonconformant Delivery as a FAIL Verdict, which decided a + question about value inside a rule about shape; the consequence of a + Seller reaching the deadline with nothing conformant recorded is now + the deadline-passed event, and what that event costs anyone is + the profile's.

+

Conformance of evidence is a check on shape, not on + substance: the Facilitator confirms that the members the profile + requires are present and well formed, and nothing about whether the + work is any good. That is why the check stays on the right side of the line + drawn in Section 2.1. Where task.deadline passes + with no delivered entry, the Facilitator records + deadline-passed (Section 4.2). No window opens, + because there is nothing to challenge.

+
+
+
+
+{
+  "pact": "0.2",
+  "type": "Delivery",
+  "vtc_id":   "vtc_7f3a91",
+  "vtc_hash": "sha256:<vtc_hash>",
+  "work_hash":  "sha256:9c1f...",
+  "work_uri":   "https://cdn.dataforge.example/o/9c1f",
+  "input_hash": "sha256:41ab...",
+  "evidence": {
+    "profile":        "acceptance",
+    "instrument_hash":"sha256:<criteria_hash>",
+    "results_hash":   "sha256:7e02...",
+    "results_uri":    "https://cdn.dataforge.example/o/7e02"
+  },
+  "signature": { "protected": "...", "signature": "..." }
+}
+
+
+
Figure 4: +A Delivery for a T0-reexec contract, acceptance profile +
+
+

The -01 revision said that a Buyer countersignature over the + Delivery constituted a receipt. The Delivery's signing member is a + single object, so no second signature could be carried, and the + sentence is withdrawn. A Buyer that wants a record of receipt has one: + the Status the Facilitator returns for the Delivery carries the + delivered entry and the Facilitator's signature over it.

+
+
+
+
+

+7. Verdicts, Challenges and the Window +

+
+
+

+7.1. Flows +

+

The flow member selects one of three shapes for the state + machine of Section 4.1. A conformant Facilitator MUST + implement verdict-first; the others are OPTIONAL, and a + Facilitator MUST refuse a contract naming a flow it does not advertise + (flow-unsupported).

+
+
+verdict-first:
+
A Verdict is recorded before the + window opens. The window opens on a PASS Verdict or on + verdict-lapsed; a FAIL Verdict ends the contract without a + window. +
+
+
+delivery-first:
+
The window opens at + delivered. A Verdict MAY be recorded inside the window + without a Challenge; a FAIL ends the contract, a PASS changes + nothing. +
+
+
+no-window:
+
No window opens and no Verdict is + accepted; delivered is followed by the terminal + path. +
+
+
+

The -01 revision had four release modes, named for when value + moved. Two of them, on-window and optimistic, + produce the same trace and differed only in which event a profile + acts on, which is a profile parameter and not a protocol matter. The + mapping is in Appendix B.

+

The window opens at the instant of the window-opened entry + and closes at that instant plus challenge.window_seconds, + carried in the entry as closes_at. A Facilitator MUST NOT + accept a Challenge after closes_at, MUST NOT extend the + window for any reason, and MUST NOT record window-closed + while a Challenge is pending.

+
+
+
+
+

+7.2. Verdicts +

+

A Verdict is a signed statement that a Delivery was evaluated + against the committed instrument, and with what outcome. It is a JSON + object, media type application/vnd.pact.verdict+json, with + the members in Section 3.5, signed once.

+
+
+
+
+{
+  "pact": "0.2",
+  "type": "Verdict",
+  "vtc_id":        "vtc_7f3a91",
+  "delivery_hash": "sha256:<delivery_hash>",
+  "outcome":       "PASS",
+  "profile":       "acceptance",
+  "instrument_hash": "sha256:<criteria_hash>",
+  "results_hash":    "sha256:7e02...",
+  "evaluated_at":  "2026-11-10T09:14:22Z",
+  "signature": { "protected": "...", "signature": "..." }
+}
+
+
+
Figure 5: +A Verdict +
+
+

The Verifier is the party identified by the kid of the + Verdict's signature. Where the contract names + parties.verifier, a Facilitator MUST refuse a Verdict signed + by any other party; otherwise it MUST refuse a Verdict whose signer + does not satisfy Section 9.1 + (verifier-not-independent). It MUST refuse a Verdict for a + contract with no delivered entry + (no-recorded-delivery); one whose delivery_hash + does not match that entry, or whose profile or + instrument_hash does not match the contract + (verdict-nonconformant); one received in a state the table + in Section 4.2 does not list for it, or under the + no-window flow (wrong-state); and one carrying + challenge_hash that names no pending Challenge, or omitting + it while the contract is DISPUTED (verdict-nonconformant). + A Verdict that answers a Challenge supersedes the Verdict that stood + before it, and both stay in the trace.

+

A Verdict commits to the instrument it ran and to the results it + produced. Without instrument_hash a Verifier could run + something other than the committed instrument and the contract would + have no way to tell; that is the substitution attack of + Section 17.4, arriving from the verification + side.

+

Under verdict-first a Verifier that never answers would + leave a contract in DELIVERED forever, and the -01 revision had no + rule for it. verification.max_verdict_seconds bounds the + wait: when it passes with no Verdict, the Facilitator records + verdict-lapsed and opens the window, so that the contract can + still be challenged and can still end. What a lapsed Verdict costs + anyone is the profile's.

+
+
+
+
+

+7.3. Challenges +

+

A Challenge is a JSON object, media type + application/vnd.pact.challenge+json, with the members in + Section 3.6, by which a party submits a fraud + proof inside the window. A Facilitator MUST refuse a Challenge + received when the contract is not in WINDOW_OPEN or DISPUTED, or + after closes_at (challenge-window-closed); one + whose delivery_hash does not match the delivered + entry (object-conflict); one whose proof does not + conform to the verification profile (proof-nonconformant); + one whose signer it cannot resolve (signature-invalid); and + one signed by the contract's Seller (unexpected-signer), + since a performer's statement against its own Delivery is not a + fraud proof and the -01 revision left the case open. A Facilitator + MUST NOT refuse a Challenge on the ground that its signer is the + contract's Buyer.

+

A Challenge that is accepted is evaluated by a party satisfying + Section 9.1, whose finding is a Verdict carrying + challenge_hash; the Challenger's own assertion is not a + finding. The Challenger is the party identified by the kid of + the Challenge's signature.

+

A Facilitator MAY require that a Challenge be accompanied by a + deposit in the amount its capability document advertises as + challenge_deposit. How a deposit is posted is the settlement + binding's, what becomes of it is the terms profile's, and this + document says nothing further about it. Section 17.14 + discusses what a deposit does and does not prevent.

+
+
+
+
+{
+  "pact": "0.2",
+  "type": "Challenge",
+  "vtc_id":        "vtc_7f3a91",
+  "delivery_hash": "sha256:<delivery_hash>",
+  "proof": {
+    "profile":         "acceptance",
+    "instrument_hash": "sha256:<criteria_hash>",
+    "results_hash":    "sha256:a91e...",
+    "results_uri":     "https://watch.example/o/a91e",
+    "failing_checks":  ["schema_valid_rate", "row_count_min"]
+  },
+  "costs": { "amount": "1.20", "currency": "USDC" },
+  "signature": { "protected": "...", "signature": "..." }
+}
+
+
+
Figure 6: +A Challenge under the acceptance profile +
+
+
+
+
+
+

+7.4. Disputes and Lapses +

+

A contract with a pending Challenge is DISPUTED. It leaves that + state when a Verdict answers the Challenge, or when + challenge.max_dispute_seconds pass with none and the + Facilitator records dispute-lapsed. A lapsed Challenge + changes no Verdict: the Verdict that stood before it stands after it. + A Facilitator MAY accept further Challenges while DISPUTED, each of + which is pending on its own account, and MUST NOT record + window-closed until none is pending.

+
+
+
+
+  Buyer        Facilitator       Verifier      Challenger
+    |              |                 |               |
+    |              |<-- POST Verdict |               |
+    |              |   [ verdict PASS ]              |
+    |              |   [ window-opened ]             |
+    |              |<------------- POST Challenge ---|
+    |              |-- Status ---------------------->|
+    |              |   [ challenge ]                 |
+    |              |-- Challenge + Delivery -------->|
+    |              |<-- POST Verdict |               |
+    |              |   [ verdict FAIL, answers,      |
+    |              |     supersedes ]                |
+    |              |   [ children-final ]            |
+    |              |   [ terminal SETTLED,           |
+    |              |     challenge_upheld true ]     |
+    |<- Outcome ---|                                 |
+
+
+
Figure 7: +The dispute path: a Challenge answered by a FAIL Verdict +
+
+

The figure carries no rank, no waterfall and no amount. The -01 + revision drew five numbered transfers on this diagram; every one of + them is now a line in a profile's schedule, keyed to the + terminal entry, and reported in terms_result.

+
+
+
+
+
+
+

+8. Facilitator Capability Discovery +

+

Before a Buyer and Seller can co-sign a VTC they must agree on a + Facilitator and know what it implements. This document registers one + well-known URI for that purpose, per [RFC8615].

+

This is deliberately narrower than agent discovery, which is the + subject of separate work and is not restated here. What is discovered + is one service's capabilities, not an agent's identity, skills or + endpoints.

+

A Facilitator SHOULD publish a JSON document, media type + application/vnd.pact.facilitator+json, with the members in + Section 3.9, at the path + /.well-known/pact-facilitator of its origin. The document MUST + be served over HTTPS. It MUST be signed, and the signature MUST verify + against a key bound to the identifier in facilitator. An + unsigned capability document is not usable for contract formation, + because terms_profiles determines which terms a party can name + and expect to be evaluated.

+
+
+
+
+{
+  "pact": "0.2",
+  "type": "FacilitatorCapabilities",
+  "facilitator": "did:web:settle.example",
+  "settlement_bindings": [
+    { "id": "https://settle.example/bindings/ledger-1",
+      "networks": ["eip155:8453"],
+      "assets":   ["USDC"] }
+  ],
+  "flows":                 ["verdict-first", "delivery-first"],
+  "verification_profiles": ["acceptance", "bisection"],
+  "terms_profiles": [
+    { "id":
+        "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution",
+      "profile_hash": "sha256:<profile_hash>" }
+  ],
+  "max_contract_value":    { "amount": "50000.00",
+                             "currency": "USDC" },
+  "endpoints": {
+    "contract":  "https://settle.example/pact/v2/contracts",
+    "delivery":  "https://settle.example/pact/v2/deliveries",
+    "verdict":   "https://settle.example/pact/v2/verdicts",
+    "challenge": "https://settle.example/pact/v2/challenges",
+    "outcome":   "https://settle.example/pact/v2/outcomes"
+  },
+  "signature": { "protected": "...", "signature": "..." }
+}
+
+
+
Figure 8: +https://settle.example/.well-known/pact-facilitator +
+
+

A client MUST NOT infer any capability from the absence of a member. + A Facilitator that does not publish a capability document can still be + named in a VTC by prior arrangement; discovery is a convenience, not a + precondition. A Facilitator MUST NOT list a terms profile whose + vectors (Section 12.1) its own implementation does not + reproduce.

+
+
+
+
+

+9. Verification Profiles +

+

A contract names both a tier, which says what class of + evidence is produced, and a profile, which says what is + actually done to check it. Four tier labels are used in this document: + T0-reexec, deterministic re-execution; T1-tee, + hardware attestation per [RFC9334]; T2-zkml, a + proof of inference; and T3-jury, staked arbitration. Tiers are + a vocabulary. Three profiles are defined below by name; any other is + identified by a URI under its definer's control, and this document + creates no registry for them. The distinction matters because the tier + name does not determine how much checking a contract gets and the + profile largely does.

+

Consider one task, a bulk data transformation, under two profiles at + the same nominal tier. Re-executing the whole computation and comparing + outputs costs approximately what performing it cost. Running a committed + acceptance instrument against the delivered artifact costs a small + fraction of a percent. Those two differ by more than two orders of + magnitude in what checking costs relative to the price. A terms profile + may make that ratio matter; this document requires only that a + verification profile state an order-of-magnitude estimate of its cost + relative to the work, since a figure nobody can estimate is a figure + nobody can use.

+

Implementations SHOULD select the cheapest profile that detects the + failures they actually care about, rather than the strongest-sounding + one. A committed acceptance instrument that is adequate is worth more + than a re-execution profile that nobody can afford to run.

+
+
+acceptance:
+
Run the instrument committed by + criteria_hash against the Delivery. The fraud proof is a + failing evaluation. Deterministic by construction, since the + instrument is fixed before work begins. Cost: a small fraction of a + percent of the work for a data transformation. +
+
+
+bisection:
+
Interactive narrowing to a single + disputed step, which is then checked directly. Cost grows + logarithmically in the size of the computation rather than + linearly. +
+
+
+full-reexec:
+
Re-execute and compare byte for + byte. Sound only where the computation is deterministic and the + environment is pinned; see Section 17.10. Cost: + approximately the work. +
+
+
+
+
+

+9.1. Verifier Independence and Identifier Normalization +

+

Independence is a relation between the party that signs a Verdict + and the parties to the contract. It MUST be derived by the evaluator + and MUST NOT be satisfied by a field in which a record declares + itself independent. A Facilitator MUST refuse a Verdict whose signer + is, after normalization, the contract's Buyer, Seller or Facilitator, + and MUST refuse a contract whose parties.verifier is any of + those three (verifier-not-independent). The last case is the + rule the -01 revision stated as a prohibition on the Facilitator's + conduct; it is an identifier comparison and is stated as one.

+

Party identifiers MUST be normalized before comparison, and the + normalization MUST fold toward identifying the same party: strip + leading and trailing whitespace; lower-case the scheme and, for + did:web and https identifiers, the host; remove any + fragment (a "#" and everything after it) and any trailing "/" or + ".". The path of a did:web identifier is case sensitive and + MUST NOT be folded. Percent-encoding MUST NOT be decoded, since an + open-ended decoder is its own attack surface. An identifier that does + not parse after normalization is not evaluable and MUST NOT be + treated as outside the parties. An independence claim reaches + exactly as far as the record's own commitments.

+
+
+
+
+
+
+

+10. Contract Trees +

+

An agent that accepts work may subcontract part of it. The + subcontract is an ordinary PACT contract whose Buyer is the parent's + Seller. What this section adds is the binding between the two, in both + directions and across Facilitators, so that a parent's Outcome Record + can commit to its children's and a reader of the parent's record can + find and check them.

+
+
+
+
+                A (Buyer)
+                    |
+                vtc_7f3a91      at did:web:settle.example
+                    |
+                B (Seller)
+                    |
+        +-----------+-----------+
+        |                       |
+    vtc_c1a2                vtc_c2b7    at did:web:other.example
+        |                       |
+    C (Seller)              D (Seller)
+
+
+
Figure 9: +A contract tree. B is Seller above and Buyer below. +
+
+
+
+

+10.1. Binding a Child to Its Parent +

+

A subcontract carries parent, a top-level member with the + parent's vtc_id, vtc_hash and + facilitator. Because parent is inside the bytes both + parties sign, the child's Buyer signature is itself the authorisation + to attach that child to that parent. The -01 revision carried this + member inside the member it has since removed; it is structural and + is now where structure is.

+

The child's Facilitator need not resolve the parent, and across + Facilitators it often cannot. It MUST record parent as + signed, and MUST allow the identifier in parent.facilitator + to retrieve the child's Status and Outcome Record + (Section 17.12). The check that the child's Buyer is + the parent's Seller is made where the parent is: at registration.

+

The -01 revision required a Facilitator to reject a child whose + parent chain contained the child's own identifier and to enforce a + maximum depth. Neither rule survives, because neither is needed. A + child commits to its parent's digest, and the parent's digest exists + before the child is signed, so no contract can commit to a descendant + and a cycle cannot be formed; depth is bounded by whatever a + Facilitator is willing to register, and no Facilitator sees more + than one level.

+
+
+
+
+

+10.2. Registration and Children Final +

+

The parent's Facilitator learns of a child when the parent's Seller + registers it: a POST of the child's co-signed contract to the + parent's contract resource (Section 13). The + registering party is the child's Buyer, which is why it holds the + child's contract and why it is authorised: it is a party to both.

+

A Facilitator MUST refuse a registration, with the problem type + named, when: the body is not a valid contract + (Section 14.2); its parent.vtc_hash is not the + parent's digest or its parent.facilitator is not this + Facilitator (parent-unresolvable); its + parties.buyer is not the parent's parties.seller + after normalization (parent-unresolvable); its latest + finality instant is not earlier than the parent's + (Section 10.3, finality-ordering-violation); + or the parent is terminal (wrong-state). An accepted + registration is recorded as child-registered.

+
+
+   child.parties.buyer  ==  parent.parties.seller
+   child.parent.vtc_hash  ==  digest(parent)
+
+
+

Without the first check any party may name any contract as its + parent. The attack is cheap and asymmetric: name a competitor's + contract as parent, subcontract a trivial task to yourself, fail it, + and put a failed child under the competitor's record. The -00 + revision carried the parent as a bare string with no hash and no + check, so the attack cost one signature.

+

A child becomes final for its parent when the parent's Facilitator + holds the child's Outcome Record. It may obtain that record itself, + by retrieving it from the child's Facilitator, or receive it from the + parent's Seller by a POST to the same resource + (Section 13). Either way the Facilitator MUST verify + the record's Facilitator signature against a key bound to the + identifier the registration recorded, and MUST verify that its + vtc_hash is the registered child's digest, before recording + child-final. Where the child's latest finality instant passes + with no record held, the Facilitator records + child-unresolved. children-final follows when every + registered child has one entry or the other, and the parent's + terminal entry follows that.

+

A child that is never registered does not exist for the parent. + Nothing in this document compels a parent's Seller to register a + child, and Section 17.6 says what that means.

+
+
+
+
+

+10.3. Finality Is Bottom-Up +

+

A parent's Outcome Record MUST carry + children_merkle_root over the Outcome Records of its + registered children (Section 12.2), so a parent cannot be + recorded until its children have been, and the parent waits in + AWAITING_CHILDREN until they are. For that wait to be bounded, every + child must be able to reach a terminal state, or be declared + unresolved, before its parent needs it.

+

The latest finality instant L of a contract is computed from its + own members and nothing else:

+
+
+   verdict-first:  L = task.deadline
+                       + verification.max_verdict_seconds
+                       + challenge.window_seconds
+                       + challenge.max_dispute_seconds
+   delivery-first: L = task.deadline
+                       + challenge.window_seconds
+                       + challenge.max_dispute_seconds
+   no-window:      L = task.deadline
+
+
+

Every wait in Table 2 is bounded by one of + those members, and a Challenge can only be received before + closes_at, so no sequence of events carries a contract past + its L except waiting for its own children. A Facilitator MUST refuse + to register a child unless L(child) is earlier than L(parent), and + MUST record child-unresolved for a registered child no later + than the first opportunity after L(child) if it holds no Outcome + Record for it by then.

+

The -01 revision compared the child's latest finality with the + parent's earliest window close, and bounded neither: under its + default mode the first Verdict could take forever, so the inequality + guaranteed nothing. max_verdict_seconds is what makes L + finite, and the waiting state is what makes the rule honest about + the case where a child is late anyway.

+
+
+
+
+ parent |== work ==|= verdict =|= window =|= dispute =|
+                                                      ^ L(parent)
+ child  |== work ==|= vrd =|= win =|= dsp =|
+                                           ^ L(child)
+
+        a child is registered only where L(child) < L(parent)
+
+
+
Figure 10: +Bottom-up finality +
+
+

What a child's outcome means for its parent is not stated here. + No entry in a parent's schedule depends on any child's outcome unless + the named terms profile says so; what this document guarantees is + that the parent's Outcome Record commits to whichever child records + exist when it is issued and names, in its trace, every child that + does not.

+
+
+
+
+
+
+

+11. The Contract Status +

+

A Contract Status is a JSON object, media type + application/vnd.pact.status+json, with the members in + Section 3.7, signed once by the Facilitator. It is the + body of every successful response to a POST in + Section 13 and of a GET on a contract resource. It + carries the contract's state and the trace recorded so far.

+
+
+
+
+{
+  "pact": "0.2",
+  "type": "ContractStatus",
+  "vtc_id":   "vtc_7f3a91",
+  "vtc_hash": "sha256:<vtc_hash>",
+  "state":    "WINDOW_OPEN",
+  "trace": [
+    { "event": "accepted",  "at": "2026-11-01T10:00:00Z",
+      "object": "sha256:<vtc_hash>" },
+    { "event": "funded",    "at": "2026-11-01T10:00:00Z" },
+    { "event": "delivered", "at": "2026-11-10T08:30:12Z",
+      "object": "sha256:<delivery_hash>" },
+    { "event": "verdict",   "at": "2026-11-10T09:14:30Z",
+      "object": "sha256:<verdict_hash>", "outcome": "PASS" },
+    { "event": "window-opened", "at": "2026-11-10T09:14:30Z",
+      "closes_at": "2026-11-10T10:14:30Z" }
+  ],
+  "issued_at": "2026-11-10T09:14:30Z",
+  "signature": { "protected": "...", "signature": "..." }
+}
+
+
+
Figure 11: +A Contract Status after the Verdict of Figure 1 +
+
+

Two rules make a Status worth keeping. A Facilitator MUST issue a + Status for every request it accepts, carrying the entry that request + caused, so that the requester holds a signed receipt of what was + recorded and when. And the trace in every Status a Facilitator issues + for a contract MUST be a prefix of the trace in every later one; two + Statuses for one contract that violate that are evidence of + equivocation, and Section 17.1 says what a holder + can do with it. The Outcome Record's trace is the last such + sequence.

+

The -01 revision returned the posted object with a state + member added to it, which no schema admitted and no signature covered. + The Status replaces that: the posted object is not echoed, and + everything in the response is inside the Facilitator's signature.

+
+
+
+
+

+12. Outcome Records +

+

An Outcome Record records what a contract did. It is a JSON object, + media type application/vnd.pact.outcome+json, with the members + in Section 3.8. It is the input to any reputation + system built on PACT, though this document defines no such system and + takes no position on how the records should be weighed.

+

A Facilitator MUST issue exactly one Outcome Record for every + contract that reaches a terminal state, including SETTLED and + ABANDONED, MUST sign it, and MUST NOT require the signature of any + other party on it. The -00 revision's record needed the signature of + the party it recorded against, which made a reputation layer built on + it structurally incapable of recording a loss. The Facilitator + signature is what makes the record evidence: without it the record is + a claim by interested parties about themselves, and with it a + fabricated history requires a Facilitator's key rather than two + identities.

+
+
+
+
+{
+  "pact": "0.2",
+  "type": "OutcomeRecord",
+  "vtc_id":   "vtc_7f3a91",
+  "vtc_hash": "sha256:<vtc_hash>",
+  "parties": {
+    "buyer":       "did:web:acme.example",
+    "seller":      "did:web:dataforge.example",
+    "facilitator": "did:web:settle.example",
+    "verifier":    "did:web:audit.example"
+  },
+  "outcome":   { "state": "SETTLED", "challenge_upheld": true },
+  "work_hash": "sha256:9c1f...",
+  "trace": [
+    { "event": "accepted",  "at": "...",
+      "object": "sha256:<vtc_hash>" },
+    { "event": "funded",    "at": "..." },
+    { "event": "delivered", "at": "...",
+      "object": "sha256:<delivery_hash>" },
+    { "event": "verdict",   "at": "...",
+      "object": "sha256:<verdict_hash>", "outcome": "PASS" },
+    { "event": "window-opened", "at": "...", "closes_at": "..." },
+    { "event": "challenge", "at": "...",
+      "object": "sha256:<challenge_hash>" },
+    { "event": "verdict",   "at": "...",
+      "object": "sha256:<verdict2_hash>", "outcome": "FAIL",
+      "answers": "sha256:<challenge_hash>",
+      "supersedes": "sha256:<verdict_hash>" },
+    { "event": "children-final", "at": "..." },
+    { "event": "terminal",  "at": "...", "state": "SETTLED",
+      "challenge_upheld": true }
+  ],
+  "terms_result": {
+    "profile":
+      "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution",
+    "profile_hash": "sha256:<profile_hash>",
+    "currency":     "USDC",
+    "transfers": [
+      { "event": 8, "from": "...", "to": "...", "amount": "...",
+        "code": "..." }
+    ]
+  },
+  "signatures": [ { "protected": "...", "signature": "..." } ]
+}
+
+
+
Figure 12: +An Outcome Record for a contract that reached SETTLED on an upheld Challenge +
+
+

The record carries one signature, the Facilitator's. The Seller did + not consent to this record and its consent is not required. The + transfers entries are elided here because their content is the + profile's; Appendix A shows them filled in for + its own profile.

+
+
+

+12.1. The Terms Result +

+

terms_result reports what the named profile's schedule + produced over the whole trace. It carries the profile identifier and + hash copied from the contract, the currency, and transfers: + an array of entries, in the order the schedule produced them, each + with event (the zero-based index of the trace entry at which + the schedule emitted it), from and to (account + names as the profile defines them), amount, and + code (a string the profile defines, naming the schedule + line that produced the entry).

+

This document defines the form of the list and two arithmetic + facts about it, and nothing about what any entry means. Over the + accounts and opening amounts the profile declares for the contract + (Section 5.3): no entry takes from an account more than + that account holds at that point in the list; and after the last + entry every account the profile marks internal holds zero. A + Facilitator MUST NOT sign an Outcome Record whose list breaks either + fact, and MUST NOT sign one whose list differs from what the + profile's schedule produces for the record's own trace. Any party + holding the contract, the trace and the profile's bundle can + recompute the list; that is the property the experiment in + Section 1.4 depends on.

+

vectors.json in a profile's bundle is an array of + objects, each with name, contract (a VTC, or the + members of one the schedule reads), trace (a complete + trace), and transfers (the list the schedule produces for + it). A Facilitator MUST reproduce every vector of a profile before + listing that profile in its capability document + (Section 8), which is the only conformance + requirement this document places on a profile implementation.

+
+
+
+
+

+12.2. The Children Merkle Root +

+

Let D be the list of 32-byte SHA-256 digests of the Outcome Record + of each registered child for which the Facilitator holds one, each + computed over the record's canonical form including its + signatures member, sorted ascending as byte strings. + children_merkle_root is MTH(D) exactly as defined in + [RFC9162] Section 2.1.1, with SHA-256 as the hash: a + leaf is SHA-256(0x00 || d), an interior node is SHA-256(0x01 || left + || right), and for n greater than one the list is split at k, the + largest power of two smaller than n. The shape is therefore fixed by + n alone, and two implementations that agree on D agree on the + root.

+

The domain separation is not optional. Without distinct prefixes an + attacker can present an interior node as though it were a leaf, and so + claim an inclusion proof for a subtree that never existed.

+

The member is present when at least one child is registered and + absent otherwise; it MUST NOT be present with an empty or zero value, + which would be indistinguishable from a tree whose children were + withheld. Where every registered child is unresolved D is empty and + the root is MTH of the empty list, SHA-256 of the empty string; the + child-unresolved entries in the trace say which records the + root does not cover. The -01 revision computed leaves over records + with their signatures removed, which let a record be re-signed + without changing the root.

+
+
+
+
+
+
+

+13. Protocol Endpoints +

+

This section specifies the operations a Facilitator exposes. Base + URIs are not fixed by this document; they are discovered from the + endpoints member of the capability document + (Section 8), so a Facilitator may mount them anywhere + on its origin.

+
+
Propose a contract:
+
POST {contract}; body, a + contract; 201 with a Status. +
+
+
Retrieve a contract's status:
+
GET + {contract}/{id}; 200 with a Status. +
+
+
Register a child:
+
POST + {contract}/{id}/children; body, the child's contract; 201 + with a Status. +
+
+
Supply a child's outcome:
+
POST + {contract}/{id}/children/{child_id}; body, the child's + Outcome Record; 200 with a Status. +
+
+
Submit a Delivery:
+
POST {delivery}; body, a + Delivery; 202 with a Status. +
+
+
Record a Verdict:
+
POST {verdict}; body, a + Verdict; 201 with a Status. +
+
+
Open a Challenge:
+
POST {challenge}; body, a + Challenge; 202 with a Status. +
+
+
Retrieve an Outcome Record:
+
GET + {outcome}/{id}; 200 with the Outcome Record. +
+
+
+

All requests and responses use the media types defined in + Section 19. All requests MUST be made over HTTPS, following + the recommendations of [RFC9325]. Status codes are as + defined in [RFC9110]. A Delivery and a Challenge are + answered 202 (Accepted) rather than 201 because + acceptance of the bytes is not acceptance of the work; what follows + depends on a Verdict the Facilitator does not itself produce.

+

A Facilitator authenticates the sender of a POST by the signature on + the body, and by nothing else in this document: it MUST reject a + Delivery not signed by the contract's Seller, a Verdict not signed by a + party admissible under Section 7.2, a Challenge whose + signer it cannot resolve, and a child registration or child outcome + whose body does not verify as Section 10.2 + requires. A Facilitator MAY require an HTTP-layer authentication in + addition. Retrieval is discussed in Section 17.12.

+
+
+

+13.1. Proposing a Contract +

+

The request body is a VTC carrying the signatures of both parties + required to sign it. A Facilitator MUST perform the checks in + Section 14, Section 5.3 and + Section 9.1 before creating the resource, MUST + refuse a contract whose parties.facilitator is not itself or + whose price.settlement, network or asset it does not + advertise (facilitator-mismatch, + settlement-unsupported), and MUST refuse otherwise with the + problem type that names the rule.

+
+
+POST /pact/v2/contracts HTTP/1.1
+Host: settle.example
+Content-Type: application/vnd.pact.contract+json
+
+{ "pact": "0.2", "type": "VerifiableTaskContract",
+  "id": "vtc_7f3a91", ... }
+
+
+
+
+HTTP/1.1 201 Created
+Location: /pact/v2/contracts/vtc_7f3a91
+Content-Type: application/vnd.pact.status+json
+
+{ "pact": "0.2", "type": "ContractStatus",
+  "vtc_id": "vtc_7f3a91", "state": "ACCEPTED",
+  "trace": [ { "event": "accepted", ... } ], ... }
+
+
+
+
+
+
+

+13.2. Idempotency +

+

Every object this protocol carries is committed by the digest of + its own canonical form, so no separate idempotency key is needed and + none is defined; the general mechanism of + [I-D.ietf-httpapi-idempotency-key-header] solves a + problem this protocol does not have. A Facilitator MUST treat a POST + whose body has a digest it has already accepted as a request for the + existing resource, and MUST respond 200 (OK) with the + current Status rather than creating a second resource or reporting a + conflict.

+

Where a POST carries the same object id as an existing + resource but a different digest, the Facilitator MUST respond + 409 (Conflict) (object-conflict). Retrying a + submission is therefore always safe, and altering one never is.

+
+
+
+
+

+13.3. Error Responses +

+

A Facilitator MUST report failures using + [RFC9457] problem details, media type + application/problem+json, with a type from + Section 19.3 for a rule in this document, or from + the profile's own namespace for a rule in a terms profile. A problem + arising from a rule in this document MUST carry section, the + number of the section stating the rule. A problem arising from a + rule in a terms profile MUST carry profile and + profile_section instead, since section cannot name + a rule outside this document. Error responses name the rule that was + violated, because a conformance failure a caller cannot locate is a + failure of the specification.

+
+
+HTTP/1.1 422 Unprocessable Content
+Content-Type: application/problem+json
+
+{
+  "type":   "tag:laxsharma79@gmail.com,2026:pact:problem:
+             signatures-unordered",
+  "title":  "Signature set not sorted",
+  "status": 422,
+  "detail": "the second entry's kid sorts before the first's
+             after normalization.",
+  "section": "14.1"
+}
+
+
+
+
+
+
+

+13.4. Exchange +

+
+
+
+
+ Buyer/Seller            Facilitator             Verifier
+      |                       |                      |
+      |-- POST {contract} --->|                      |
+      |<-- 201 Status --------|                      |
+      |                       |                      |
+      |-- POST {delivery} --->|                      |
+      |<-- 202 Status --------|                      |
+      |                       |                      |
+      |                       |-- GET work_uri ----->|
+      |                       |<-- POST {verdict} ---|
+      |                       |-- 201 Status ------->|
+      |                       |                      |
+      |-- GET {contract}/id ->|                      |
+      |<-- 200 Status --------|                      |
+      |                       |                      |
+      |-- GET {outcome}/id -->|                      |
+      |<-- 200 Outcome -------|                      |
+
+
+
Figure 13: +HTTP exchange for the flow in Figure 1 +
+
+
+
+
+
+
+
+

+14. Conformance +

+

Every rule a PACT conformance checker enforces is stated in this + document as normative text. This section collects the rules that a + schema language cannot express, so that an implementation built from + this document alone passes a conformance suite built from it. A rule + that lives only in a test suite is not a requirement, and an implementer + who cannot find it in the specification will not implement it.

+
+
+

+14.1. Signatures +

+

Every signature carried by a VTC, Delivery, Verdict, Challenge, + Status, Outcome Record or capability document is a JWS + [RFC7515] in the General JSON Serialization of + Section 7.2.1 of that document, with the payload detached as its + Appendix F describes. The payload is BASE64URL of the JCS-canonical + bytes of the object with the signing member removed, so the JWS + Signing Input is ASCII(BASE64URL(UTF8(protected)) || "." || + BASE64URL(JCS(object))) exactly as Section 5.1 of + [RFC7515] defines it. The payload is never + transmitted; a verifier reconstructs it from the object it holds, and + verifies over the protected header exactly as transmitted, never + over a header it re-serialized. The following constraints apply.

+
    +
  • The protected header MUST carry alg, kid and + typ. +
  • +
  • + alg MUST be ES256 or ES384 + [RFC7518], or EdDSA [RFC8037] + with an Ed25519 key; a verifier MAY also accept Ed448. A verifier + MUST reject any other value, and MUST reject none. Absent + an allowlist an attacker selects the algorithm, which permits both + unsigned acceptance and confusion of a public key for a symmetric + secret. +
  • +
  • + kid MUST appear inside the protected header and MUST + NOT be carried as a sibling of it. A key identifier outside the + signed bytes is rewritable in transit, which allows an attacker who + can publish a key document to re-attribute a genuine signature to + itself. +
  • +
  • + typ MUST be the full media type of the object signed, + including the application/ prefix, so that a signature + over one object type cannot be replayed as a signature over + another. Section 4.1.9 of [RFC7515] recommends + omitting the prefix; this document requires the full form so that + typ equals the registered media type character for + character. Explicit typing follows Section 3.11 of + [RFC8725]. +
  • +
  • A signatures array MUST be sorted by the normalized + kid of its entries (Section 9.1), ties + broken by the unnormalized kid, both compared as + sequences of Unicode code points; a verifier MUST reject an + unsorted array (signatures-unordered). Two clients that + each attach their own entry and exchange the object would + otherwise produce two arrays, and since the digest covers the + array, two digests for one agreement. +
  • +
  • An ECDSA signature MUST have its s value in the low + half of the curve order, that is s at most n/2 for the + order n of the curve [SP800-186], and a verifier + MUST reject one that does not. [RFC7518] fixes the + encoding and not which of the two valid s values is + accepted; accepting both lets anyone holding a valid signature + produce a second one over the same bytes without the key, and a + second signature is a second digest. EdDSA verification per + [RFC8032] already rejects a non-canonical + S, so the rule is stated for ECDSA only. +
  • +
+
+
+

+14.1.1. Key Resolution +

+

A kid is a URI naming a public key. A verifier MUST + resolve it as follows, and MUST reject a signature whose + kid it cannot resolve.

+
    +
  • A did: identifier is a DID URL + [DID-CORE]. The verifier resolves the DID document + by the method the identifier names and selects the verification + method its fragment identifies. Examples in this document use + did:web [DID-WEB]; no method is + required or excluded. +
  • +
  • An https: identifier dereferences, over TLS, to a + JWK Set [RFC7517]; the verifier selects the key + whose kid member equals the fragment. +
  • +
+

The part of a kid before its fragment MUST equal, after + the normalization in Section 9.1, the party + identifier the signature is attributed to. Verifying a signature + establishes that the holder of that key signed; that the key + belongs to the party is a property of the identity method, and this + document does not add to it. An identity system for agents defined + elsewhere, such as [I-D.ietf-wimse-aims], is used by + naming its identifiers here and resolving them by its rules.

+
+
+
+
+
+
+

+14.2. Rules Not Expressible in a Schema +

+
    +
  • + parties.buyer and parties.seller MUST be + distinct after the normalization in + Section 9.1 (parties-not-distinct). +
  • +
  • A contract MUST carry exactly one verifying signature whose + kid covers parties.buyer, exactly one whose + kid covers parties.seller, and no other + (signature-missing, unexpected-signer). A count + of signatures is not sufficient: two signatures covering one + identifier MUST be rejected. +
  • +
  • + challenge.window_seconds MUST be greater than zero, + and task.deadline MUST be later than the instant of + acceptance (deadline-invalid). +
  • +
  • Every URI member inside hash-committed content MUST have a + sibling hash member, and a validator MUST reject content carrying + harness_uri, rubric_uri, schema_uri or + sample_uri without its hash. +
  • +
  • An acceptance object MUST carry the members required + for the contract's tier. An empty acceptance object MUST be + rejected. +
  • +
  • + terms.profile and terms.profile_hash MUST + match an entry the Facilitator advertises, and + terms.parameters MUST validate against that profile's + schema (terms-unsupported, + terms-parameters-invalid). +
  • +
  • Every amount MUST have the form in + Section 2 (amount-invalid), and every + object MUST validate against the schema published for its media + type (schema-invalid). +
  • +
+
+
+
+
+

+14.3. Test Vectors +

+

Each rule above has an accepting and a rejecting form. A + conformance suite built from this section alone, with no reference to + any implementation, should reach the same verdicts. Rejecting vectors + name the rule they violate.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Table 3: +Conformance vectors +
IDMutation from a valid objectExpect
V-01unmodified valid VTCaccept
V-02 + alg set to none +reject
V-03 + alg set to HS256 +reject
V-04 + kid moved outside the protected + headerreject
V-05 + typ of a Delivery on a VTC + signaturereject
V-06buyer and seller set to the same + identifierreject
V-07buyer and seller differing only by trailing + "/"reject
V-08two signatures, both from the buyerreject
V-09 + window_seconds of 0reject
V-10 + acceptance as an empty objectreject
V-11 + harness_uri with + harness_hash removedreject
V-12 + terms.profile_hash not advertised + by the Facilitatorreject
V-13 + terms.parameters failing the + profile's schemareject
V-14Delivery with evidence absentreject, no entry
V-15child whose buyer is not the parent's + sellerreject
V-16child with L(child) not earlier than + L(parent)reject
V-17Verdict signed by the sellerreject
V-18object keys ordered by code point, with a + supplementary-plane keydigest mismatch
V-19buyer and seller differing only in the + case of a did:web pathaccept
V-20object carrying a member this document + does not define for itreject
V-21 + signatures not sorted by + normalized kid +reject
V-22ECDSA signature with s above + n/2reject
V-23Verdict with delivery_hash computed + over the Delivery without its signaturereject
V-24Outcome Record whose transfers + overdraw an account of the profilereject
+
+

V-07 and V-18 are the two most often got wrong. V-07 fails wherever + party comparison is a string equality on unnormalized identifiers. + V-18 fails wherever canonicalization sorts keys by Unicode code point, + which agrees with the required UTF-16 order for every ASCII key and so + passes every vector an implementer would think to write. V-19 is the + opposite mistake, folding more than Section 9.1 + allows, and the -01 reference validator made it.

+
+
+
+
+
+
+

+15. Worked Example +

+

The tables and digests below are the reference repository's, at the + tag named in Section 16. The object figures in earlier + sections use short illustrative identifiers for page width; the + repository examples carry the full ones, and the digests here are + computed over those. The figures that the -01 revision printed here + about a bond and a required detection rate are now the profile's, and + Appendix A carries them.

+

A buyer commissions a data transformation at a price of 180.00 USDC + under the verdict-first flow, the acceptance + verification profile, and the terms profile of + Appendix A with the parameters shown there. The + digests carried by the reference TaskSpec, contract and profile + are:

+
+
+ spec_hash       sha256:<spec_hash>
+ criteria_hash   sha256:<criteria_hash>
+ profile_hash    sha256:<profile_hash>
+ vtc_hash        sha256:<vtc_hash>
+ delivery_hash   sha256:<delivery_hash>
+
+
+

criteria_hash is the manifest digest of + Section 5.1 over the acceptance instrument bundle, and the + same value appears as acceptance.harness_hash inside the + TaskSpec, so the instrument is committed both by the contract and from + within the specification it belongs to. profile_hash is the + same construction over the profile bundle. vtc_hash is the + digest of the signed contract, and delivery_hash of the signed + Delivery, both per Section 2.

+

Every value above changed from the -01 revision, for four reasons + that are each recorded so that a reader comparing the two documents can + account for the difference: spec_hash because the TaskSpec + now carries the sibling hashes Section 5.1 always required; + vtc_hash because the contract's members changed + (Appendix B) and because spec_hash did; + delivery_hash because it now covers the Delivery's signature; + and profile_hash because it did not exist.

+

The trace the reference implementation records for this contract + on the path of Figure 1, and on the dispute path of + Figure 7, together with the transfer lists the + profile produces for each, are the vectors in the profile's bundle, + and Appendix A prints them.

+
+
+
+
+

+16. Implementation Status +

+

This section records the status of known implementations of this + document per [RFC7942], and is to be removed before + publication as an RFC.

+

One implementation is known to the author, and the author wrote it: + https://github.com/pact-spec/spec, under the Revised BSD licence. At + tag v0.2.0 it comprises the object schemas, the examples whose digests + Section 15 prints, a conformance validator that runs + <NN> checks including every vector of + Section 14.3, a Facilitator serving the endpoints of + Section 13 with the profile of + Appendix A, and clients for the other roles. Its + previous tag, v0.1.0, implemented the -01 revision and is the source + of the measurements the author has published about it. No second + implementation exists, so nothing in Section 1.4 has + been tested, and this document claims no interoperability.

+
+
+
+
+

+17. Security Considerations +

+

Most of what follows was found by adversarial review of earlier + revisions rather than anticipated when they were written. Each + subsection states the attack, why it worked, and the requirement in + this document that closes it. Where a threat is only mitigated rather + than closed, that is said. The table first: for each party, what the + protocol enforces against it, what it records about it, and who can + check the record without trusting the Facilitator.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Table 4: +What the protocol enforces, records and lets others check, by party +
PartyEnforced against itRecorded about itCheckable by
Buyercannot alter the task, instrument or terms after signing; + cannot attach a child to a contract it is not party toits signature on the contract; any Challenge it + signsanyone holding the contract
Sellercannot deliver against a substituted instrument or input; + cannot judge its own Delivery; cannot re-sign a record without + changing every digest over itits signature on the contract and the Delivery; the + Verdicts and Challenges on its Deliveryanyone holding the contract and the Delivery
Verifiercannot be a party to the contract; must commit to the + instrument it ran and its resultsits Verdicts, superseded ones includedanyone holding the Delivery and the instrument
Facilitatornothingwhat it chose to sign, in the order it chose, on a clock + that is its ownany holder of two of its Statuses, for equivocation; + nobody, for omission or for time, without a witness outside + this document
+
+
+
+

+17.1. Trust in the Facilitator +

+

The Facilitator row is the honest one. This protocol enforces + nothing against a Facilitator; it makes some kinds of misbehaviour + attributable and says plainly which ones it does not.

+

Equivocation, issuing two inconsistent histories for one contract, + is attributable: every Status is signed, every Status's trace is a + prefix of every later one, and two Statuses that break that rule are + proof, checkable by anyone holding both, that the Facilitator signed + contradictory records. A Facilitator that wants to make its records + publicly append-only can register its Outcome Records with a SCITT + transparency service [RFC9943] and hand the receipt + [RFC9942] to the parties; this document does not + require it and defines no log of its own.

+

Omission is not attributable. A Facilitator that declines to + record a Delivery, or records it late, produces no signed evidence of + having declined, and a Status it does not issue proves nothing. A + client SHOULD retain every Status it receives, and a party that + submitted a record and holds no Status for it has a claim it can + make only outside this protocol. Making omission attributable needs + a witness the Facilitator does not control, such as a monitor with a + gossip path of the kind [RFC9162] assumes, and this + document specifies none.

+

Time is the Facilitator's. Every instant in a trace is read from + its clock, and nothing in this document lets a party prove that a + recorded instant is wrong. This document therefore states the + assumption rather than hiding it: the Facilitator is a trusted + timekeeper, and a deployment that cannot accept that should look to + an external timestamping service, which this document does not + specify and does not preclude.

+

Whatever a Facilitator does with anything of value under a terms + profile is the profile's subject and is not addressed here.

+
+
+
+
+

+17.2. Verifier Capture +

+

A verification tier states how strongly work is checked. It does + not state who checked it, and those fail separately. A re-execution + transcript produced by the Seller and the same transcript produced by + an independent Challenger are the same method and different evidence. + Where a proof is generated and verified entirely inside one party, + the tier is satisfied and the contract is unprotected. + Section 9.1 requires that independence be derived + by the evaluator from the parties named in the contract, and forbids + satisfying it with a self-asserted field.

+
+
+
+
+

+17.3. Algorithm, Key and Encoding Confusion +

+

Absent an algorithm allowlist an attacker chooses the algorithm. + The two consequences are alg of none, which makes + every signature check vacuous, and presenting an ECDSA public key as + an HMAC secret, which lets anyone holding the public key forge. + Section 14.1 fixes the permitted set.

+

A kid carried as a sibling of the protected header rather + than inside it is outside the signed bytes and is rewritable in + transit. An attacker who can publish a key document can then + re-attribute a victim's genuine signature to an identifier it + controls, without breaking any cryptography. + Section 14.1 requires kid inside the protected + header.

+

Because every digest in this document covers a signature set, a + second valid encoding of one signature is a second digest for one + record. ECDSA has two valid s values per signature and JWS + does not choose between them; the low-S rule in + Section 14.1 does. The order of a signature set is a + second source of the same problem, and the sorting rule closes + it.

+
+
+
+
+

+17.4. Substitution of Committed Content +

+

The -00 revision committed harness_uri as a string. The + bytes at that URI were covered by nothing. A Buyer could therefore + sign a contract, replace the acceptance instrument afterwards, run the + replacement, and submit its failure as a textbook-valid fraud proof. + Cost of the attack: one file overwrite. The mirror attack works + against a Seller that hosts the input sample. + Section 5.1 requires a sibling hash over the dereferenced + bytes for every URI inside committed content, and + Section 7.2 requires a Verdict to commit to the + instrument it actually ran, which closes the same attack from the + verification side.

+
+
+
+
+

+17.5. Fetching Committed Content +

+

A work_uri, results_uri or any other URI in a + record is supplied by a counterparty and points wherever that + counterparty chose. An implementation that fetches it MUST fetch over + HTTPS only, MUST NOT follow a redirect to a scheme other than HTTPS, + MUST refuse to connect to a private, loopback or link-local address + (the ranges of [RFC1918], [RFC4193] and + their loopback and link-local counterparts), and MUST verify the + sibling hash over the full received bytes before any byte is used + for anything. A fetcher that acts on partial or unverified content + has handed its counterparty a way to make it execute, store or judge + something that was never committed to.

+
+
+
+
+

+17.6. Children: Attachment and Omission +

+

Naming a parent contract cost one signature in the -00 revision + and was checked against nothing. Section 10.2 + requires the child's Buyer to be the parent's Seller, checked by the + Facilitator that holds the parent against the parent's own bytes.

+

The converse gap is stated rather than closed: a parent's Seller + that never registers a failing child keeps it out of the parent's + record, since this document compels no registration. A profile that + wants children visible must make registration worth the Seller's + while, or a Buyer that wants them visible must ask for the child's + Status directly, which this document does not require the child's + Facilitator to give it.

+
+
+
+
+

+17.7. Buying Silence from a Challenger +

+

Wherever what a discoverer gains by reporting is less than what a + performer loses by being reported, there is a private payment that + leaves both better off than reporting, and silence dominates whatever + reward a profile designed. This document cannot close that, because + every figure involved is the profile's. What it does is record every + Challenge, in order, whoever signed it, so that a profile can act on + each independently, and it forbids a Facilitator from refusing a + Challenge because the Buyer signed it + (Section 7.3), so that the party with the most to + recover is always admissible.

+
+
+
+
+

+17.8. Non-Delivery +

+

Under the -00 revision a contract in which nothing was ever + delivered had no path to an end: the deadline carried no stated + consequence and no window opened because there was nothing to + challenge. Section 4.2 makes the deadline an event and + ABANDONED a terminal state that every contract can reach. What + reaching it costs anyone is the profile's, and a profile that makes + delivering nothing cheaper than delivering something wrong has + recreated the -00 incentive.

+
+
+
+
+

+17.9. Cross-Venue Replay +

+

A VTC that does not name its Facilitator, network and asset is a + signed instrument replayable against any of them; Section 5 + requires all three inside the signed content. A digest computed over + a contract excluding its signatures proves what was written and not + who agreed to it, so entries can be appended or stripped without + invalidating the commitment; Section 2 defines + every digest over the signature set. A Delivery, Verdict or Challenge + replayed against a different contract fails because each carries + vtc_id and a hash that binds it to one contract and one + Delivery, and the typ rule of Section 14.1 + stops a signature over one object type standing for another.

+
+
+
+
+

+17.10. Nondeterminism as Shield and as Weapon +

+

A re-execution profile that does not state what determinism it + assumes cuts both ways. An honest Seller doing model-assisted work is + convicted by a re-execution that differs for ordinary reasons. A + cheating Seller escapes any fraud proof by asserting nondeterminism, + unfalsifiably. A verification profile MUST state whether it is + deterministic and what tolerance applies, and a contract naming one + that does not is not safely enforceable by anyone.

+
+
+
+
+

+17.11. Fabricated History +

+

The argument for reputation derived from Outcome Records is that + faking a history requires running real contracts. That argument fails + if records do not name the parties or carry no Facilitator signature, + since two cooperating identities can then manufacture history at the + cost of two signatures. Section 12 requires both. It + fails in the other direction if a negative outcome requires the + signature of the party it records against; reputation that is + structurally incapable of recording a loss is not evidence of + anything.

+
+
+
+
+

+17.12. Retrieval +

+

A GET on a contract's Status or Outcome Record MUST be refused + unless the requester is a party named in the contract's + parties, the identifier in the contract's + parent.facilitator, or a party the Facilitator has chosen to + admit; a Facilitator MAY open retrieval more widely and SHOULD say so + in its capability document. How a requester proves which identifier + it is, on a GET with no body to sign, is an HTTP-layer matter this + document leaves to the deployment. The -01 revision left retrieval + unauthenticated by default, which published every contract graph a + Facilitator held to anyone who could guess an identifier.

+
+
+
+
+

+17.13. Key Compromise and Rotation +

+

A signature here is a long-lived commitment, and a compromised key + signs contracts the party never agreed to. Rotation and revocation + belong to the identity method behind the kid + (Section 14.1.1), and this document does not restate them. + Two things it does require: a Facilitator MUST record, with each + record it accepts, the key material or its digest as resolved at the + time of acceptance, so that a later rotation does not make an earlier + signature unverifiable; and a Facilitator MUST NOT accept a record + whose kid resolves to a key the identity method marks as + revoked at the time of acceptance.

+
+
+
+
+

+17.14. Denial of Service by Challenge +

+

Every accepted Challenge costs an independent evaluation. Without a + cost to the Challenger, a party can exhaust a Verifier's or a + Facilitator's capacity by challenging every Delivery. The deposit of + Section 7.3 is one defence, and it is a MAY because a + deposit also deters the honest challenger an open model relies on. A + Facilitator that requires no deposit SHOULD rate-limit Challenges per + Challenger and per contract, and SHOULD publish that it does so.

+
+
+
+
+
+
+

+18. Privacy Considerations +

+

PACT moves contracts and evidence about work, and both leak.

+
+
+

+18.1. Input Disclosure Before Contract Formation +

+

Publishing a representative input sample so that a counterparty can + price the work discloses production data to parties with whom no + contract exists and who may be in unknown jurisdictions. Samples + SHOULD be synthetic or de-identified. Where a real sample is + necessary, it SHOULD be disclosed only after a confidentiality + undertaking, and the constraints member SHOULD carry the + retention and deletion terms. This document cannot enforce any of + that and does not pretend to.

+
+
+
+
+

+18.2. The Contract Graph +

+

A Facilitator that publishes its Outcome Records makes the + contract graph public. From it a reader can reconstruct an + organisation's suppliers, spend and cadence, which is commercially + sensitive even when no individual is identifiable. Transparency and + counterparty privacy are in genuine tension here, and this document + resolves it in favour of neither: retrieval is restricted by default + (Section 17.12), a Facilitator MAY publish + aggregates, and SHOULD NOT publish per-contract records identifying + both parties without their agreement. Outcome Records leak the same + graph by construction, since each names both parties and the + counterparty retains a signed copy indefinitely. Selective + disclosure over Outcome Records, so that a holder can prove a + completed contract without revealing the counterparty, is possible + with mechanisms specified elsewhere and is not specified here.

+
+
+
+
+

+18.3. Challenger Access +

+

An open challenge model requires that some party outside the + contract can obtain the deliverable and the input in order to build a + fraud proof. That is in direct conflict with confidentiality of both. + The conflict is real and this document does not dissolve it. What it + does is make the choice visible: a contract whose content cannot be + disclosed to a Challenger will receive no Challenge from outside its + parties, and a terms profile that counts on one has counted on + nothing.

+
+
+
+
+

+18.4. Retention +

+

Retention duties stated for dispute purposes can conflict with + erasure rights asserted by a data subject. Contracts SHOULD state a + retention period, and implementers should be aware that a hash + commitment survives deletion of the content it commits to, which is + usually the property they want and occasionally the one they must + explain.

+
+
+
+
+
+
+

+19. IANA Considerations +

+

This document asks IANA to register seven media types in the vendor + tree and one well-known URI. It creates no registry. It defines + problem types but does not ask for a registry of them + (Section 19.3). The -01 revision asked for two + registries, one of verification profiles and one of settlement + bindings, and listed under the second an identifier in another + project's namespace that nobody had defined; both requests are + withdrawn. A profile of either kind is identified by a URI under its + definer's control and needs no registration.

+
+
+

+19.1. Media Types +

+

IANA is requested to register the following in the "Media Types" + registry, per [RFC6838], in the vendor tree. The + template below is given once in full; the seven registrations differ + only in the subtype name and the object they carry.

+
+
Type name:
+
application +
+
+
Subtype name:
+
see Table 5 +
+
+
Required parameters:
+
N/A +
+
+
Optional parameters:
+
N/A +
+
+
Encoding considerations:
+
binary; the content is JSON + text as defined in [RFC8259], encoded in UTF-8 +
+
+
Security considerations:
+
See + Section 17 of this document. In particular these + media types carry signed objects whose signatures MUST be verified + under the constraints in Section 14.1; accepting one + without algorithm restriction permits signature forgery. +
+
+
Interoperability considerations:
+
Objects MUST be + canonicalized per [RFC8785] before hashing or + signing. Implementations that canonicalize by sorting object keys + on Unicode code point rather than UTF-16 code unit will produce + divergent digests for keys outside the Basic Multilingual + Plane. +
+
+
Published specification:
+
This document +
+
+
Applications that use this media type:
+
Services and + autonomous agents forming and recording task contracts under this + specification +
+
+
Fragment identifier considerations:
+
As specified for + application/json +
+
+
Additional information:
+
Deprecated alias names: none. + Magic numbers: none. File extensions: .json. Macintosh file type + code: TEXT +
+
+
Person & email address to contact:
+
Laxmikant Sharma + <laxsharma79@gmail.com> +
+
+
Intended usage:
+
COMMON +
+
+
Restrictions on usage:
+
None +
+
+
Author:
+
Laxmikant Sharma +
+
+
Change controller:
+
Laxmikant Sharma +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Table 5: +Media types registered by this document +
Subtype nameObjectDefined in
vnd.pact.contract+jsonVerifiable Task Contract + Section 5 +
vnd.pact.delivery+jsonDelivery + Section 6 +
vnd.pact.verdict+jsonVerdict + Section 7.2 +
vnd.pact.challenge+jsonChallenge + Section 7.3 +
vnd.pact.status+jsonContract Status + Section 11 +
vnd.pact.outcome+jsonOutcome Record + Section 12 +
vnd.pact.facilitator+jsonCapability document + Section 8 +
+
+

The -01 revision asked for these in the standards tree under the + names pact-contract+json and so on. Registration in that + tree from outside the IETF stream needs approval this document does + not have ([RFC6838], Section 3.1), and the vendor + tree is where an individual's specification belongs.

+
+
+
+
+

+19.2. Well-Known URI +

+

IANA is requested to register the following in the "Well-Known + URIs" registry, per [RFC8615].

+
+
URI suffix:
+
pact-facilitator +
+
+
Change controller:
+
Laxmikant Sharma +
+
+
Specification document(s):
+
This document, + Section 8 +
+
+
Status:
+
provisional +
+
+
Related information:
+
The resource is served with media + type application/vnd.pact.facilitator+json and MUST be + signed. +
+
+
+
+
+
+
+

+19.3. Problem Types +

+

This document creates no registry for its problem types. + [RFC9457] Section 4.2 establishes the "HTTP Problem + Types" registry for types intended for reuse across applications; + the types below are specific to this protocol and are identified by + URIs in a namespace this document defines, which that specification + permits without registration. Each is the identifier in the table + appended to the prefix + tag:laxsharma79@gmail.com,2026:pact:problem:, a tag URI + [RFC4151] under the author's control. A tag URI is + an identifier and is not dereferenceable, which is why it was + chosen over the -01 revision's prefix on a code-hosting site: an + identifier should not change when hosting does. Documentation for + every type is maintained in the repository named in + Section 16. Each entry carries the identifier, the + HTTP status it accompanies, and the section stating the rule it + reports. A terms profile that refuses a request defines its own + types under its own prefix and reports them as + Section 13.3 says.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Table 6: +Problem types defined by this document +
IdentifierStatusDefined in
algorithm-not-permitted400 + Section 14.1 +
amount-invalid422 + Section 14.2 +
challenge-window-closed409 + Section 7.3 +
child-outcome-invalid422 + Section 10.2 +
deadline-invalid422 + Section 14.2 +
evidence-nonconformant422 + Section 6 +
facilitator-mismatch422 + Section 13.1 +
finality-ordering-violation422 + Section 10.3 +
flow-unsupported422 + Section 7.1 +
internal-error500 + Section 13 +
no-recorded-delivery409 + Section 7.2 +
object-conflict409 + Section 13.2 +
parent-unresolvable422 + Section 10.2 +
parties-not-distinct422 + Section 14.2 +
payload-too-large413 + Section 13 +
proof-nonconformant422 + Section 7.3 +
retrieval-restricted403 + Section 17.12 +
schema-invalid422 + Section 14.2 +
settlement-unsupported422 + Section 13.1 +
signature-invalid401 + Section 14.1 +
signature-missing401 + Section 14.2 +
signatures-unordered422 + Section 14.1 +
terms-parameters-invalid422 + Section 5.3 +
terms-unsupported422 + Section 5.3 +
unexpected-signer422 + Section 14.2 +
unknown-contract404 + Section 13 +
verdict-nonconformant422 + Section 7.2 +
verifier-not-independent422 + Section 9.1 +
wrong-state409 + Section 4.2 +
+
+

The table is generated from the reference implementation's own + list, so that every type an implementation of this document emits + has a line here. The -01 revision listed eight of the twenty-nine its + implementation used.

+
+
+
+
+
+

+20. Normative References +

+
+
[RFC2119]
+
+Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/info/rfc2119>.
+
+
[RFC8174]
+
+Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/info/rfc8174>.
+
+
[RFC8785]
+
+Rundgren, A., Jordan, B., and S. Erdtman, "JSON Canonicalization Scheme (JCS)", RFC 8785, DOI 10.17487/RFC8785, , <https://www.rfc-editor.org/info/rfc8785>.
+
+
[RFC9457]
+
+Nottingham, M., Wilde, E., and S. Dalal, "Problem Details for HTTP APIs", RFC 9457, DOI 10.17487/RFC9457, , <https://www.rfc-editor.org/info/rfc9457>.
+
+
[RFC7515]
+
+Jones, M., Bradley, J., and N. Sakimura, "JSON Web Signature (JWS)", RFC 7515, DOI 10.17487/RFC7515, , <https://www.rfc-editor.org/info/rfc7515>.
+
+
[RFC7518]
+
+Jones, M., "JSON Web Algorithms (JWA)", RFC 7518, DOI 10.17487/RFC7518, , <https://www.rfc-editor.org/info/rfc7518>.
+
+
[RFC8037]
+
+Liusvaara, I., "CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE)", RFC 8037, DOI 10.17487/RFC8037, , <https://www.rfc-editor.org/info/rfc8037>.
+
+
[RFC8032]
+
+Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, DOI 10.17487/RFC8032, , <https://www.rfc-editor.org/info/rfc8032>.
+
+
[RFC7517]
+
+Jones, M., "JSON Web Key (JWK)", RFC 7517, DOI 10.17487/RFC7517, , <https://www.rfc-editor.org/info/rfc7517>.
+
+
[RFC8615]
+
+Nottingham, M., "Well-Known Uniform Resource Identifiers (URIs)", RFC 8615, DOI 10.17487/RFC8615, , <https://www.rfc-editor.org/info/rfc8615>.
+
+
[RFC6838]
+
+Freed, N., Klensin, J., and T. Hansen, "Media Type Specifications and Registration Procedures", BCP 13, RFC 6838, DOI 10.17487/RFC6838, , <https://www.rfc-editor.org/info/rfc6838>.
+
+
[RFC8259]
+
+Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, , <https://www.rfc-editor.org/info/rfc8259>.
+
+
[RFC9162]
+
+Laurie, B., Messeri, E., and R. Stradling, "Certificate Transparency Version 2.0", RFC 9162, DOI 10.17487/RFC9162, , <https://www.rfc-editor.org/info/rfc9162>.
+
+
[RFC3339]
+
+Klyne, G. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, DOI 10.17487/RFC3339, , <https://www.rfc-editor.org/info/rfc3339>.
+
+
[RFC9110]
+
+Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Semantics", STD 97, RFC 9110, DOI 10.17487/RFC9110, , <https://www.rfc-editor.org/info/rfc9110>.
+
+
[RFC9325]
+
+Sheffer, Y., Saint-Andre, P., and T. Fossati, "Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)", BCP 195, RFC 9325, DOI 10.17487/RFC9325, , <https://www.rfc-editor.org/info/rfc9325>.
+
+
[RFC4151]
+
+Kindberg, T. and S. Hawke, "The 'tag' URI Scheme", RFC 4151, DOI 10.17487/RFC4151, , <https://www.rfc-editor.org/info/rfc4151>.
+
+
[RFC1918]
+
+Rekhter, Y., Moskowitz, B., Karrenberg, D., de Groot, G. J., and E. Lear, "Address Allocation for Private Internets", BCP 5, RFC 1918, DOI 10.17487/RFC1918, , <https://www.rfc-editor.org/info/rfc1918>.
+
+
[RFC4193]
+
+Hinden, R. and B. Haberman, "Unique Local IPv6 Unicast Addresses", RFC 4193, DOI 10.17487/RFC4193, , <https://www.rfc-editor.org/info/rfc4193>.
+
+
[I-D.bhutton-json-schema]
+
+Wright, A., Andrews, H., Hutton, B., and G. Dennis, "JSON Schema: A Media Type for Describing JSON Documents", Work in Progress, Internet-Draft, draft-bhutton-json-schema-01, , <https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01>.
+
+
[DID-CORE]
+
+W3C, "Decentralized Identifiers (DIDs) v1.0", W3C Recommendation, , <https://www.w3.org/TR/2022/REC-did-core-20220719/>.
+
+
[DID-WEB]
+
+W3C Credentials Community Group, "did:web Method Specification", , <https://w3c-ccg.github.io/did-method-web/>.
+
+
+
+
+

+21. Informative References +

+
+
[RFC9334]
+
+Birkholz, H., Thaler, D., Richardson, M., Smith, N., and W. Pan, "Remote ATtestation procedureS (RATS) Architecture", RFC 9334, DOI 10.17487/RFC9334, , <https://www.rfc-editor.org/info/rfc9334>.
+
+
[RFC9711]
+
+Lundblade, L., Mandyam, G., O'Donoghue, J., and C. Wallace, "The Entity Attestation Token (EAT)", RFC 9711, DOI 10.17487/RFC9711, , <https://www.rfc-editor.org/info/rfc9711>.
+
+
[RFC9943]
+
+Birkholz, H., Delignat-Lavaud, A., Fournet, C., Deshpande, Y., and S. Lasker, "An Architecture for Trustworthy and Transparent Digital Supply Chains", RFC 9943, DOI 10.17487/RFC9943, , <https://www.rfc-editor.org/info/rfc9943>.
+
+
[RFC9942]
+
+Steele, O., Birkholz, H., Delignat-Lavaud, A., and C. Fournet, "CBOR Object Signing and Encryption (COSE) Receipts", RFC 9942, DOI 10.17487/RFC9942, , <https://www.rfc-editor.org/info/rfc9942>.
+
+
[RFC8725]
+
+Sheffer, Y., Hardt, D., and M. Jones, "JSON Web Token Best Current Practices", BCP 225, RFC 8725, DOI 10.17487/RFC8725, , <https://www.rfc-editor.org/info/rfc8725>.
+
+
[RFC7942]
+
+Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", BCP 205, RFC 7942, DOI 10.17487/RFC7942, , <https://www.rfc-editor.org/info/rfc7942>.
+
+
[RFC8555]
+
+Barnes, R., Hoffman-Andrews, J., McCarney, D., and J. Kasten, "Automatic Certificate Management Environment (ACME)", RFC 8555, DOI 10.17487/RFC8555, , <https://www.rfc-editor.org/info/rfc8555>.
+
+
[RFC5280]
+
+Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., and W. Polk, "Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile", RFC 5280, DOI 10.17487/RFC5280, , <https://www.rfc-editor.org/info/rfc5280>.
+
+
[RFC3647]
+
+Chokhani, S., Ford, W., Sabett, R., Merrill, C., and S. Wu, "Internet X.509 Public Key Infrastructure Certificate Policy and Certification Practices Framework", RFC 3647, DOI 10.17487/RFC3647, , <https://www.rfc-editor.org/info/rfc3647>.
+
+
[RFC2801]
+
+Burdett, D., "Internet Open Trading Protocol - IOTP Version 1.0", RFC 2801, DOI 10.17487/RFC2801, , <https://www.rfc-editor.org/info/rfc2801>.
+
+
[I-D.ietf-httpapi-idempotency-key-header]
+
+Jena, J. and S. Dalal, "The Idempotency-Key HTTP Header Field", Work in Progress, Internet-Draft, draft-ietf-httpapi-idempotency-key-header-07, , <https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-07>.
+
+
[I-D.ietf-satp-core]
+
+Hargreaves, M., Hardjono, T., Belchior, R., Ramakrishna, V., and A. Chiriac, "Secure Asset Transfer Protocol (SATP) Core", Work in Progress, Internet-Draft, draft-ietf-satp-core-16, , <https://datatracker.ietf.org/doc/html/draft-ietf-satp-core-16>.
+
+
[I-D.hood-agtp-commerce]
+
+Hood, C., "AGTP-Commerce: Open Commerce Specification for Agent-to-Agent Transactions", Work in Progress, Internet-Draft, draft-hood-agtp-commerce-00, , <https://datatracker.ietf.org/doc/html/draft-hood-agtp-commerce-00>.
+
+
[I-D.ietf-wimse-aims]
+
+Kasselman, P., Lombardo, J., Rosomakho, Y., Campbell, B., Steele, N., and A. Parecki, "AI Identity Management System", Work in Progress, Internet-Draft, draft-ietf-wimse-aims-00, , <https://datatracker.ietf.org/doc/html/draft-ietf-wimse-aims-00>.
+
+
[I-D.stone-vcap-ap2-binding]
+
+Stone, B. E. N. S. S. T. O. N., "VCAP-AP2 Binding: Verified Delivery Settlement for the Agent Payments Protocol", Work in Progress, Internet-Draft, draft-stone-vcap-ap2-binding-01, , <https://datatracker.ietf.org/doc/html/draft-stone-vcap-ap2-binding-01>.
+
+
[I-D.sahu-agent-action-receipts]
+
+sahu, N., "Signed, Hash-Chained Action Receipts for AI Agents", Work in Progress, Internet-Draft, draft-sahu-agent-action-receipts-00, , <https://datatracker.ietf.org/doc/html/draft-sahu-agent-action-receipts-00>.
+
+
[I-D.mih-sato-agent-accountability-composition]
+
+Mih, S., Sato, Schrock, I., Bu, S., and A. Sokolov, "Agent Accountability: Composition and Conformance", Work in Progress, Internet-Draft, draft-mih-sato-agent-accountability-composition-01, , <https://datatracker.ietf.org/doc/html/draft-mih-sato-agent-accountability-composition-01>.
+
+
[I-D.asor-wimse-agent-delegation-chain]
+
+Asor, R., "Verifiable Attenuated Delegation for AI Agent Chains", Work in Progress, Internet-Draft, draft-asor-wimse-agent-delegation-chain-01, , <https://datatracker.ietf.org/doc/html/draft-asor-wimse-agent-delegation-chain-01>.
+
+
[I-D.pinto-agent-authz-contestability]
+
+Pinto, T., "Contestability Bindings for Authorized Agent Actions", Work in Progress, Internet-Draft, draft-pinto-agent-authz-contestability-01, , <https://datatracker.ietf.org/doc/html/draft-pinto-agent-authz-contestability-01>.
+
+
[I-D.laxsharma-pact-01]
+
+Sharma, L., "PACT: Liability and Settlement for Autonomous Agent Contracts", Internet-Draft, draft-laxsharma-pact-01, superseded by this document, , <https://www.ietf.org/archive/id/draft-laxsharma-pact-01.html>.
+
+
[ASOKAN98]
+
+Asokan, N., Shoup, V., and M. Waidner, "Asynchronous Protocols for Optimistic Fair Exchange", Proceedings of the IEEE Symposium on Security and Privacy, , <https://doi.org/10.1109/secpri.1998.674826>.
+
+
[BELENKIY08]
+
+Belenkiy, M., Chase, M., Erway, C.C., Jannotti, J., Kupcu, A., and A. Lysyanskaya, "Incentivizing Outsourced Computation", Proceedings of the 3rd International Workshop on Economics + of Networked Systems (NetEcon '08), pp. 85-90, , <https://doi.org/10.1145/1403027.1403046>.
+
+
[POLINSKY99]
+
+Polinsky, A.M. and S. Shavell, "Public Enforcement of Law", Encyclopedia of Law and Economics, entry 8000, + Edward Elgar. The result is attributed therein to Bentham (1789), .
+
+
[SP800-186]
+
+National Institute of Standards and Technology, "Recommendations for Discrete Logarithm-based Cryptography: Elliptic Curve Domain Parameters", NIST Special Publication 800-186, , <https://doi.org/10.6028/NIST.SP.800-186>.
+
+
+
+
+
+

+Appendix A. An Example Terms Profile: bonded-restitution +

+

This appendix is not normative. It carries one terms profile, + under an example identifier and unregistered, so that the experiment + in Section 1.4 has something to run against and the + vectors in the reference repository have something to reproduce. It + is the -01 revision's settlement content written as a schedule over + the events of Section 4.2, with the choices the -01 + revision left open now made, and it is offered as an example of the + form a profile takes, not as a recommendation of these terms. What the + figures below mean between the parties to a contract that names this + profile is a question this document does not answer and its author is + not qualified to answer; a profile meant for use needs an owner who + is.

+
+
+

+A.1. Identity and Bundle +

+

Identifier: + tag:laxsharma79@gmail.com,2026:pact:bonded-restitution. + The bundle in the reference repository, under + profiles/bonded-restitution/, contains + README.md (this text), parameters.schema.json and + vectors.json; profile_hash is the manifest digest + over those three files and Section 15 prints it. + Problem types this profile reports are under the prefix + tag:laxsharma79@gmail.com,2026:pact:bonded-restitution:problem:.

+
+
+
+
+

+A.2. Parameters +

+
+
+seller_bond:
+
amount, required. What the + Seller posts before performance. +
+
+
+verification_fund:
+
amount, required. What + the Buyer posts to pay for checking. +
+
+
+cap:
+
amount, required. The most that leaves + the Seller's accounts under this contract. +
+
+
+restitution_basis:
+
string, required. + released or price. +
+
+
+remainder_to:
+
string, optional. + buyer or sink; sink when absent. +
+
+
+verifier_fee:
+
amount, optional. Paid from + the fund at each Verdict; 0.00 when absent. +
+
+
+principal_on:
+
string, required. The event at + which the price moves to the Seller: verdict (a PASS + Verdict), delivered, or window-closed. +
+
+
+assurance:
+
object, required. mode + (certain, committed-sample or open) and + q_min (a number greater than zero and at most one). +
+
+
+

The -01 revision's four release modes map onto flow and + principal_on as Appendix B shows.

+
+
+
+
+

+A.3. Accounts +

+

Three internal accounts, opened empty: escrow, + bond, fund. External accounts, unbounded as sources + and sinks: buyer, seller, verifier, + challenger:<kid> for each Challenger, and + sink. Closure requires the three internal accounts to hold + zero after the last entry.

+
+
+
+
+

+A.4. Admission +

+

At accepted the profile evaluates, exactly and in the + contract's currency, with P the price, B seller_bond, q + assurance.q_min, and E equal to P when principal_on + is delivered and zero otherwise:

+
+
+        B  >=  P * (1 - q) / q  +  E
+
+
+

and reports assurance-constraint-unsatisfied when it does + not hold, or when assurance.mode is open alone. The + inequality is the classical deterrence bound + ([POLINSKY99]; [BELENKIY08] Theorem 1 + for outsourced computation), with E the one term the -01 revision + added: value that moved before a Verdict cannot be recovered by the + schedule, so it raises what the Seller must post one for one. A + contract whose seller_bond or verification_fund + exceeds cap is reported as + parameters-inconsistent.

+
+
+
+
+

+A.5. Schedule +

+

For each event the schedule emits the entries below, in the order + listed, omitting any entry whose amount is zero. Every event of + Table 2 not named here emits nothing. Amounts + are computed from the contract and the trace prefix; "released" is + the sum of principal entries emitted so far.

+
+
+funded:
+
buyer to escrow, P, lock; + seller to bond, B, bond; buyer to fund, + verification_fund, fund. +
+
+
+delivered:
+
if principal_on is + delivered: escrow to seller, the escrow balance, + principal. +
+
+
+verdict:
+
fund to verifier, the lesser of + verifier_fee and the fund balance, + verification; then if the outcome is PASS, no Challenge is + answered, and principal_on is verdict: escrow to + seller, the escrow balance, principal. +
+
+
+window-closed:
+
if principal_on is + window-closed and the standing Verdict is not FAIL: escrow + to seller, the escrow balance, principal. +
+
+
+terminal, FINAL:
+
escrow to seller, the escrow + balance, principal; bond to seller, the bond balance, + return; fund to buyer, the fund balance, + fund-return. +
+
+
+terminal, ABANDONED:
+
escrow to buyer, the + escrow balance, reverse; bond to seller, the bond balance, + return; fund to buyer, the fund balance, + fund-return. The -01 revision said the bond was slashed + "to the extent of" the basis here and never said by how much; with + the price reversed the Buyer's loss is zero under either basis, so + nothing is slashed. +
+
+
+terminal, SETTLED:
+
in five ranks, each drawing + only what remains. (1) escrow to buyer, the escrow balance, + reverse. (2) if challenge_upheld: fund to the + Challenger whose Challenge the standing Verdict answers, the lesser + of that Challenge's costs and the fund balance, + costs. (3) bond to buyer, the lesser of the bond balance, + cap, and the Buyer's loss, restitution; the loss + is "released" under basis released and P minus the rank-1 + entry under basis price, which differ only when the price + moved in part. (4) if challenge_upheld: bond to that + Challenger, the bond balance, bounty. (5) bond to buyer or + sink per remainder_to, the bond balance, + remainder. Then fund to buyer, the fund balance, + fund-return. +
+
+
+

Ranks 2 and 4 pay one Challenger, the one whose Challenge the + standing Verdict answers. A Challenge that was not answered by the + standing Verdict, whether lapsed, rejected or superseded, receives + nothing. Rank 4 gives the whole remaining bond, because the -01 + revision forbade capping it at a fraction chosen for tidiness and + fixed no figure; a profile owner who wants a different rule changes + this line and the vectors with it.

+
+
+
+
+

+A.6. Vectors +

+

With P 180.00, B 18.00, fund 0.50, cap 180.00, basis + released, remainder to sink, no verifier fee, + principal_on verdict, assurance certain + with q 1.0, under the verdict-first flow, and a Challenge + claiming costs of 1.20. Amounts are in USDC. Trace indexes count + from zero. The lists below are what vectors.json carries + for the two paths in the figures of this document; the repository's + file also carries the SETTLED-by-Verifier and ABANDONED paths and + the price basis.

+
+
+
+
+ trace   0 accepted  1 funded  2 delivered  3 verdict PASS
+         4 window-opened  5 window-closed  6 children-final
+         7 terminal FINAL
+
+ event  from     to        amount   code
+   1    buyer    escrow    180.00   lock
+   1    seller   bond       18.00   bond
+   1    buyer    fund        0.50   fund
+   3    escrow   seller    180.00   principal
+   7    bond     seller     18.00   return
+   7    fund     buyer       0.50   fund-return
+
+
+
Figure 14: +FINAL: the path of Figure 1 +
+
+
+
+
+
+ trace   0 accepted  1 funded  2 delivered  3 verdict PASS
+         4 window-opened  5 challenge  6 verdict FAIL (answers 5,
+         supersedes 3)  7 children-final  8 terminal SETTLED,
+         challenge_upheld true
+
+ event  from     to               amount   code
+   1    buyer    escrow           180.00   lock
+   1    seller   bond              18.00   bond
+   1    buyer    fund               0.50   fund
+   3    escrow   seller           180.00   principal
+   8    fund     challenger:<kid>   0.50   costs
+   8    bond     buyer             18.00   restitution
+
+
+
Figure 15: +SETTLED on an upheld Challenge: the path of Figure 5 +
+
+

In the second vector rank 1 emits nothing because the escrow is + empty, rank 2 pays the lesser of 1.20 and the fund's 0.50, rank 3 + pays the whole bond because the Buyer's loss (180.00 released) exceeds + it, and ranks 4 and 5 and the fund return emit nothing because + nothing remains. Both lists satisfy closure: after the last entry the + three internal accounts hold zero.

+
+
+
+
+
+
+

+Appendix B. Changes from -01 +

+

This revision separates the protocol from the meaning of its terms. + The -01 revision, in its title, abstract, Section 1.2 and throughout, + made who owed whom the subject of the document; two readers on the + IETF dispatch list observed in September 2026 that this placed it + outside what the IETF is placed to evaluate, and they were right. What + follows is the list of what changed, with the wire consequences + first.

+
    +
  • The pact version is 0.2 and every committed digest + changed (Section 15). +
  • +
  • The liability member is gone. A contract carries + terms: a profile URI, a digest over the profile's bundle, + and an opaque parameter object (Section 5.3). The -01 + figures are the parameters of the profile in + Appendix A. assurance moved into + that profile's parameters; parent moved to the top level + and gained facilitator. +
  • +
  • The four release modes are replaced by three flows and a + profile parameter: on-verification is + verdict-first with principal_on verdict; + on-window is delivery-first with + window-closed; optimistic is + delivery-first with delivered; + unsecured is no-window with delivered + (Section 7.1). +
  • +
  • + verification.max_verdict_seconds is added, with the + verdict-lapsed event, so a silent Verifier cannot hold a + contract in DELIVERED forever (Section 7.2). +
  • +
  • The Work Attestation is the Outcome Record, with the RATS + collision explained (Section 1.3). Its subject, + role, amounts and outcome vocabulary are + replaced by parties, an outcome object, the full + trace, and terms_result (Section 12). One + record per contract. +
  • +
  • Every response is a signed Contract Status carrying the trace, + replacing the unsigned state member the -01 revision added + to echoed objects (Section 11). +
  • +
  • The event trace and the state machine over it are new + (Section 4.2); RELEASING and PROPOSED are gone, + AWAITING_CHILDREN is added. +
  • +
  • + delivery_hash covers the Delivery's signature; every + digest covers the signature set (Section 2). + Signature sets are sorted and ECDSA is low-S + (Section 14.1). Merkle leaves cover signatures + (Section 12.2). +
  • +
  • A nonconformant Delivery is refused and recorded nowhere; the + -01 revision treated it as a FAIL Verdict + (Section 6). The Buyer countersignature sentence is + withdrawn. +
  • +
  • A Verdict may carry challenge_hash; a Challenge may + carry costs; a Seller-signed Challenge is refused + (Section 7). +
  • +
  • Contract trees work across Facilitators: child registration, + child outcome supply, child-unresolved, a finite latest + finality instant per contract and the rule L(child) before + L(parent); the depth and cycle rules are withdrawn with the reason + (Section 10). The -01 Section 10.2 is one sentence in + Section 10.3. +
  • +
  • Section 3 is a data dictionary and a role table + (Section 3); no sentence in it requires anything of + a party. +
  • +
  • Media types move to the vendor tree; the two registries and + the pact-escrow row are withdrawn; problem types move to a + tag URI namespace and the table lists every type the implementation + emits (Section 19). +
  • +
  • Retrieval is restricted by default and fetch discipline is + stated (Section 17.12, Section 17.5). + The threat model says plainly what is enforced against a + Facilitator, which is nothing, and what is attributable + (Section 17.1). +
  • +
  • The experiment is restated over protocol observables + (Section 1.4). +
  • +
+
+
+
+
+

+Acknowledgements +

+

Rich Salz and John C Klensin, on the IETF dispatch list in + September 2026, read the -01 revision as a document about who owes + whom with a protocol attached, and said so; this revision's split + between records and terms is the consequence, and the author is + grateful for the reading. The UTF-16 key-ordering vector that exposed + a latent canonicalization defect in the reference validator, and the + formulation of verifier independence as a relation the evaluator + derives rather than a field the record declares, came from Tersign + (wowlegend) on x402-foundation/x402 issue 3065. The observation that + verification tiers say how work is checked and never who checks it + came from msaleme on the same thread. Rich Smith's A2A Settlement + Extension was the clearest instance of the pattern the -01 revision + corrected, and he engaged with the critique on a2aproject/A2A + discussion 1576.

+
+
+
+
+

+Author's Address +

+
+
Laxmikant Sharma
+
Independent
+ +
+
+
+ + + diff --git a/draft/draft-laxsharma-pact-02.txt b/draft/draft-laxsharma-pact-02.txt new file mode 100644 index 0000000..05654be --- /dev/null +++ b/draft/draft-laxsharma-pact-02.txt @@ -0,0 +1,4032 @@ + + + + +Network Working Group L. Sharma +Internet-Draft Independent +Intended status: Experimental 16 September 2026 +Expires: 20 March 2027 + + + PACT: Co-Signed Task Contracts, Delivery and Verdict Records, and + Outcome Records for Autonomous Agents + draft-laxsharma-pact-02 + +Abstract + + Autonomous agents can already prove who they are, show whose + authority they act under, find one another, call one another, and + pay. What they cannot do with any existing specification is agree on + a task in a form a third party can check, deliver against it, have + the delivery judged by someone other than the performer, and carry + away a record of the outcome that a stranger can verify. This + document specifies PACT, a set of signed JSON records that closes + that gap. + + PACT defines four things: a co-signed task contract whose digest + covers its signature set, so the commitment proves who agreed and not + only what was written; a Verdict record bound by digest to the + Delivery record it judges; a Facilitator-signed event trace and + Outcome Record for every contract, so what happened is recorded once, + in one order, by a party that is not the performer; and a Merkle + commitment from a parent contract's Outcome Record to the Outcome + Records of its subcontracts. + + Settlement terms are carried by reference to a profile defined + outside this document. This document specifies no escrow, custody or + release of value, and takes no position on the legal effect of any + record it defines. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + + + + + + +Sharma Expires 20 March 2027 [Page 1] + +Internet-Draft PACT September 2026 + + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 20 March 2027. + +Copyright Notice + + Copyright (c) 2026 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4 + 1.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . 4 + 1.2. What This Document Specifies, and What It Does Not . . . 5 + 1.3. Relationship to Existing Work . . . . . . . . . . . . . . 6 + 1.4. The Experiment . . . . . . . . . . . . . . . . . . . . . 7 + 2. Conventions and Definitions . . . . . . . . . . . . . . . . . 7 + 2.1. Terminology . . . . . . . . . . . . . . . . . . . . . . . 9 + 3. Data Dictionary . . . . . . . . . . . . . . . . . . . . . . . 10 + 3.1. Members Common to Every Record . . . . . . . . . . . . . 10 + 3.2. Contract Members . . . . . . . . . . . . . . . . . . . . 10 + 3.3. TaskSpec Members . . . . . . . . . . . . . . . . . . . . 11 + 3.4. Delivery Members . . . . . . . . . . . . . . . . . . . . 12 + 3.5. Verdict Members . . . . . . . . . . . . . . . . . . . . . 12 + 3.6. Challenge Members . . . . . . . . . . . . . . . . . . . . 13 + 3.7. Contract Status Members . . . . . . . . . . . . . . . . . 13 + 3.8. Outcome Record Members . . . . . . . . . . . . . . . . . 14 + 3.9. Capability Document Members . . . . . . . . . . . . . . . 14 + 3.10. Roles . . . . . . . . . . . . . . . . . . . . . . . . . . 15 + 4. Protocol Overview . . . . . . . . . . . . . . . . . . . . . . 16 + 4.1. States . . . . . . . . . . . . . . . . . . . . . . . . . 17 + 4.2. Events . . . . . . . . . . . . . . . . . . . . . . . . . 18 + 5. The Verifiable Task Contract . . . . . . . . . . . . . . . . 21 + 5.1. Hash Commitments and Content Conveyance . . . . . . . . . 23 + 5.2. The Task Specification . . . . . . . . . . . . . . . . . 23 + 5.3. Terms . . . . . . . . . . . . . . . . . . . . . . . . . . 24 + + + +Sharma Expires 20 March 2027 [Page 2] + +Internet-Draft PACT September 2026 + + + 6. The Delivery Record . . . . . . . . . . . . . . . . . . . . . 25 + 7. Verdicts, Challenges and the Window . . . . . . . . . . . . . 26 + 7.1. Flows . . . . . . . . . . . . . . . . . . . . . . . . . . 26 + 7.2. Verdicts . . . . . . . . . . . . . . . . . . . . . . . . 26 + 7.3. Challenges . . . . . . . . . . . . . . . . . . . . . . . 28 + 7.4. Disputes and Lapses . . . . . . . . . . . . . . . . . . . 29 + 8. Facilitator Capability Discovery . . . . . . . . . . . . . . 29 + 9. Verification Profiles . . . . . . . . . . . . . . . . . . . . 31 + 9.1. Verifier Independence and Identifier Normalization . . . 32 + 10. Contract Trees . . . . . . . . . . . . . . . . . . . . . . . 32 + 10.1. Binding a Child to Its Parent . . . . . . . . . . . . . 33 + 10.2. Registration and Children Final . . . . . . . . . . . . 33 + 10.3. Finality Is Bottom-Up . . . . . . . . . . . . . . . . . 34 + 11. The Contract Status . . . . . . . . . . . . . . . . . . . . . 35 + 12. Outcome Records . . . . . . . . . . . . . . . . . . . . . . . 36 + 12.1. The Terms Result . . . . . . . . . . . . . . . . . . . . 39 + 12.2. The Children Merkle Root . . . . . . . . . . . . . . . . 39 + 13. Protocol Endpoints . . . . . . . . . . . . . . . . . . . . . 40 + 13.1. Proposing a Contract . . . . . . . . . . . . . . . . . . 41 + 13.2. Idempotency . . . . . . . . . . . . . . . . . . . . . . 41 + 13.3. Error Responses . . . . . . . . . . . . . . . . . . . . 42 + 13.4. Exchange . . . . . . . . . . . . . . . . . . . . . . . . 42 + 14. Conformance . . . . . . . . . . . . . . . . . . . . . . . . . 43 + 14.1. Signatures . . . . . . . . . . . . . . . . . . . . . . . 43 + 14.1.1. Key Resolution . . . . . . . . . . . . . . . . . . . 44 + 14.2. Rules Not Expressible in a Schema . . . . . . . . . . . 45 + 14.3. Test Vectors . . . . . . . . . . . . . . . . . . . . . . 46 + 15. Worked Example . . . . . . . . . . . . . . . . . . . . . . . 47 + 16. Implementation Status . . . . . . . . . . . . . . . . . . . . 48 + 17. Security Considerations . . . . . . . . . . . . . . . . . . . 49 + 17.1. Trust in the Facilitator . . . . . . . . . . . . . . . . 51 + 17.2. Verifier Capture . . . . . . . . . . . . . . . . . . . . 51 + 17.3. Algorithm, Key and Encoding Confusion . . . . . . . . . 52 + 17.4. Substitution of Committed Content . . . . . . . . . . . 52 + 17.5. Fetching Committed Content . . . . . . . . . . . . . . . 52 + 17.6. Children: Attachment and Omission . . . . . . . . . . . 53 + 17.7. Buying Silence from a Challenger . . . . . . . . . . . . 53 + 17.8. Non-Delivery . . . . . . . . . . . . . . . . . . . . . . 53 + 17.9. Cross-Venue Replay . . . . . . . . . . . . . . . . . . . 53 + 17.10. Nondeterminism as Shield and as Weapon . . . . . . . . . 54 + 17.11. Fabricated History . . . . . . . . . . . . . . . . . . . 54 + 17.12. Retrieval . . . . . . . . . . . . . . . . . . . . . . . 54 + 17.13. Key Compromise and Rotation . . . . . . . . . . . . . . 54 + 17.14. Denial of Service by Challenge . . . . . . . . . . . . . 55 + 18. Privacy Considerations . . . . . . . . . . . . . . . . . . . 55 + 18.1. Input Disclosure Before Contract Formation . . . . . . . 55 + 18.2. The Contract Graph . . . . . . . . . . . . . . . . . . . 55 + 18.3. Challenger Access . . . . . . . . . . . . . . . . . . . 56 + + + +Sharma Expires 20 March 2027 [Page 3] + +Internet-Draft PACT September 2026 + + + 18.4. Retention . . . . . . . . . . . . . . . . . . . . . . . 56 + 19. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 56 + 19.1. Media Types . . . . . . . . . . . . . . . . . . . . . . 56 + 19.2. Well-Known URI . . . . . . . . . . . . . . . . . . . . . 58 + 19.3. Problem Types . . . . . . . . . . . . . . . . . . . . . 59 + 20. Normative References . . . . . . . . . . . . . . . . . . . . 60 + 21. Informative References . . . . . . . . . . . . . . . . . . . 62 + Appendix A. An Example Terms Profile: bonded-restitution . . . . 66 + A.1. Identity and Bundle . . . . . . . . . . . . . . . . . . . 66 + A.2. Parameters . . . . . . . . . . . . . . . . . . . . . . . 66 + A.3. Accounts . . . . . . . . . . . . . . . . . . . . . . . . 67 + A.4. Admission . . . . . . . . . . . . . . . . . . . . . . . . 67 + A.5. Schedule . . . . . . . . . . . . . . . . . . . . . . . . 67 + A.6. Vectors . . . . . . . . . . . . . . . . . . . . . . . . . 69 + Appendix B. Changes from -01 . . . . . . . . . . . . . . . . . . 70 + Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 71 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 71 + +1. Introduction + +1.1. Motivation + + By late 2026 an autonomous agent can prove who it is, show whose + authority it acts under, discover another agent, call it, record what + happened in a tamper-evident receipt, and pay for the call. Each of + those is the subject of active standardisation, and several are + specified in more detail than this document specifies anything. + + What none of them provides is interoperability at the level of the + task. Two agents built by different vendors have no common record of + what one asked the other to do, no common form for the result, no way + to have that result judged by a third implementation against criteria + fixed before the work began, and no record of the outcome that a + fourth implementation can verify without trusting any of the first + three. Receipts record that an action occurred. Audit records + establish whether behaviour matched intent. Payment schemes move + value on the payer's instruction. None of them says what was agreed, + what was delivered, or whether the one met the other. + + That gap is not an oversight in those documents; it is outside their + scope, and correctly so. It is the gap this document addresses, and + only that gap. + + + + + + + + + +Sharma Expires 20 March 2027 [Page 4] + +Internet-Draft PACT September 2026 + + +1.2. What This Document Specifies, and What It Does Not + + PACT specifies exactly four things: a co-signed contract record whose + digest covers its signature set (Section 5); a Delivery record and + the Verdict record bound to it by digest (Section 6, Section 7.2); an + event trace, signed by a Facilitator, from which one Outcome Record + per contract is produced (Section 11, Section 12); and a Merkle + commitment from a parent's Outcome Record to its children's + (Section 10). + + A contract names its settlement terms by reference: a profile + identifier, a digest over the profile's bytes, and a parameter object + that this document does not read (Section 5.3). What those terms + mean, and everything about who holds or moves value under them, is + the profile's to say. This document specifies the records, their + digests, who signs each one, the order in which a Facilitator records + events, and a commitment across records. That is the whole of it. + + A deployment relies on other specifications, agreements or + arrangements for: the meaning of the terms a contract names; agent + identity and key distribution; delegation of authority from a human + or organisational principal; agent discovery; transport security + beyond [RFC9325]; an audit or accountability architecture; a + transparency service; a payment rail or settlement network; a + reputation system; and the resolution of any disagreement the records + do not settle. + + Carrying terms by reference is an old pattern in this series. ACME + [RFC8555] carries a terms-of-service URL and requires a client to + assert agreement to it before an account is created, without defining + a single term. A certificate carries its policy as an identifier + whose rules live outside the IETF ([RFC5280], Section 4.2.1.4), and + the framework for writing those rules [RFC3647] says it does not aim + to provide legal advice. The Internet Open Trading Protocol + [RFC2801] specified the messages of a trade and left the trade's + terms to the parties. PACT follows that line. + + Two mechanisms present in the -00 revision remain withdrawn: contract + channels, and the sealed-bid award procedure. The reasons are + recorded in [I-D.laxsharma-pact-01] and are not repeated. The change + from -01 to this revision is listed in Appendix B. + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 5] + +Internet-Draft PACT September 2026 + + +1.3. Relationship to Existing Work + + PACT's agree, perform, verify, record loop is an instance of + optimistic fair exchange [ASOKAN98], in which a third party is + contacted only when the exchange fails. What that literature + establishes is what a third party must be able to observe for an + exchange to be fair; the records in this document are that + observation, written down in a form two implementations can compare. + + Two adjacent Internet-Drafts address agent commerce settlement + directly. [I-D.hood-agtp-commerce] carries Work Completion Records + and an audit-verified settlement timing; [I-D.stone-vcap-ap2-binding] + binds verified commerce settlement to the Agent Payments Protocol. + Neither carries a co-signed contract whose digest covers its + signatures, and PACT is designed to be usable alongside either. + + Five bodies of IETF work touch the same records, and the relationship + to each is stated here so that it is not left to the reader. + + RATS. [RFC9334] defines Verifier, Evidence and Attestation Result as + terms of art: a RATS Verifier appraises Evidence about an + Attester. PACT's Verifier evaluates a Delivery against an + instrument the parties committed to, and its terminal record is an + Outcome Record, not an attestation. The -01 revision called that + record a Work Attestation and used the RATS words with other + meanings; this revision renames the record and defines its + remaining shared vocabulary in Section 2.1. Where a verification + tier relies on hardware attestation, the RATS architecture applies + unchanged and PACT consumes its results. + + SCITT. [RFC9943] defines signed-statement transparency and [RFC9942] + defines COSE receipts for it. PACT does not define a transparency + service; a Facilitator that wants its Outcome Records to be + publicly append-only can register them with a SCITT transparency + service, and Section 17.1 says which Facilitator misbehaviour that + closes. The Merkle commitment in Section 12.2 is not a + transparency log: it is a fixed commitment from one record to a + known, finite set of other records, and it uses the tree of + [RFC9162] for its construction only. + + HTTPAPI. [I-D.ietf-httpapi-idempotency-key-header] is the general + mechanism for making a POST safe to retry. PACT does not use it, + because every object it carries is committed by the digest of its + own canonical form and that digest is the idempotency key + (Section 13.2). Error reporting follows [RFC9457]. + + WIMSE. [I-D.ietf-wimse-aims] gives workload and agent identity a + + + + +Sharma Expires 20 March 2027 [Page 6] + +Internet-Draft PACT September 2026 + + + home. PACT does not define an identity format; a kid resolves as + Section 14.1.1 says, and that section is written so that an + identity system defined elsewhere can be named without changing + this document. + + SATP. [I-D.ietf-satp-core] transfers a digital asset between two + gateways with evidence a third party can verify. An Outcome + Record is not an asset transfer and does not move one; it is a + signed statement that certain records were received in a certain + order, and what any of that means for an asset is the terms + profile's to say. + + Verification evidence formats for hardware-attested tiers are + specified in [RFC9334] and [RFC9711]. Signed, hash-chained action + receipts [I-D.sahu-agent-action-receipts], composition of + accountability records + [I-D.mih-sato-agent-accountability-composition], delegation chains + [I-D.asor-wimse-agent-delegation-chain], and contestability bindings + [I-D.pinto-agent-authz-contestability] are each specified elsewhere, + and PACT consumes rather than restates them. + +1.4. The Experiment + + This document is Experimental. The question it tests is stated over + protocol observables only. Given the same sequence of posted records + and the same clock readings, two independent Facilitator + implementations should produce the same event trace (Section 11). + Given the same trace and the same terms profile, they should produce + the same Outcome Record body (Section 12), byte for byte after + canonicalization. The experiment succeeds if two independent + Facilitators, serving Buyers and Sellers built by different + implementers, reach every terminal state in Figure 2 with Outcome + Records either can verify and that agree. It fails, and that would + itself be worth recording, if the trace turns out to under-determine + the outcome, which is to say if two honest implementations reading + the same records disagree about what happened. Experience should be + reported to the author and to the repository named in Section 16. + The non-normative profile in Appendix A exists so that the experiment + can be run before any other profile is written. + +2. Conventions and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in BCP + 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + + + +Sharma Expires 20 March 2027 [Page 7] + +Internet-Draft PACT September 2026 + + + Canonical form. Every JSON object defined here is canonicalized with + JCS [RFC8785] before hashing or signing. Implementations MUST order + object keys by UTF-16 code unit as [RFC8785] Section 3.2.3 requires. + Sorting by Unicode code point is a common substitution; it agrees + with the required order throughout the Basic Multilingual Plane and + diverges above it. + + Object digest. The digest of an object is the string sha256: + followed by the lowercase hexadecimal SHA-256 of the canonical form + of the whole object, including every signature member it carries. + Every hash member in this document that names another object + (vtc_hash, delivery_hash, challenge_hash, the object member of a + trace entry, and the leaves of Section 12.2) is that object's digest. + A digest that excluded signatures would prove what was written and + not who agreed to it; the -00 revision had that defect and the -01 + revision fixed it for the contract only. This revision applies one + construction everywhere. + + Signing input. A signature over an object is computed over the + canonical form of the object with the signing member (signature or + signatures) removed, as Section 14.1 specifies. The digest of an + object and the signing input of an object are therefore different + byte strings, and the difference is the signature set. + + Version. The pact member carries a version of the form major.minor; + this document defines 0.2. Every object defined here is hash- + committed and signed, so a member an implementation does not + recognise is inside the commitment and cannot be ignored safely. An + implementation MUST reject an object whose pact version it does not + implement, and MUST reject an object carrying a member this document + does not define for it, with one exception: the contents of + terms.parameters (Section 5.3) are defined by the named profile and + this document reads none of them. Extension is by a new version, not + by adding members. + + Time. Every timestamp is an RFC 3339 date-time [RFC3339] in UTC with + the "Z" designator. The Facilitator's clock governs every deadline + and window in this document: the instant at which the Facilitator + records an event is the instant that counts, that instant is what the + trace carries, and parties should allow for skew when acting near a + boundary. Section 17.1 says what that clock can and cannot prove. + + Amounts. An amount is a decimal string with no exponent and a + fractional part of two to eighteen digits; comparisons are exact and + no rounding is implied. A currency is an asset identifier whose + namespace is defined by the settlement binding named in + price.settlement, and need not be an ISO 4217 code. A network is a + ledger identifier in the form the same binding defines. This + + + +Sharma Expires 20 March 2027 [Page 8] + +Internet-Draft PACT September 2026 + + + document carries amounts; it does not say what any amount is for. + Where a record produced under this document lists amounts, as + terms_result does (Section 12.1), the meaning of every entry is the + named profile's. + + Identifiers. A party identifier is a URI. Two identifiers name the + same party when they are equal after the normalization in + Section 9.1, and every comparison of identifiers in this document is + made after that normalization. + +2.1. Terminology + + Four words in this document have meanings elsewhere that are close + enough to mislead, and are defined here once. + + Contract: Used in this document for a co-signed JSON object of the + form in Section 5, and for nothing else. This document takes no + position on whether any such object is a contract in law, in any + jurisdiction, and defines no obligation between the parties that + sign one. + + Verifier, Verdict: A Verifier here is the party that evaluates a + Delivery against the instrument the contract committed to, and a + Verdict is its signed finding. This is not the Verifier of + [RFC9334], which appraises Evidence about an Attester; the two + roles may be played by the same software in a hardware-attested + tier, and are still different roles. + + Evidence: The evidence member of a Delivery is the set of artefacts + a Verifier evaluates, produced by the Seller. It is not Evidence + in the sense of [RFC9334]. The member name is kept from -01 + because renaming it would change every committed digest for no + gain in clarity that this note does not provide. + + Facilitator: The party that runs the state machine of Section 4 for + a contract: it accepts or refuses the records posted to it, + records events in one order on its own clock, and signs the trace + and the Outcome Record. Nothing in this document says that a + Facilitator holds anything of value, and nothing in it requires + that it does. + + The remaining roles are defined by what they sign and receive in + Section 3.10, and the objects by their members in Section 3. + + + + + + + + +Sharma Expires 20 March 2027 [Page 9] + +Internet-Draft PACT September 2026 + + +3. Data Dictionary + + This section lists every member this document defines, by the object + that carries it, with its type, whether it is required in that + object, and what it commits to. It is a dictionary and not a + rulebook: the rule that a record omitting a required member, or + carrying one this document does not define for it, does not conform + is stated once in Section 2; the rules a Facilitator applies when it + accepts or refuses a record are in Section 14 and in the section that + defines the record. No sentence in this section requires anything of + any party. Where a member's meaning is the named terms profile's, + the entry says so and says nothing more. + + Types are JSON types. A digest is a string of the form in Section 2. + An amount is a string of the form in Section 2. A URI is a string. + A timestamp is a string of the form in Section 2. Cardinality is + written as required or optional. + +3.1. Members Common to Every Record + + pact: string, required. The protocol version; 0.2 for objects + defined by this document. + + type: string, required. The object's type name: + VerifiableTaskContract, Delivery, Verdict, Challenge, + ContractStatus, OutcomeRecord, or FacilitatorCapabilities. + + signature: object, required in Delivery, Verdict, Challenge, + ContractStatus and the capability document. One JWS entry of the + form in Section 14.1, by the single signer of that record. + + signatures: array of objects, required in the contract and in the + Outcome Record. JWS entries of the form in Section 14.1, sorted + as that section says. Commits, in the contract, to who agreed; in + the Outcome Record, to which Facilitator issued it. + +3.2. Contract Members + + Carried in the Verifiable Task Contract (Section 5), media type + application/vnd.pact.contract+json. + + id: string, required. Contract identifier, unique among the + contracts of the Facilitator named in parties.facilitator. + + parties: object, required. The identifiers of the parties, by role: + buyer (URI, required), seller (URI, required), facilitator (URI, + required), verifier (URI, optional). Commits to who plays each + role for this contract. + + + +Sharma Expires 20 March 2027 [Page 10] + +Internet-Draft PACT September 2026 + + + task: object, required. spec_hash (digest, required) commits to a + TaskSpec (Section 5.2); spec_uri (URI, optional) says where its + bytes may be fetched; deadline (timestamp, required) is the + instant after which the deadline-passed event may be recorded + (Section 4.2). + + price: object, required. amount (amount, required), currency + (string, required), settlement (URI, required, naming a settlement + binding), network (string, required, in the form the binding + defines). Commits to a figure and a venue that both parties + signed. The meaning of the figure is the named terms profile's. + + verification: object, required. tier (string, required), profile + (string or URI, required; Section 9), criteria_hash (digest, + required; the manifest digest of the acceptance instrument per + Section 5.1), max_verdict_seconds (integer, required; the longest + interval after delivered within which a first Verdict is recorded + before verdict-lapsed may be), arbiter (URI, optional). Commits + to how a Delivery is judged and by what. + + flow: string, required. One of verdict-first, delivery-first, no- + window (Section 7.1). Selects the shape of the state machine for + this contract. + + terms: object, required (Section 5.3). profile (URI, required) names + a terms profile; profile_hash (digest, required) commits to the + profile's bytes as Section 5.3 says; parameters (object, required, + may be empty) carries the profile's parameters. This document + reads no member of parameters; every one of them means what the + named profile says. + + challenge: object, required. window_seconds (integer, required, + greater than zero) is the duration of the challenge window; + max_dispute_seconds (integer, required) is the longest interval + after a challenge event within which a Verdict on that Challenge + is recorded before dispute-lapsed may be. + + parent: object, optional; present only in a subcontract + (Section 10). vtc_id (string, required), vtc_hash (digest, + required), and facilitator (URI, required) identify the parent + contract and the Facilitator that holds it. + +3.3. TaskSpec Members + + The TaskSpec is the content committed by task.spec_hash + (Section 5.2). It is not transmitted over the endpoints of this + document. + + + + +Sharma Expires 20 March 2027 [Page 11] + +Internet-Draft PACT September 2026 + + + description: string, required. A statement of the work in natural + language. + + inputs: object, optional. schema_uri with schema_hash, and where a + representative sample is published, sample_uri with sample_hash; + each URI with its digest over the dereferenced bytes. + + deliverable: object, required. format (string) and schema_uri with + schema_hash. + + acceptance: object, required. The verification instrument: + harness_uri with harness_hash for re-execution tiers, enclave and + model policy for attestation tiers, a proof statement with its + verifying key for proving tiers, or rubric_uri with rubric_hash + for judgment tiers; plus thresholds (object) in machine-readable + form. harness_hash equals the contract's criteria_hash. + + constraints: object, optional. Tool prohibitions, confidentiality + and compliance conditions, in a form this document does not + define. + +3.4. Delivery Members + + Carried in the Delivery (Section 6), media type application/ + vnd.pact.delivery+json. + + vtc_id, vtc_hash: string and digest, required. Identify and commit + to the contract performed. + + work_hash: digest, required. Commits to the delivered bytes, or to + a manifest per Section 5.1 where the deliverable is a bundle. + + work_uri: URI, optional. Where the bytes may be fetched, subject to + Section 17.5. + + input_hash: digest, required for tiers whose fraud proof re- + executes. Commits to the production input actually consumed. + + evidence: object, required. Members profiled by verification.tier + and verification.profile; for the acceptance profile, profile, + instrument_hash, results_hash and results_uri. Conformance to the + profile is a validity condition of the Delivery, not a judgement + on the work. + +3.5. Verdict Members + + Carried in the Verdict (Section 7.2), media type application/ + vnd.pact.verdict+json. + + + +Sharma Expires 20 March 2027 [Page 12] + +Internet-Draft PACT September 2026 + + + vtc_id: string, required. + + delivery_hash: digest, required. Commits to the Delivery judged, + including the Seller's signature over it. + + challenge_hash: digest, optional. Present when the Verdict answers + a Challenge; commits to that Challenge. + + outcome: string, required. PASS or FAIL. + + profile, instrument_hash: string and digest, required. The + verification profile applied and the digest of the instrument + actually run, which equals the contract's criteria_hash. + + results_hash: digest, required. Commits to the Verifier's own + results. + + evaluated_at: timestamp, required. The Verifier's own clock; + informational, since the trace carries the Facilitator's. + +3.6. Challenge Members + + Carried in the Challenge (Section 7.3), media type application/ + vnd.pact.challenge+json. + + vtc_id, delivery_hash: string and digest, required. Identify the + contract and commit to the Delivery challenged. + + proof: object, required. Members profiled by verification.profile; + for the acceptance profile, profile, instrument_hash, + results_hash, results_uri and failing_checks (array of strings). + + costs: object, optional. amount and currency: a figure the + Challenger asserts for producing the proof. This document records + it in the trace and reads it for nothing; its meaning is the named + terms profile's. + +3.7. Contract Status Members + + Carried in the Contract Status (Section 11), media type application/ + vnd.pact.status+json, the Facilitator's signed response to every + accepted request. + + vtc_id, vtc_hash: string and digest, required. + + state: string, required. A state name from Figure 2. + + trace: array of objects, required. The event trace so far, in the + + + +Sharma Expires 20 March 2027 [Page 13] + +Internet-Draft PACT September 2026 + + + order recorded (Section 4.2). Each entry carries event (string, + required), at (timestamp, required), object (digest, required + where the event was caused by a posted record), and the event- + specific members listed in Section 4.2. + + issued_at: timestamp, required. When this status was signed. + +3.8. Outcome Record Members + + Carried in the Outcome Record (Section 12), media type application/ + vnd.pact.outcome+json. + + vtc_id, vtc_hash: string and digest, required. + + parties: object, required. The contract's parties object, copied, + so that the record names its subjects and which side of the + contract each was on. + + outcome: object, required. state (string, required; FINAL, SETTLED + or ABANDONED) and challenge_upheld (boolean, required). + + work_hash: digest, required where a Delivery was recorded. Binds + the record to what was produced. + + trace: array of objects, required. The complete event trace, ending + with the terminal event. + + terms_result: object, required (Section 12.1). profile and + profile_hash (copied from the contract), currency (string), and + transfers (array of objects), each with from (string), to + (string), amount (amount) and code (string). The entries are the + named profile's output for the trace; this document defines their + form and two arithmetic invariants over them, and nothing about + their meaning. + + children_merkle_root: digest, required where the contract has + registered children and absent otherwise (Section 12.2). + +3.9. Capability Document Members + + Carried in the Facilitator capability document (Section 8), media + type application/vnd.pact.facilitator+json. + + facilitator: URI, required. The identifier that appears in + parties.facilitator. + + settlement_bindings: array of objects, required. Each with id + (URI), networks and assets (arrays of strings). + + + +Sharma Expires 20 March 2027 [Page 14] + +Internet-Draft PACT September 2026 + + + flows: array of strings, required. The flows of Section 7.1 the + Facilitator implements. + + verification_profiles: array of strings, required. + + terms_profiles: array of objects, required, with at least one entry. + Each with id (URI) and profile_hash (digest): the terms profiles, + at the revisions named, whose schedules this Facilitator + evaluates. + + max_contract_value: object, optional. amount and currency. + + challenge_deposit: object, optional. amount and currency; see + Section 7.3. + + endpoints: object, required. Maps each endpoint name in Section 13 + to an absolute URI. + +3.10. Roles + + A role is defined by where its identifier appears, what it signs, and + what it receives. Nothing else about a role is defined here. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 15] + +Internet-Draft PACT September 2026 + + + +=============+======================+=================+============+ + | Role | Identifier appears | Signs | Receives | + | | in | | | + +=============+======================+=================+============+ + | Buyer | parties.buyer | the contract; | Contract | + | | | a child | Status, | + | | | registration | Outcome | + | | | (Section | Record | + | | | 10.2) | | + +-------------+----------------------+-----------------+------------+ + | Seller | parties.seller | the contract; | Contract | + | | | the Delivery | Status, | + | | | | Outcome | + | | | | Record | + +-------------+----------------------+-----------------+------------+ + | Facilitator | parties.facilitator, | Contract | every | + | | parent.facilitator, | Status, | posted | + | | the capability | Outcome | record | + | | document | Record, the | | + | | | capability | | + | | | document | | + +-------------+----------------------+-----------------+------------+ + | Verifier | parties.verifier, or | the Verdict | the | + | | the kid of a Verdict | | Delivery | + | | | | and, on a | + | | | | Challenge, | + | | | | the | + | | | | Challenge | + +-------------+----------------------+-----------------+------------+ + | Challenger | the kid of a | the Challenge | Contract | + | | Challenge | | Status | + +-------------+----------------------+-----------------+------------+ + + Table 1: Roles, by what each signs and receives + + One identifier may play more than one role across contracts, and + Section 9.1 says which combinations within one contract a Facilitator + refuses. + +4. Protocol Overview + + A contract passes through four phases. Propose establishes the + record. Agree co-signs it and a Facilitator accepts it. Complete + produces a Delivery and a Verdict on it. Record produces an Outcome + Record. Every step after Agree is an event the Facilitator records + on its own clock, in one order, and the sequence of those events is + the contract's trace. The trace is the protocol's central object: + the state machine is defined over it, every response a Facilitator + + + +Sharma Expires 20 March 2027 [Page 16] + +Internet-Draft PACT September 2026 + + + gives carries the prefix recorded so far, and the Outcome Record + carries the whole of it. + + Buyer Facilitator Seller Verifier + | | | | + |<==== contract negotiated and co-signed ==>| | + | | | | + |-- POST contract ->| | | + |<-- Status --------| [ accepted ] | | + | | [ funded ] | | + | | | | + | | [ Seller performs ] | + | |<-- POST Delivery --| | + | |-- Status --------->| [ delivered ] | + | | | | + | |---- Delivery, criteria_hash ------->| + | |<--- POST Verdict -------------------| + | |---- Status ------------------------>| + | | [ verdict PASS ] [ window-opened ] + | | [ window-closed ] [ children-final ] + | | [ terminal FINAL ] | + | | | | + |<-- Outcome Record-|-- Outcome Record ->| | + + Figure 1: Message flow under the verdict-first flow, without a + Challenge + + Every accepted request is answered with a Contract Status + (Section 11), a Facilitator-signed object carrying the state and the + trace so far. Nothing in the figure moves value, and no arrow in it + is named for a movement of value. What a terms profile does at each + bracketed event is the profile's, and it is reported once, in the + Outcome Record, as a list the profile produced and the Facilitator + signed. + +4.1. States + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 17] + +Internet-Draft PACT September 2026 + + + ACCEPTED -funded-> FUNDED -delivered-> DELIVERED + | | | + | deadline- | deadline- | window-opened + | passed | passed v + | | WINDOW_OPEN <------+ + | | | | | + | | challenge | | window | verdict PASS + | | v | closed | on it, or + | | DISPUTED----|--------+ dispute- + | | | | lapsed + | | verdict | | + | | FAIL | | + v v v v + +--------------------------------------------------+ + | AWAITING_CHILDREN | + +--------------------------------------------------+ + | children-final, then terminal + v + FINAL SETTLED ABANDONED + + Figure 2: Contract states + + The figure omits three arrows that the table carries: a FAIL Verdict + recorded in DELIVERED or in WINDOW_OPEN also leads to + AWAITING_CHILDREN; under the no-window flow DELIVERED leads there + directly; and a Verdict that is late (verdict-lapsed) opens the + window without one. FINAL, SETTLED and ABANDONED are terminal and + each produces exactly one Outcome Record. The -01 revision named one + of these states for a movement of value; no state here is. + + The state named PROPOSED in earlier revisions is gone. Between the + parties' signatures and the Facilitator's acceptance a contract + exists only on the parties' side, so no Facilitator could observe + that state and the reference implementation never reported it. + +4.2. Events + + A trace entry is a JSON object with event (one of the names below), + at (the Facilitator's clock when it was recorded), object where the + entry records a posted record (that record's digest, Section 2), and + the members listed for the event. A Facilitator MUST record the + entries of one contract in the order it recorded them and MUST NOT + reorder, remove or alter an entry once a Status carrying it has been + issued (Section 17.1 says what that rule does and does not prove). + + + + + + + +Sharma Expires 20 March 2027 [Page 18] + +Internet-Draft PACT September 2026 + + + +==========+====================+===================================+ + |Event | Recorded in; then | Members and condition | + +==========+====================+===================================+ + |accepted | none; then | object is vtc_hash. The | + | | ACCEPTED | contract passed Section 13.1. | + +----------+--------------------+-----------------------------------+ + |funded | ACCEPTED; then | ref (string, optional, in the | + | | FUNDED | form the settlement binding | + | | | defines). Recorded when every | + | | | account the named terms profile | + | | | requires shows finality on the | + | | | settlement binding named in | + | | | price.settlement; how a | + | | | Facilitator observes that is the | + | | | binding's to say, and this is | + | | | the only sentence in this | + | | | document that mentions an | + | | | account. | + +----------+--------------------+-----------------------------------+ + |deadline- | ACCEPTED or | task.deadline has passed with no | + |passed | FUNDED; then | delivered entry. | + | | AWAITING_CHILDREN | | + +----------+--------------------+-----------------------------------+ + |delivered | FUNDED; then | object is the Delivery's digest. | + | | DELIVERED | The Delivery passed Section 6. | + +----------+--------------------+-----------------------------------+ + |window- | DELIVERED; then | Under delivery-first, | + |opened | WINDOW_OPEN | immediately after delivered; | + | | | under verdict-first, immediately | + | | | after a PASS verdict or after | + | | | verdict-lapsed. closes_at | + | | | (timestamp, required) is at plus | + | | | challenge.window_seconds. | + +----------+--------------------+-----------------------------------+ + |verdict | DELIVERED, | object is the Verdict's digest; | + | | WINDOW_OPEN or | outcome (PASS or FAIL); answers | + | | DISPUTED; then see | (digest of the Challenge, when | + | | the condition | the Verdict carries | + | | | challenge_hash); supersedes | + | | | (digest of the Verdict it | + | | | replaces, when one stood). | + | | | Then: FAIL leads to | + | | | AWAITING_CHILDREN; PASS in | + | | | DELIVERED leads to window- | + | | | opened; PASS in WINDOW_OPEN | + | | | changes nothing; PASS in | + | | | DISPUTED leads to WINDOW_OPEN | + | | | once no Challenge is pending. | + + + +Sharma Expires 20 March 2027 [Page 19] + +Internet-Draft PACT September 2026 + + + +----------+--------------------+-----------------------------------+ + |verdict- | DELIVERED; then | Under verdict-first, | + |lapsed | WINDOW_OPEN | verification.max_verdict_seconds | + | | | have passed since delivered with | + | | | no verdict. window-opened | + | | | follows. | + +----------+--------------------+-----------------------------------+ + |challenge | WINDOW_OPEN or | object is the Challenge's | + | | DISPUTED; then | digest; costs copied from the | + | | DISPUTED | Challenge when present. The | + | | | Challenge passed Section 7.3 | + | | | before closes_at. | + +----------+--------------------+-----------------------------------+ + |dispute- | DISPUTED; then | object is the Challenge's | + |lapsed | WINDOW_OPEN | digest. | + | | | challenge.max_dispute_seconds | + | | | have passed since that challenge | + | | | entry with no Verdict answering | + | | | it. Leads to WINDOW_OPEN once | + | | | no Challenge is pending; the | + | | | earlier Verdict, if any, stands. | + +----------+--------------------+-----------------------------------+ + |window- | WINDOW_OPEN; then | closes_at has passed and no | + |closed | AWAITING_CHILDREN | Challenge is pending. The | + | | | window is never extended: a | + | | | dispute that outlasts it delays | + | | | this entry and does not move | + | | | closes_at. | + +----------+--------------------+-----------------------------------+ + |child- | any non-terminal; | object is the child contract's | + |registered| unchanged | digest; facilitator (URI). | + | | | Section 10.2. | + +----------+--------------------+-----------------------------------+ + |child- | any non-terminal; | object is the child's Outcome | + |final | unchanged | Record digest; child (the child | + | | | contract's digest). | + +----------+--------------------+-----------------------------------+ + |child- | any non-terminal; | child (the child contract's | + |unresolved| unchanged | digest). The child's latest | + | | | finality instant (Section 10.3) | + | | | has passed and no Outcome Record | + | | | for it is held. | + +----------+--------------------+-----------------------------------+ + |children- | AWAITING_CHILDREN; | Every registered child has a | + |final | then terminal | child-final or child-unresolved | + | | follows | entry. A contract with no | + | | | registered children records this | + | | | entry on entering | + + + +Sharma Expires 20 March 2027 [Page 20] + +Internet-Draft PACT September 2026 + + + | | | AWAITING_CHILDREN. | + +----------+--------------------+-----------------------------------+ + |terminal | AWAITING_CHILDREN; | state (the terminal state) and | + | | then FINAL, | challenge_upheld (boolean). | + | | SETTLED or | ABANDONED where deadline-passed | + | | ABANDONED | was recorded; SETTLED where the | + | | | standing Verdict is FAIL, with | + | | | challenge_upheld true when that | + | | | Verdict answers a Challenge; | + | | | FINAL otherwise. | + +----------+--------------------+-----------------------------------+ + + Table 2: Events: the state each is recorded in, the state that + follows, and what the entry carries + + The standing Verdict is the last verdict entry in the trace that no + later entry supersedes. A Challenge is pending from its challenge + entry until a verdict entry answers it or a dispute-lapsed entry + names it. + + Every instant in the table is read from the Facilitator's clock, and + an entry conditioned on an instant having passed is recorded at the + first opportunity after it, which need not be that instant. Two + Facilitators given the same posted records with the same clock + readings record the same trace; that is the determinism the + experiment in Section 1.4 tests, and the reason every condition above + is stated over the trace and the clock and nothing else. + +5. The Verifiable Task Contract + + A VTC is a JSON object, media type application/ + vnd.pact.contract+json, with the members in Section 3.2. A VTC is + valid only if every required member is present, the parties are + distinct, and both the Buyer and the Seller have contributed exactly + one signature that verifies against a key bound to its identifier + (Section 14.2). The Facilitator and any Verifier do not sign the + VTC; their assent is expressed by acting on it, and a Facilitator + that will not act on a contract refuses it at Section 13.1. + + The settlement identifier, the network and the asset are all carried + inside price so that a co-signed VTC is bound to one venue. The -00 + revision omitted them, which made a signed contract replayable + against any facilitator, chain or token contract. + + + + + + + + +Sharma Expires 20 March 2027 [Page 21] + +Internet-Draft PACT September 2026 + + + { + "pact": "0.2", + "type": "VerifiableTaskContract", + "id": "vtc_7f3a91", + "parties": { + "buyer": "did:web:acme.example", + "seller": "did:web:dataforge.example", + "facilitator": "did:web:settle.example", + "verifier": "did:web:audit.example" + }, + "task": { + "spec_hash": "sha256:", + "deadline": "2026-11-14T00:00:00Z" + }, + "price": { + "amount": "180.00", + "currency": "USDC", + "settlement": "https://settle.example/bindings/ledger-1", + "network": "eip155:8453" + }, + "verification": { + "tier": "T0-reexec", + "profile": "acceptance", + "criteria_hash": "sha256:", + "max_verdict_seconds": 86400 + }, + "flow": "verdict-first", + "terms": { + "profile": + "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "profile_hash": "sha256:", + "parameters": { "...": "the profile's; not read here" } + }, + "challenge": { + "window_seconds": 3600, + "max_dispute_seconds": 86400 + }, + "signatures": [ { "protected": "...", "signature": "..." }, + { "protected": "...", "signature": "..." } ] + } + + Figure 3: A Verifiable Task Contract, signatures abbreviated + + Digests are elided here; the reference repository's values are in + Section 15. The parameters object is shown elided on purpose: + nothing in this document depends on what is in it. + + + + + +Sharma Expires 20 March 2027 [Page 22] + +Internet-Draft PACT September 2026 + + +5.1. Hash Commitments and Content Conveyance + + Every URI carried inside hash-committed content MUST be accompanied + by a sibling hash over the dereferenced bytes. The -00 revision + committed harness_uri as a string while leaving the bytes at that URI + uncommitted, which permitted a Buyer to substitute the acceptance + instrument after signature, run the substituted instrument, and + submit the failure as a valid fraud proof. The -01 revision stated + the rule and its own reference TaskSpec broke it for three of four + URIs; this revision's example carries all four sibling hashes, and + the validator checks each. + + Where the committed content is a bundle of files rather than a single + octet stream, the commitment MUST be computed as SHA-256(JCS(M)) + where M is an object mapping each file's path, relative to the bundle + root and expressed with "/" separators, to SHA-256 of its bytes, over + every file in the bundle. A manifest of per-file digests is + specified rather than an archive digest because archive formats carry + ordering, timestamp and permission metadata that is not stable across + producers. The same construction commits to a terms profile + (Section 5.3). + +5.2. The Task Specification + + The content committed by spec_hash is a TaskSpec: a JSON object with + the members in Section 3.3, canonicalized per [RFC8785] before + hashing. It is not transmitted over the endpoints of this document; + the parties exchange it before signing, and spec_uri may say where. + + The acceptance object MUST carry the members required for the + contract's tier: harness_uri and harness_hash for re-execution tiers, + enclave and model policy for attestation tiers, a proof statement + with its verifying key for proving tiers, or rubric_uri and + rubric_hash for judgment tiers. An empty acceptance object MUST be + rejected. The -00 revision's schema permitted one, which made every + fraud proof impossible. + + Thresholds MUST be stated so that they cannot be satisfied by + returning almost nothing. A threshold expressed only as a rate over + returned rows is satisfied by returning one correct row out of + millions; a completeness condition relative to the committed input is + therefore required wherever the deliverable is a transformation of + that input. + + + + + + + + +Sharma Expires 20 March 2027 [Page 23] + +Internet-Draft PACT September 2026 + + +5.3. Terms + + The terms member names the settlement terms both parties signed, by + reference. It carries a profile URI, a profile_hash, and a + parameters object. This document defines no obligation between + parties and takes no position on the legal effect of any object it + defines; what the named profile says the parties have agreed to, and + what any of it means between them, is the profile's and its authors' + to say. + + profile_hash is the manifest digest of Section 5.1 over the profile's + bundle. A bundle usable with this document contains at least three + files: the profile's prose, parameters.schema.json, a JSON Schema + [I-D.bhutton-json-schema] for the parameters object, and + vectors.json, whose form Section 12.1 gives. A digest over prose + alone would commit the parties to bytes and not to behaviour; the + schema and the vectors are what make two implementations of the + profile checkable against each other. + + A Facilitator MUST refuse a contract whose terms.profile and + terms.profile_hash do not match an entry in the terms_profiles array + of its own capability document (Section 8), so that no party signs + terms the Facilitator will not evaluate, and MUST refuse a contract + whose parameters do not validate against the named profile's + parameters.schema.json. It reads parameters for no other purpose. + The rule of Section 2 that an undefined member is rejected does not + apply inside parameters; the profile's schema governs there. + + A profile usable with this document defines, in its prose, a + schedule: a total, deterministic function from a contract and a trace + prefix (Section 4.2) to the list of entries the profile emits at the + last event of that prefix, in the form of Section 12.1. Total means + every event in Table 2 has a defined result, including the ones a + profile author would rather not think about: a lapsed dispute, an + unresolved child, a contract abandoned before it was funded. + Deterministic means the result depends on the contract, the trace and + nothing else, so that any party holding those can recompute it. The + prose also names the accounts the schedule uses and how each one's + opening amount is computed from the contract. This document does not + register profiles and defines none normatively; Appendix A carries + one for the experiment. + + Everything the -01 revision said in its Section 5.3, and everything + it said in its Section 7 about what is posted, released, returned or + forfeited and when, is now the content of a profile. The member that + carried those figures inside the contract is gone; the figures a + profile needs are in parameters, and the -01 figures in particular + are the parameters of the profile in Appendix A. + + + +Sharma Expires 20 March 2027 [Page 24] + +Internet-Draft PACT September 2026 + + +6. The Delivery Record + + The Delivery is the record a contract is judged against. It is a + JSON object, media type application/vnd.pact.delivery+json, with the + members in Section 3.4, signed once by the Seller. + + A Facilitator MUST refuse a Delivery, with the problem type named, + when: its vtc_hash does not match the contract (object-conflict); the + contract is not in FUNDED (wrong-state); its signature does not + verify against a key bound to parties.seller (signature-invalid, + unexpected-signer); its evidence member is absent or does not conform + to the verification profile named in the contract (evidence- + nonconformant); or input_hash is absent where the tier re-executes + (evidence-nonconformant). A refused Delivery is recorded in no + trace; the contract stays in FUNDED and a conformant Delivery may + follow before the deadline. The -01 revision treated a nonconformant + Delivery as a FAIL Verdict, which decided a question about value + inside a rule about shape; the consequence of a Seller reaching the + deadline with nothing conformant recorded is now the deadline-passed + event, and what that event costs anyone is the profile's. + + Conformance of evidence is a check on shape, not on substance: the + Facilitator confirms that the members the profile requires are + present and well formed, and nothing about whether the work is any + good. That is why the check stays on the right side of the line + drawn in Section 2.1. Where task.deadline passes with no delivered + entry, the Facilitator records deadline-passed (Section 4.2). No + window opens, because there is nothing to challenge. + + { + "pact": "0.2", + "type": "Delivery", + "vtc_id": "vtc_7f3a91", + "vtc_hash": "sha256:", + "work_hash": "sha256:9c1f...", + "work_uri": "https://cdn.dataforge.example/o/9c1f", + "input_hash": "sha256:41ab...", + "evidence": { + "profile": "acceptance", + "instrument_hash":"sha256:", + "results_hash": "sha256:7e02...", + "results_uri": "https://cdn.dataforge.example/o/7e02" + }, + "signature": { "protected": "...", "signature": "..." } + } + + Figure 4: A Delivery for a T0-reexec contract, acceptance profile + + + + +Sharma Expires 20 March 2027 [Page 25] + +Internet-Draft PACT September 2026 + + + The -01 revision said that a Buyer countersignature over the Delivery + constituted a receipt. The Delivery's signing member is a single + object, so no second signature could be carried, and the sentence is + withdrawn. A Buyer that wants a record of receipt has one: the + Status the Facilitator returns for the Delivery carries the delivered + entry and the Facilitator's signature over it. + +7. Verdicts, Challenges and the Window + +7.1. Flows + + The flow member selects one of three shapes for the state machine of + Section 4.1. A conformant Facilitator MUST implement verdict-first; + the others are OPTIONAL, and a Facilitator MUST refuse a contract + naming a flow it does not advertise (flow-unsupported). + + verdict-first: A Verdict is recorded before the window opens. The + window opens on a PASS Verdict or on verdict-lapsed; a FAIL + Verdict ends the contract without a window. + + delivery-first: The window opens at delivered. A Verdict MAY be + recorded inside the window without a Challenge; a FAIL ends the + contract, a PASS changes nothing. + + no-window: No window opens and no Verdict is accepted; delivered is + followed by the terminal path. + + The -01 revision had four release modes, named for when value moved. + Two of them, on-window and optimistic, produce the same trace and + differed only in which event a profile acts on, which is a profile + parameter and not a protocol matter. The mapping is in Appendix B. + + The window opens at the instant of the window-opened entry and closes + at that instant plus challenge.window_seconds, carried in the entry + as closes_at. A Facilitator MUST NOT accept a Challenge after + closes_at, MUST NOT extend the window for any reason, and MUST NOT + record window-closed while a Challenge is pending. + +7.2. Verdicts + + A Verdict is a signed statement that a Delivery was evaluated against + the committed instrument, and with what outcome. It is a JSON + object, media type application/vnd.pact.verdict+json, with the + members in Section 3.5, signed once. + + + + + + + +Sharma Expires 20 March 2027 [Page 26] + +Internet-Draft PACT September 2026 + + + { + "pact": "0.2", + "type": "Verdict", + "vtc_id": "vtc_7f3a91", + "delivery_hash": "sha256:", + "outcome": "PASS", + "profile": "acceptance", + "instrument_hash": "sha256:", + "results_hash": "sha256:7e02...", + "evaluated_at": "2026-11-10T09:14:22Z", + "signature": { "protected": "...", "signature": "..." } + } + + Figure 5: A Verdict + + The Verifier is the party identified by the kid of the Verdict's + signature. Where the contract names parties.verifier, a Facilitator + MUST refuse a Verdict signed by any other party; otherwise it MUST + refuse a Verdict whose signer does not satisfy Section 9.1 (verifier- + not-independent). It MUST refuse a Verdict for a contract with no + delivered entry (no-recorded-delivery); one whose delivery_hash does + not match that entry, or whose profile or instrument_hash does not + match the contract (verdict-nonconformant); one received in a state + the table in Section 4.2 does not list for it, or under the no-window + flow (wrong-state); and one carrying challenge_hash that names no + pending Challenge, or omitting it while the contract is DISPUTED + (verdict-nonconformant). A Verdict that answers a Challenge + supersedes the Verdict that stood before it, and both stay in the + trace. + + A Verdict commits to the instrument it ran and to the results it + produced. Without instrument_hash a Verifier could run something + other than the committed instrument and the contract would have no + way to tell; that is the substitution attack of Section 17.4, + arriving from the verification side. + + Under verdict-first a Verifier that never answers would leave a + contract in DELIVERED forever, and the -01 revision had no rule for + it. verification.max_verdict_seconds bounds the wait: when it passes + with no Verdict, the Facilitator records verdict-lapsed and opens the + window, so that the contract can still be challenged and can still + end. What a lapsed Verdict costs anyone is the profile's. + + + + + + + + + +Sharma Expires 20 March 2027 [Page 27] + +Internet-Draft PACT September 2026 + + +7.3. Challenges + + A Challenge is a JSON object, media type application/ + vnd.pact.challenge+json, with the members in Section 3.6, by which a + party submits a fraud proof inside the window. A Facilitator MUST + refuse a Challenge received when the contract is not in WINDOW_OPEN + or DISPUTED, or after closes_at (challenge-window-closed); one whose + delivery_hash does not match the delivered entry (object-conflict); + one whose proof does not conform to the verification profile (proof- + nonconformant); one whose signer it cannot resolve (signature- + invalid); and one signed by the contract's Seller (unexpected- + signer), since a performer's statement against its own Delivery is + not a fraud proof and the -01 revision left the case open. A + Facilitator MUST NOT refuse a Challenge on the ground that its signer + is the contract's Buyer. + + A Challenge that is accepted is evaluated by a party satisfying + Section 9.1, whose finding is a Verdict carrying challenge_hash; the + Challenger's own assertion is not a finding. The Challenger is the + party identified by the kid of the Challenge's signature. + + A Facilitator MAY require that a Challenge be accompanied by a + deposit in the amount its capability document advertises as + challenge_deposit. How a deposit is posted is the settlement + binding's, what becomes of it is the terms profile's, and this + document says nothing further about it. Section 17.14 discusses what + a deposit does and does not prevent. + + { + "pact": "0.2", + "type": "Challenge", + "vtc_id": "vtc_7f3a91", + "delivery_hash": "sha256:", + "proof": { + "profile": "acceptance", + "instrument_hash": "sha256:", + "results_hash": "sha256:a91e...", + "results_uri": "https://watch.example/o/a91e", + "failing_checks": ["schema_valid_rate", "row_count_min"] + }, + "costs": { "amount": "1.20", "currency": "USDC" }, + "signature": { "protected": "...", "signature": "..." } + } + + Figure 6: A Challenge under the acceptance profile + + + + + + +Sharma Expires 20 March 2027 [Page 28] + +Internet-Draft PACT September 2026 + + +7.4. Disputes and Lapses + + A contract with a pending Challenge is DISPUTED. It leaves that + state when a Verdict answers the Challenge, or when + challenge.max_dispute_seconds pass with none and the Facilitator + records dispute-lapsed. A lapsed Challenge changes no Verdict: the + Verdict that stood before it stands after it. A Facilitator MAY + accept further Challenges while DISPUTED, each of which is pending on + its own account, and MUST NOT record window-closed until none is + pending. + + Buyer Facilitator Verifier Challenger + | | | | + | |<-- POST Verdict | | + | | [ verdict PASS ] | + | | [ window-opened ] | + | |<------------- POST Challenge ---| + | |-- Status ---------------------->| + | | [ challenge ] | + | |-- Challenge + Delivery -------->| + | |<-- POST Verdict | | + | | [ verdict FAIL, answers, | + | | supersedes ] | + | | [ children-final ] | + | | [ terminal SETTLED, | + | | challenge_upheld true ] | + |<- Outcome ---| | + + Figure 7: The dispute path: a Challenge answered by a FAIL Verdict + + The figure carries no rank, no waterfall and no amount. The -01 + revision drew five numbered transfers on this diagram; every one of + them is now a line in a profile's schedule, keyed to the terminal + entry, and reported in terms_result. + +8. Facilitator Capability Discovery + + Before a Buyer and Seller can co-sign a VTC they must agree on a + Facilitator and know what it implements. This document registers one + well-known URI for that purpose, per [RFC8615]. + + This is deliberately narrower than agent discovery, which is the + subject of separate work and is not restated here. What is + discovered is one service's capabilities, not an agent's identity, + skills or endpoints. + + + + + + +Sharma Expires 20 March 2027 [Page 29] + +Internet-Draft PACT September 2026 + + + A Facilitator SHOULD publish a JSON document, media type application/ + vnd.pact.facilitator+json, with the members in Section 3.9, at the + path /.well-known/pact-facilitator of its origin. The document MUST + be served over HTTPS. It MUST be signed, and the signature MUST + verify against a key bound to the identifier in facilitator. An + unsigned capability document is not usable for contract formation, + because terms_profiles determines which terms a party can name and + expect to be evaluated. + + { + "pact": "0.2", + "type": "FacilitatorCapabilities", + "facilitator": "did:web:settle.example", + "settlement_bindings": [ + { "id": "https://settle.example/bindings/ledger-1", + "networks": ["eip155:8453"], + "assets": ["USDC"] } + ], + "flows": ["verdict-first", "delivery-first"], + "verification_profiles": ["acceptance", "bisection"], + "terms_profiles": [ + { "id": + "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "profile_hash": "sha256:" } + ], + "max_contract_value": { "amount": "50000.00", + "currency": "USDC" }, + "endpoints": { + "contract": "https://settle.example/pact/v2/contracts", + "delivery": "https://settle.example/pact/v2/deliveries", + "verdict": "https://settle.example/pact/v2/verdicts", + "challenge": "https://settle.example/pact/v2/challenges", + "outcome": "https://settle.example/pact/v2/outcomes" + }, + "signature": { "protected": "...", "signature": "..." } + } + + Figure 8: https://settle.example/.well-known/pact-facilitator + + A client MUST NOT infer any capability from the absence of a member. + A Facilitator that does not publish a capability document can still + be named in a VTC by prior arrangement; discovery is a convenience, + not a precondition. A Facilitator MUST NOT list a terms profile + whose vectors (Section 12.1) its own implementation does not + reproduce. + + + + + + +Sharma Expires 20 March 2027 [Page 30] + +Internet-Draft PACT September 2026 + + +9. Verification Profiles + + A contract names both a tier, which says what class of evidence is + produced, and a profile, which says what is actually done to check + it. Four tier labels are used in this document: T0-reexec, + deterministic re-execution; T1-tee, hardware attestation per + [RFC9334]; T2-zkml, a proof of inference; and T3-jury, staked + arbitration. Tiers are a vocabulary. Three profiles are defined + below by name; any other is identified by a URI under its definer's + control, and this document creates no registry for them. The + distinction matters because the tier name does not determine how much + checking a contract gets and the profile largely does. + + Consider one task, a bulk data transformation, under two profiles at + the same nominal tier. Re-executing the whole computation and + comparing outputs costs approximately what performing it cost. + Running a committed acceptance instrument against the delivered + artifact costs a small fraction of a percent. Those two differ by + more than two orders of magnitude in what checking costs relative to + the price. A terms profile may make that ratio matter; this document + requires only that a verification profile state an order-of-magnitude + estimate of its cost relative to the work, since a figure nobody can + estimate is a figure nobody can use. + + Implementations SHOULD select the cheapest profile that detects the + failures they actually care about, rather than the strongest-sounding + one. A committed acceptance instrument that is adequate is worth + more than a re-execution profile that nobody can afford to run. + + acceptance: Run the instrument committed by criteria_hash against + the Delivery. The fraud proof is a failing evaluation. + Deterministic by construction, since the instrument is fixed + before work begins. Cost: a small fraction of a percent of the + work for a data transformation. + + bisection: Interactive narrowing to a single disputed step, which is + then checked directly. Cost grows logarithmically in the size of + the computation rather than linearly. + + full-reexec: Re-execute and compare byte for byte. Sound only where + the computation is deterministic and the environment is pinned; + see Section 17.10. Cost: approximately the work. + + + + + + + + + +Sharma Expires 20 March 2027 [Page 31] + +Internet-Draft PACT September 2026 + + +9.1. Verifier Independence and Identifier Normalization + + Independence is a relation between the party that signs a Verdict and + the parties to the contract. It MUST be derived by the evaluator and + MUST NOT be satisfied by a field in which a record declares itself + independent. A Facilitator MUST refuse a Verdict whose signer is, + after normalization, the contract's Buyer, Seller or Facilitator, and + MUST refuse a contract whose parties.verifier is any of those three + (verifier-not-independent). The last case is the rule the -01 + revision stated as a prohibition on the Facilitator's conduct; it is + an identifier comparison and is stated as one. + + Party identifiers MUST be normalized before comparison, and the + normalization MUST fold toward identifying the same party: strip + leading and trailing whitespace; lower-case the scheme and, for + did:web and https identifiers, the host; remove any fragment (a "#" + and everything after it) and any trailing "/" or ".". The path of a + did:web identifier is case sensitive and MUST NOT be folded. + Percent-encoding MUST NOT be decoded, since an open-ended decoder is + its own attack surface. An identifier that does not parse after + normalization is not evaluable and MUST NOT be treated as outside the + parties. An independence claim reaches exactly as far as the + record's own commitments. + +10. Contract Trees + + An agent that accepts work may subcontract part of it. The + subcontract is an ordinary PACT contract whose Buyer is the parent's + Seller. What this section adds is the binding between the two, in + both directions and across Facilitators, so that a parent's Outcome + Record can commit to its children's and a reader of the parent's + record can find and check them. + + A (Buyer) + | + vtc_7f3a91 at did:web:settle.example + | + B (Seller) + | + +-----------+-----------+ + | | + vtc_c1a2 vtc_c2b7 at did:web:other.example + | | + C (Seller) D (Seller) + + Figure 9: A contract tree. B is Seller above and Buyer below. + + + + + +Sharma Expires 20 March 2027 [Page 32] + +Internet-Draft PACT September 2026 + + +10.1. Binding a Child to Its Parent + + A subcontract carries parent, a top-level member with the parent's + vtc_id, vtc_hash and facilitator. Because parent is inside the bytes + both parties sign, the child's Buyer signature is itself the + authorisation to attach that child to that parent. The -01 revision + carried this member inside the member it has since removed; it is + structural and is now where structure is. + + The child's Facilitator need not resolve the parent, and across + Facilitators it often cannot. It MUST record parent as signed, and + MUST allow the identifier in parent.facilitator to retrieve the + child's Status and Outcome Record (Section 17.12). The check that + the child's Buyer is the parent's Seller is made where the parent is: + at registration. + + The -01 revision required a Facilitator to reject a child whose + parent chain contained the child's own identifier and to enforce a + maximum depth. Neither rule survives, because neither is needed. A + child commits to its parent's digest, and the parent's digest exists + before the child is signed, so no contract can commit to a descendant + and a cycle cannot be formed; depth is bounded by whatever a + Facilitator is willing to register, and no Facilitator sees more than + one level. + +10.2. Registration and Children Final + + The parent's Facilitator learns of a child when the parent's Seller + registers it: a POST of the child's co-signed contract to the + parent's contract resource (Section 13). The registering party is + the child's Buyer, which is why it holds the child's contract and why + it is authorised: it is a party to both. + + A Facilitator MUST refuse a registration, with the problem type + named, when: the body is not a valid contract (Section 14.2); its + parent.vtc_hash is not the parent's digest or its parent.facilitator + is not this Facilitator (parent-unresolvable); its parties.buyer is + not the parent's parties.seller after normalization (parent- + unresolvable); its latest finality instant is not earlier than the + parent's (Section 10.3, finality-ordering-violation); or the parent + is terminal (wrong-state). An accepted registration is recorded as + child-registered. + + child.parties.buyer == parent.parties.seller + child.parent.vtc_hash == digest(parent) + + + + + + +Sharma Expires 20 March 2027 [Page 33] + +Internet-Draft PACT September 2026 + + + Without the first check any party may name any contract as its + parent. The attack is cheap and asymmetric: name a competitor's + contract as parent, subcontract a trivial task to yourself, fail it, + and put a failed child under the competitor's record. The -00 + revision carried the parent as a bare string with no hash and no + check, so the attack cost one signature. + + A child becomes final for its parent when the parent's Facilitator + holds the child's Outcome Record. It may obtain that record itself, + by retrieving it from the child's Facilitator, or receive it from the + parent's Seller by a POST to the same resource (Section 13). Either + way the Facilitator MUST verify the record's Facilitator signature + against a key bound to the identifier the registration recorded, and + MUST verify that its vtc_hash is the registered child's digest, + before recording child-final. Where the child's latest finality + instant passes with no record held, the Facilitator records child- + unresolved. children-final follows when every registered child has + one entry or the other, and the parent's terminal entry follows that. + + A child that is never registered does not exist for the parent. + Nothing in this document compels a parent's Seller to register a + child, and Section 17.6 says what that means. + +10.3. Finality Is Bottom-Up + + A parent's Outcome Record MUST carry children_merkle_root over the + Outcome Records of its registered children (Section 12.2), so a + parent cannot be recorded until its children have been, and the + parent waits in AWAITING_CHILDREN until they are. For that wait to + be bounded, every child must be able to reach a terminal state, or be + declared unresolved, before its parent needs it. + + The latest finality instant L of a contract is computed from its own + members and nothing else: + + verdict-first: L = task.deadline + + verification.max_verdict_seconds + + challenge.window_seconds + + challenge.max_dispute_seconds + delivery-first: L = task.deadline + + challenge.window_seconds + + challenge.max_dispute_seconds + no-window: L = task.deadline + + Every wait in Table 2 is bounded by one of those members, and a + Challenge can only be received before closes_at, so no sequence of + events carries a contract past its L except waiting for its own + children. A Facilitator MUST refuse to register a child unless + + + +Sharma Expires 20 March 2027 [Page 34] + +Internet-Draft PACT September 2026 + + + L(child) is earlier than L(parent), and MUST record child-unresolved + for a registered child no later than the first opportunity after + L(child) if it holds no Outcome Record for it by then. + + The -01 revision compared the child's latest finality with the + parent's earliest window close, and bounded neither: under its + default mode the first Verdict could take forever, so the inequality + guaranteed nothing. max_verdict_seconds is what makes L finite, and + the waiting state is what makes the rule honest about the case where + a child is late anyway. + + parent |== work ==|= verdict =|= window =|= dispute =| + ^ L(parent) + child |== work ==|= vrd =|= win =|= dsp =| + ^ L(child) + + a child is registered only where L(child) < L(parent) + + Figure 10: Bottom-up finality + + What a child's outcome means for its parent is not stated here. No + entry in a parent's schedule depends on any child's outcome unless + the named terms profile says so; what this document guarantees is + that the parent's Outcome Record commits to whichever child records + exist when it is issued and names, in its trace, every child that + does not. + +11. The Contract Status + + A Contract Status is a JSON object, media type application/ + vnd.pact.status+json, with the members in Section 3.7, signed once by + the Facilitator. It is the body of every successful response to a + POST in Section 13 and of a GET on a contract resource. It carries + the contract's state and the trace recorded so far. + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 35] + +Internet-Draft PACT September 2026 + + + { + "pact": "0.2", + "type": "ContractStatus", + "vtc_id": "vtc_7f3a91", + "vtc_hash": "sha256:", + "state": "WINDOW_OPEN", + "trace": [ + { "event": "accepted", "at": "2026-11-01T10:00:00Z", + "object": "sha256:" }, + { "event": "funded", "at": "2026-11-01T10:00:00Z" }, + { "event": "delivered", "at": "2026-11-10T08:30:12Z", + "object": "sha256:" }, + { "event": "verdict", "at": "2026-11-10T09:14:30Z", + "object": "sha256:", "outcome": "PASS" }, + { "event": "window-opened", "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" } + ], + "issued_at": "2026-11-10T09:14:30Z", + "signature": { "protected": "...", "signature": "..." } + } + + Figure 11: A Contract Status after the Verdict of Figure 1 + + Two rules make a Status worth keeping. A Facilitator MUST issue a + Status for every request it accepts, carrying the entry that request + caused, so that the requester holds a signed receipt of what was + recorded and when. And the trace in every Status a Facilitator + issues for a contract MUST be a prefix of the trace in every later + one; two Statuses for one contract that violate that are evidence of + equivocation, and Section 17.1 says what a holder can do with it. + The Outcome Record's trace is the last such sequence. + + The -01 revision returned the posted object with a state member added + to it, which no schema admitted and no signature covered. The Status + replaces that: the posted object is not echoed, and everything in the + response is inside the Facilitator's signature. + +12. Outcome Records + + An Outcome Record records what a contract did. It is a JSON object, + media type application/vnd.pact.outcome+json, with the members in + Section 3.8. It is the input to any reputation system built on PACT, + though this document defines no such system and takes no position on + how the records should be weighed. + + A Facilitator MUST issue exactly one Outcome Record for every + contract that reaches a terminal state, including SETTLED and + ABANDONED, MUST sign it, and MUST NOT require the signature of any + + + +Sharma Expires 20 March 2027 [Page 36] + +Internet-Draft PACT September 2026 + + + other party on it. The -00 revision's record needed the signature of + the party it recorded against, which made a reputation layer built on + it structurally incapable of recording a loss. The Facilitator + signature is what makes the record evidence: without it the record is + a claim by interested parties about themselves, and with it a + fabricated history requires a Facilitator's key rather than two + identities. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 37] + +Internet-Draft PACT September 2026 + + + { + "pact": "0.2", + "type": "OutcomeRecord", + "vtc_id": "vtc_7f3a91", + "vtc_hash": "sha256:", + "parties": { + "buyer": "did:web:acme.example", + "seller": "did:web:dataforge.example", + "facilitator": "did:web:settle.example", + "verifier": "did:web:audit.example" + }, + "outcome": { "state": "SETTLED", "challenge_upheld": true }, + "work_hash": "sha256:9c1f...", + "trace": [ + { "event": "accepted", "at": "...", + "object": "sha256:" }, + { "event": "funded", "at": "..." }, + { "event": "delivered", "at": "...", + "object": "sha256:" }, + { "event": "verdict", "at": "...", + "object": "sha256:", "outcome": "PASS" }, + { "event": "window-opened", "at": "...", "closes_at": "..." }, + { "event": "challenge", "at": "...", + "object": "sha256:" }, + { "event": "verdict", "at": "...", + "object": "sha256:", "outcome": "FAIL", + "answers": "sha256:", + "supersedes": "sha256:" }, + { "event": "children-final", "at": "..." }, + { "event": "terminal", "at": "...", "state": "SETTLED", + "challenge_upheld": true } + ], + "terms_result": { + "profile": + "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "profile_hash": "sha256:", + "currency": "USDC", + "transfers": [ + { "event": 8, "from": "...", "to": "...", "amount": "...", + "code": "..." } + ] + }, + "signatures": [ { "protected": "...", "signature": "..." } ] + } + + Figure 12: An Outcome Record for a contract that reached SETTLED + on an upheld Challenge + + + + +Sharma Expires 20 March 2027 [Page 38] + +Internet-Draft PACT September 2026 + + + The record carries one signature, the Facilitator's. The Seller did + not consent to this record and its consent is not required. The + transfers entries are elided here because their content is the + profile's; Appendix A shows them filled in for its own profile. + +12.1. The Terms Result + + terms_result reports what the named profile's schedule produced over + the whole trace. It carries the profile identifier and hash copied + from the contract, the currency, and transfers: an array of entries, + in the order the schedule produced them, each with event (the zero- + based index of the trace entry at which the schedule emitted it), + from and to (account names as the profile defines them), amount, and + code (a string the profile defines, naming the schedule line that + produced the entry). + + This document defines the form of the list and two arithmetic facts + about it, and nothing about what any entry means. Over the accounts + and opening amounts the profile declares for the contract + (Section 5.3): no entry takes from an account more than that account + holds at that point in the list; and after the last entry every + account the profile marks internal holds zero. A Facilitator MUST + NOT sign an Outcome Record whose list breaks either fact, and MUST + NOT sign one whose list differs from what the profile's schedule + produces for the record's own trace. Any party holding the contract, + the trace and the profile's bundle can recompute the list; that is + the property the experiment in Section 1.4 depends on. + + vectors.json in a profile's bundle is an array of objects, each with + name, contract (a VTC, or the members of one the schedule reads), + trace (a complete trace), and transfers (the list the schedule + produces for it). A Facilitator MUST reproduce every vector of a + profile before listing that profile in its capability document + (Section 8), which is the only conformance requirement this document + places on a profile implementation. + +12.2. The Children Merkle Root + + Let D be the list of 32-byte SHA-256 digests of the Outcome Record of + each registered child for which the Facilitator holds one, each + computed over the record's canonical form including its signatures + member, sorted ascending as byte strings. children_merkle_root is + MTH(D) exactly as defined in [RFC9162] Section 2.1.1, with SHA-256 as + the hash: a leaf is SHA-256(0x00 || d), an interior node is SHA- + 256(0x01 || left || right), and for n greater than one the list is + split at k, the largest power of two smaller than n. The shape is + therefore fixed by n alone, and two implementations that agree on D + agree on the root. + + + +Sharma Expires 20 March 2027 [Page 39] + +Internet-Draft PACT September 2026 + + + The domain separation is not optional. Without distinct prefixes an + attacker can present an interior node as though it were a leaf, and + so claim an inclusion proof for a subtree that never existed. + + The member is present when at least one child is registered and + absent otherwise; it MUST NOT be present with an empty or zero value, + which would be indistinguishable from a tree whose children were + withheld. Where every registered child is unresolved D is empty and + the root is MTH of the empty list, SHA-256 of the empty string; the + child-unresolved entries in the trace say which records the root does + not cover. The -01 revision computed leaves over records with their + signatures removed, which let a record be re-signed without changing + the root. + +13. Protocol Endpoints + + This section specifies the operations a Facilitator exposes. Base + URIs are not fixed by this document; they are discovered from the + endpoints member of the capability document (Section 8), so a + Facilitator may mount them anywhere on its origin. + + Propose a contract: POST {contract}; body, a contract; 201 with a + Status. + + Retrieve a contract's status: GET {contract}/{id}; 200 with a + Status. + + Register a child: POST {contract}/{id}/children; body, the child's + contract; 201 with a Status. + + Supply a child's outcome: POST {contract}/{id}/children/{child_id}; + body, the child's Outcome Record; 200 with a Status. + + Submit a Delivery: POST {delivery}; body, a Delivery; 202 with a + Status. + + Record a Verdict: POST {verdict}; body, a Verdict; 201 with a + Status. + + Open a Challenge: POST {challenge}; body, a Challenge; 202 with a + Status. + + Retrieve an Outcome Record: GET {outcome}/{id}; 200 with the Outcome + Record. + + All requests and responses use the media types defined in Section 19. + All requests MUST be made over HTTPS, following the recommendations + of [RFC9325]. Status codes are as defined in [RFC9110]. A Delivery + + + +Sharma Expires 20 March 2027 [Page 40] + +Internet-Draft PACT September 2026 + + + and a Challenge are answered 202 (Accepted) rather than 201 because + acceptance of the bytes is not acceptance of the work; what follows + depends on a Verdict the Facilitator does not itself produce. + + A Facilitator authenticates the sender of a POST by the signature on + the body, and by nothing else in this document: it MUST reject a + Delivery not signed by the contract's Seller, a Verdict not signed by + a party admissible under Section 7.2, a Challenge whose signer it + cannot resolve, and a child registration or child outcome whose body + does not verify as Section 10.2 requires. A Facilitator MAY require + an HTTP-layer authentication in addition. Retrieval is discussed in + Section 17.12. + +13.1. Proposing a Contract + + The request body is a VTC carrying the signatures of both parties + required to sign it. A Facilitator MUST perform the checks in + Section 14, Section 5.3 and Section 9.1 before creating the resource, + MUST refuse a contract whose parties.facilitator is not itself or + whose price.settlement, network or asset it does not advertise + (facilitator-mismatch, settlement-unsupported), and MUST refuse + otherwise with the problem type that names the rule. + + POST /pact/v2/contracts HTTP/1.1 + Host: settle.example + Content-Type: application/vnd.pact.contract+json + + { "pact": "0.2", "type": "VerifiableTaskContract", + "id": "vtc_7f3a91", ... } + + HTTP/1.1 201 Created + Location: /pact/v2/contracts/vtc_7f3a91 + Content-Type: application/vnd.pact.status+json + + { "pact": "0.2", "type": "ContractStatus", + "vtc_id": "vtc_7f3a91", "state": "ACCEPTED", + "trace": [ { "event": "accepted", ... } ], ... } + +13.2. Idempotency + + Every object this protocol carries is committed by the digest of its + own canonical form, so no separate idempotency key is needed and none + is defined; the general mechanism of + [I-D.ietf-httpapi-idempotency-key-header] solves a problem this + protocol does not have. A Facilitator MUST treat a POST whose body + has a digest it has already accepted as a request for the existing + resource, and MUST respond 200 (OK) with the current Status rather + than creating a second resource or reporting a conflict. + + + +Sharma Expires 20 March 2027 [Page 41] + +Internet-Draft PACT September 2026 + + + Where a POST carries the same object id as an existing resource but a + different digest, the Facilitator MUST respond 409 (Conflict) + (object-conflict). Retrying a submission is therefore always safe, + and altering one never is. + +13.3. Error Responses + + A Facilitator MUST report failures using [RFC9457] problem details, + media type application/problem+json, with a type from Section 19.3 + for a rule in this document, or from the profile's own namespace for + a rule in a terms profile. A problem arising from a rule in this + document MUST carry section, the number of the section stating the + rule. A problem arising from a rule in a terms profile MUST carry + profile and profile_section instead, since section cannot name a rule + outside this document. Error responses name the rule that was + violated, because a conformance failure a caller cannot locate is a + failure of the specification. + + HTTP/1.1 422 Unprocessable Content + Content-Type: application/problem+json + + { + "type": "tag:laxsharma79@gmail.com,2026:pact:problem: + signatures-unordered", + "title": "Signature set not sorted", + "status": 422, + "detail": "the second entry's kid sorts before the first's + after normalization.", + "section": "14.1" + } + +13.4. Exchange + + + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 42] + +Internet-Draft PACT September 2026 + + + Buyer/Seller Facilitator Verifier + | | | + |-- POST {contract} --->| | + |<-- 201 Status --------| | + | | | + |-- POST {delivery} --->| | + |<-- 202 Status --------| | + | | | + | |-- GET work_uri ----->| + | |<-- POST {verdict} ---| + | |-- 201 Status ------->| + | | | + |-- GET {contract}/id ->| | + |<-- 200 Status --------| | + | | | + |-- GET {outcome}/id -->| | + |<-- 200 Outcome -------| | + + Figure 13: HTTP exchange for the flow in Figure 1 + +14. Conformance + + Every rule a PACT conformance checker enforces is stated in this + document as normative text. This section collects the rules that a + schema language cannot express, so that an implementation built from + this document alone passes a conformance suite built from it. A rule + that lives only in a test suite is not a requirement, and an + implementer who cannot find it in the specification will not + implement it. + +14.1. Signatures + + Every signature carried by a VTC, Delivery, Verdict, Challenge, + Status, Outcome Record or capability document is a JWS [RFC7515] in + the General JSON Serialization of Section 7.2.1 of that document, + with the payload detached as its Appendix F describes. The payload + is BASE64URL of the JCS-canonical bytes of the object with the + signing member removed, so the JWS Signing Input is + ASCII(BASE64URL(UTF8(protected)) || "." || BASE64URL(JCS(object))) + exactly as Section 5.1 of [RFC7515] defines it. The payload is never + transmitted; a verifier reconstructs it from the object it holds, and + verifies over the protected header exactly as transmitted, never over + a header it re-serialized. The following constraints apply. + + * The protected header MUST carry alg, kid and typ. + + + + + + +Sharma Expires 20 March 2027 [Page 43] + +Internet-Draft PACT September 2026 + + + * alg MUST be ES256 or ES384 [RFC7518], or EdDSA [RFC8037] with an + Ed25519 key; a verifier MAY also accept Ed448. A verifier MUST + reject any other value, and MUST reject none. Absent an allowlist + an attacker selects the algorithm, which permits both unsigned + acceptance and confusion of a public key for a symmetric secret. + + * kid MUST appear inside the protected header and MUST NOT be + carried as a sibling of it. A key identifier outside the signed + bytes is rewritable in transit, which allows an attacker who can + publish a key document to re-attribute a genuine signature to + itself. + + * typ MUST be the full media type of the object signed, including + the application/ prefix, so that a signature over one object type + cannot be replayed as a signature over another. Section 4.1.9 of + [RFC7515] recommends omitting the prefix; this document requires + the full form so that typ equals the registered media type + character for character. Explicit typing follows Section 3.11 of + [RFC8725]. + + * A signatures array MUST be sorted by the normalized kid of its + entries (Section 9.1), ties broken by the unnormalized kid, both + compared as sequences of Unicode code points; a verifier MUST + reject an unsorted array (signatures-unordered). Two clients that + each attach their own entry and exchange the object would + otherwise produce two arrays, and since the digest covers the + array, two digests for one agreement. + + * An ECDSA signature MUST have its s value in the low half of the + curve order, that is s at most n/2 for the order n of the curve + [SP800-186], and a verifier MUST reject one that does not. + [RFC7518] fixes the encoding and not which of the two valid s + values is accepted; accepting both lets anyone holding a valid + signature produce a second one over the same bytes without the + key, and a second signature is a second digest. EdDSA + verification per [RFC8032] already rejects a non-canonical S, so + the rule is stated for ECDSA only. + +14.1.1. Key Resolution + + A kid is a URI naming a public key. A verifier MUST resolve it as + follows, and MUST reject a signature whose kid it cannot resolve. + + * A did: identifier is a DID URL [DID-CORE]. The verifier resolves + the DID document by the method the identifier names and selects + the verification method its fragment identifies. Examples in this + document use did:web [DID-WEB]; no method is required or excluded. + + + + +Sharma Expires 20 March 2027 [Page 44] + +Internet-Draft PACT September 2026 + + + * An https: identifier dereferences, over TLS, to a JWK Set + [RFC7517]; the verifier selects the key whose kid member equals + the fragment. + + The part of a kid before its fragment MUST equal, after the + normalization in Section 9.1, the party identifier the signature is + attributed to. Verifying a signature establishes that the holder of + that key signed; that the key belongs to the party is a property of + the identity method, and this document does not add to it. An + identity system for agents defined elsewhere, such as + [I-D.ietf-wimse-aims], is used by naming its identifiers here and + resolving them by its rules. + +14.2. Rules Not Expressible in a Schema + + * parties.buyer and parties.seller MUST be distinct after the + normalization in Section 9.1 (parties-not-distinct). + + * A contract MUST carry exactly one verifying signature whose kid + covers parties.buyer, exactly one whose kid covers parties.seller, + and no other (signature-missing, unexpected-signer). A count of + signatures is not sufficient: two signatures covering one + identifier MUST be rejected. + + * challenge.window_seconds MUST be greater than zero, and + task.deadline MUST be later than the instant of acceptance + (deadline-invalid). + + * Every URI member inside hash-committed content MUST have a sibling + hash member, and a validator MUST reject content carrying + harness_uri, rubric_uri, schema_uri or sample_uri without its + hash. + + * An acceptance object MUST carry the members required for the + contract's tier. An empty acceptance object MUST be rejected. + + * terms.profile and terms.profile_hash MUST match an entry the + Facilitator advertises, and terms.parameters MUST validate against + that profile's schema (terms-unsupported, terms-parameters- + invalid). + + * Every amount MUST have the form in Section 2 (amount-invalid), and + every object MUST validate against the schema published for its + media type (schema-invalid). + + + + + + + +Sharma Expires 20 March 2027 [Page 45] + +Internet-Draft PACT September 2026 + + +14.3. Test Vectors + + Each rule above has an accepting and a rejecting form. A conformance + suite built from this section alone, with no reference to any + implementation, should reach the same verdicts. Rejecting vectors + name the rule they violate. + + +======+=================================================+==========+ + | ID | Mutation from a valid object | Expect | + +======+=================================================+==========+ + | V-01 | unmodified valid VTC | accept | + +------+-------------------------------------------------+----------+ + | V-02 | alg set to none | reject | + +------+-------------------------------------------------+----------+ + | V-03 | alg set to HS256 | reject | + +------+-------------------------------------------------+----------+ + | V-04 | kid moved outside the protected header | reject | + +------+-------------------------------------------------+----------+ + | V-05 | typ of a Delivery on a VTC signature | reject | + +------+-------------------------------------------------+----------+ + | V-06 | buyer and seller set to the same identifier | reject | + +------+-------------------------------------------------+----------+ + | V-07 | buyer and seller differing only by trailing | reject | + | | "/" | | + +------+-------------------------------------------------+----------+ + | V-08 | two signatures, both from the buyer | reject | + +------+-------------------------------------------------+----------+ + | V-09 | window_seconds of 0 | reject | + +------+-------------------------------------------------+----------+ + | V-10 | acceptance as an empty object | reject | + +------+-------------------------------------------------+----------+ + | V-11 | harness_uri with harness_hash removed | reject | + +------+-------------------------------------------------+----------+ + | V-12 | terms.profile_hash not advertised by the | reject | + | | Facilitator | | + +------+-------------------------------------------------+----------+ + | V-13 | terms.parameters failing the profile's | reject | + | | schema | | + +------+-------------------------------------------------+----------+ + | V-14 | Delivery with evidence absent | reject, | + | | | no entry | + +------+-------------------------------------------------+----------+ + | V-15 | child whose buyer is not the parent's | reject | + | | seller | | + +------+-------------------------------------------------+----------+ + | V-16 | child with L(child) not earlier than | reject | + | | L(parent) | | + +------+-------------------------------------------------+----------+ + + + +Sharma Expires 20 March 2027 [Page 46] + +Internet-Draft PACT September 2026 + + + | V-17 | Verdict signed by the seller | reject | + +------+-------------------------------------------------+----------+ + | V-18 | object keys ordered by code point, with a | digest | + | | supplementary-plane key | mismatch | + +------+-------------------------------------------------+----------+ + | V-19 | buyer and seller differing only in the case | accept | + | | of a did:web path | | + +------+-------------------------------------------------+----------+ + | V-20 | object carrying a member this document does | reject | + | | not define for it | | + +------+-------------------------------------------------+----------+ + | V-21 | signatures not sorted by normalized kid | reject | + +------+-------------------------------------------------+----------+ + | V-22 | ECDSA signature with s above n/2 | reject | + +------+-------------------------------------------------+----------+ + | V-23 | Verdict with delivery_hash computed over | reject | + | | the Delivery without its signature | | + +------+-------------------------------------------------+----------+ + | V-24 | Outcome Record whose transfers overdraw an | reject | + | | account of the profile | | + +------+-------------------------------------------------+----------+ + + Table 3: Conformance vectors + + V-07 and V-18 are the two most often got wrong. V-07 fails wherever + party comparison is a string equality on unnormalized identifiers. + V-18 fails wherever canonicalization sorts keys by Unicode code + point, which agrees with the required UTF-16 order for every ASCII + key and so passes every vector an implementer would think to write. + V-19 is the opposite mistake, folding more than Section 9.1 allows, + and the -01 reference validator made it. + +15. Worked Example + + The tables and digests below are the reference repository's, at the + tag named in Section 16. The object figures in earlier sections use + short illustrative identifiers for page width; the repository + examples carry the full ones, and the digests here are computed over + those. The figures that the -01 revision printed here about a bond + and a required detection rate are now the profile's, and Appendix A + carries them. + + A buyer commissions a data transformation at a price of 180.00 USDC + under the verdict-first flow, the acceptance verification profile, + and the terms profile of Appendix A with the parameters shown there. + The digests carried by the reference TaskSpec, contract and profile + are: + + + + +Sharma Expires 20 March 2027 [Page 47] + +Internet-Draft PACT September 2026 + + + spec_hash sha256: + criteria_hash sha256: + profile_hash sha256: + vtc_hash sha256: + delivery_hash sha256: + + criteria_hash is the manifest digest of Section 5.1 over the + acceptance instrument bundle, and the same value appears as + acceptance.harness_hash inside the TaskSpec, so the instrument is + committed both by the contract and from within the specification it + belongs to. profile_hash is the same construction over the profile + bundle. vtc_hash is the digest of the signed contract, and + delivery_hash of the signed Delivery, both per Section 2. + + Every value above changed from the -01 revision, for four reasons + that are each recorded so that a reader comparing the two documents + can account for the difference: spec_hash because the TaskSpec now + carries the sibling hashes Section 5.1 always required; vtc_hash + because the contract's members changed (Appendix B) and because + spec_hash did; delivery_hash because it now covers the Delivery's + signature; and profile_hash because it did not exist. + + The trace the reference implementation records for this contract on + the path of Figure 1, and on the dispute path of Figure 7, together + with the transfer lists the profile produces for each, are the + vectors in the profile's bundle, and Appendix A prints them. + +16. Implementation Status + + This section records the status of known implementations of this + document per [RFC7942], and is to be removed before publication as an + RFC. + + One implementation is known to the author, and the author wrote it: + https://github.com/pact-spec/spec, under the Revised BSD licence. At + tag v0.2.0 it comprises the object schemas, the examples whose + digests Section 15 prints, a conformance validator that runs + checks including every vector of Section 14.3, a Facilitator serving + the endpoints of Section 13 with the profile of Appendix A, and + clients for the other roles. Its previous tag, v0.1.0, implemented + the -01 revision and is the source of the measurements the author has + published about it. No second implementation exists, so nothing in + Section 1.4 has been tested, and this document claims no + interoperability. + + + + + + + +Sharma Expires 20 March 2027 [Page 48] + +Internet-Draft PACT September 2026 + + +17. Security Considerations + + Most of what follows was found by adversarial review of earlier + revisions rather than anticipated when they were written. Each + subsection states the attack, why it worked, and the requirement in + this document that closes it. Where a threat is only mitigated + rather than closed, that is said. The table first: for each party, + what the protocol enforces against it, what it records about it, and + who can check the record without trusting the Facilitator. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 49] + +Internet-Draft PACT September 2026 + + + +=============+=====================+==============+===============+ + | Party | Enforced against it | Recorded | Checkable by | + | | | about it | | + +=============+=====================+==============+===============+ + | Buyer | cannot alter the | its | anyone | + | | task, instrument or | signature on | holding the | + | | terms after | the | contract | + | | signing; cannot | contract; | | + | | attach a child to a | any | | + | | contract it is not | Challenge it | | + | | party to | signs | | + +-------------+---------------------+--------------+---------------+ + | Seller | cannot deliver | its | anyone | + | | against a | signature on | holding the | + | | substituted | the contract | contract and | + | | instrument or | and the | the Delivery | + | | input; cannot judge | Delivery; | | + | | its own Delivery; | the Verdicts | | + | | cannot re-sign a | and | | + | | record without | Challenges | | + | | changing every | on its | | + | | digest over it | Delivery | | + +-------------+---------------------+--------------+---------------+ + | Verifier | cannot be a party | its | anyone | + | | to the contract; | Verdicts, | holding the | + | | must commit to the | superseded | Delivery and | + | | instrument it ran | ones | the | + | | and its results | included | instrument | + +-------------+---------------------+--------------+---------------+ + | Facilitator | nothing | what it | any holder of | + | | | chose to | two of its | + | | | sign, in the | Statuses, for | + | | | order it | equivocation; | + | | | chose, on a | nobody, for | + | | | clock that | omission or | + | | | is its own | for time, | + | | | | without a | + | | | | witness | + | | | | outside this | + | | | | document | + +-------------+---------------------+--------------+---------------+ + + Table 4: What the protocol enforces, records and lets others + check, by party + + + + + + + +Sharma Expires 20 March 2027 [Page 50] + +Internet-Draft PACT September 2026 + + +17.1. Trust in the Facilitator + + The Facilitator row is the honest one. This protocol enforces + nothing against a Facilitator; it makes some kinds of misbehaviour + attributable and says plainly which ones it does not. + + Equivocation, issuing two inconsistent histories for one contract, is + attributable: every Status is signed, every Status's trace is a + prefix of every later one, and two Statuses that break that rule are + proof, checkable by anyone holding both, that the Facilitator signed + contradictory records. A Facilitator that wants to make its records + publicly append-only can register its Outcome Records with a SCITT + transparency service [RFC9943] and hand the receipt [RFC9942] to the + parties; this document does not require it and defines no log of its + own. + + Omission is not attributable. A Facilitator that declines to record + a Delivery, or records it late, produces no signed evidence of having + declined, and a Status it does not issue proves nothing. A client + SHOULD retain every Status it receives, and a party that submitted a + record and holds no Status for it has a claim it can make only + outside this protocol. Making omission attributable needs a witness + the Facilitator does not control, such as a monitor with a gossip + path of the kind [RFC9162] assumes, and this document specifies none. + + Time is the Facilitator's. Every instant in a trace is read from its + clock, and nothing in this document lets a party prove that a + recorded instant is wrong. This document therefore states the + assumption rather than hiding it: the Facilitator is a trusted + timekeeper, and a deployment that cannot accept that should look to + an external timestamping service, which this document does not + specify and does not preclude. + + Whatever a Facilitator does with anything of value under a terms + profile is the profile's subject and is not addressed here. + +17.2. Verifier Capture + + A verification tier states how strongly work is checked. It does not + state who checked it, and those fail separately. A re-execution + transcript produced by the Seller and the same transcript produced by + an independent Challenger are the same method and different evidence. + Where a proof is generated and verified entirely inside one party, + the tier is satisfied and the contract is unprotected. Section 9.1 + requires that independence be derived by the evaluator from the + parties named in the contract, and forbids satisfying it with a self- + asserted field. + + + + +Sharma Expires 20 March 2027 [Page 51] + +Internet-Draft PACT September 2026 + + +17.3. Algorithm, Key and Encoding Confusion + + Absent an algorithm allowlist an attacker chooses the algorithm. The + two consequences are alg of none, which makes every signature check + vacuous, and presenting an ECDSA public key as an HMAC secret, which + lets anyone holding the public key forge. Section 14.1 fixes the + permitted set. + + A kid carried as a sibling of the protected header rather than inside + it is outside the signed bytes and is rewritable in transit. An + attacker who can publish a key document can then re-attribute a + victim's genuine signature to an identifier it controls, without + breaking any cryptography. Section 14.1 requires kid inside the + protected header. + + Because every digest in this document covers a signature set, a + second valid encoding of one signature is a second digest for one + record. ECDSA has two valid s values per signature and JWS does not + choose between them; the low-S rule in Section 14.1 does. The order + of a signature set is a second source of the same problem, and the + sorting rule closes it. + +17.4. Substitution of Committed Content + + The -00 revision committed harness_uri as a string. The bytes at + that URI were covered by nothing. A Buyer could therefore sign a + contract, replace the acceptance instrument afterwards, run the + replacement, and submit its failure as a textbook-valid fraud proof. + Cost of the attack: one file overwrite. The mirror attack works + against a Seller that hosts the input sample. Section 5.1 requires a + sibling hash over the dereferenced bytes for every URI inside + committed content, and Section 7.2 requires a Verdict to commit to + the instrument it actually ran, which closes the same attack from the + verification side. + +17.5. Fetching Committed Content + + A work_uri, results_uri or any other URI in a record is supplied by a + counterparty and points wherever that counterparty chose. An + implementation that fetches it MUST fetch over HTTPS only, MUST NOT + follow a redirect to a scheme other than HTTPS, MUST refuse to + connect to a private, loopback or link-local address (the ranges of + [RFC1918], [RFC4193] and their loopback and link-local counterparts), + and MUST verify the sibling hash over the full received bytes before + any byte is used for anything. A fetcher that acts on partial or + unverified content has handed its counterparty a way to make it + execute, store or judge something that was never committed to. + + + + +Sharma Expires 20 March 2027 [Page 52] + +Internet-Draft PACT September 2026 + + +17.6. Children: Attachment and Omission + + Naming a parent contract cost one signature in the -00 revision and + was checked against nothing. Section 10.2 requires the child's Buyer + to be the parent's Seller, checked by the Facilitator that holds the + parent against the parent's own bytes. + + The converse gap is stated rather than closed: a parent's Seller that + never registers a failing child keeps it out of the parent's record, + since this document compels no registration. A profile that wants + children visible must make registration worth the Seller's while, or + a Buyer that wants them visible must ask for the child's Status + directly, which this document does not require the child's + Facilitator to give it. + +17.7. Buying Silence from a Challenger + + Wherever what a discoverer gains by reporting is less than what a + performer loses by being reported, there is a private payment that + leaves both better off than reporting, and silence dominates whatever + reward a profile designed. This document cannot close that, because + every figure involved is the profile's. What it does is record every + Challenge, in order, whoever signed it, so that a profile can act on + each independently, and it forbids a Facilitator from refusing a + Challenge because the Buyer signed it (Section 7.3), so that the + party with the most to recover is always admissible. + +17.8. Non-Delivery + + Under the -00 revision a contract in which nothing was ever delivered + had no path to an end: the deadline carried no stated consequence and + no window opened because there was nothing to challenge. Section 4.2 + makes the deadline an event and ABANDONED a terminal state that every + contract can reach. What reaching it costs anyone is the profile's, + and a profile that makes delivering nothing cheaper than delivering + something wrong has recreated the -00 incentive. + +17.9. Cross-Venue Replay + + A VTC that does not name its Facilitator, network and asset is a + signed instrument replayable against any of them; Section 5 requires + all three inside the signed content. A digest computed over a + contract excluding its signatures proves what was written and not who + agreed to it, so entries can be appended or stripped without + invalidating the commitment; Section 2 defines every digest over the + signature set. A Delivery, Verdict or Challenge replayed against a + different contract fails because each carries vtc_id and a hash that + binds it to one contract and one Delivery, and the typ rule of + + + +Sharma Expires 20 March 2027 [Page 53] + +Internet-Draft PACT September 2026 + + + Section 14.1 stops a signature over one object type standing for + another. + +17.10. Nondeterminism as Shield and as Weapon + + A re-execution profile that does not state what determinism it + assumes cuts both ways. An honest Seller doing model-assisted work + is convicted by a re-execution that differs for ordinary reasons. A + cheating Seller escapes any fraud proof by asserting nondeterminism, + unfalsifiably. A verification profile MUST state whether it is + deterministic and what tolerance applies, and a contract naming one + that does not is not safely enforceable by anyone. + +17.11. Fabricated History + + The argument for reputation derived from Outcome Records is that + faking a history requires running real contracts. That argument + fails if records do not name the parties or carry no Facilitator + signature, since two cooperating identities can then manufacture + history at the cost of two signatures. Section 12 requires both. It + fails in the other direction if a negative outcome requires the + signature of the party it records against; reputation that is + structurally incapable of recording a loss is not evidence of + anything. + +17.12. Retrieval + + A GET on a contract's Status or Outcome Record MUST be refused unless + the requester is a party named in the contract's parties, the + identifier in the contract's parent.facilitator, or a party the + Facilitator has chosen to admit; a Facilitator MAY open retrieval + more widely and SHOULD say so in its capability document. How a + requester proves which identifier it is, on a GET with no body to + sign, is an HTTP-layer matter this document leaves to the deployment. + The -01 revision left retrieval unauthenticated by default, which + published every contract graph a Facilitator held to anyone who could + guess an identifier. + +17.13. Key Compromise and Rotation + + A signature here is a long-lived commitment, and a compromised key + signs contracts the party never agreed to. Rotation and revocation + belong to the identity method behind the kid (Section 14.1.1), and + this document does not restate them. Two things it does require: a + Facilitator MUST record, with each record it accepts, the key + material or its digest as resolved at the time of acceptance, so that + a later rotation does not make an earlier signature unverifiable; and + a Facilitator MUST NOT accept a record whose kid resolves to a key + + + +Sharma Expires 20 March 2027 [Page 54] + +Internet-Draft PACT September 2026 + + + the identity method marks as revoked at the time of acceptance. + +17.14. Denial of Service by Challenge + + Every accepted Challenge costs an independent evaluation. Without a + cost to the Challenger, a party can exhaust a Verifier's or a + Facilitator's capacity by challenging every Delivery. The deposit of + Section 7.3 is one defence, and it is a MAY because a deposit also + deters the honest challenger an open model relies on. A Facilitator + that requires no deposit SHOULD rate-limit Challenges per Challenger + and per contract, and SHOULD publish that it does so. + +18. Privacy Considerations + + PACT moves contracts and evidence about work, and both leak. + +18.1. Input Disclosure Before Contract Formation + + Publishing a representative input sample so that a counterparty can + price the work discloses production data to parties with whom no + contract exists and who may be in unknown jurisdictions. Samples + SHOULD be synthetic or de-identified. Where a real sample is + necessary, it SHOULD be disclosed only after a confidentiality + undertaking, and the constraints member SHOULD carry the retention + and deletion terms. This document cannot enforce any of that and + does not pretend to. + +18.2. The Contract Graph + + A Facilitator that publishes its Outcome Records makes the contract + graph public. From it a reader can reconstruct an organisation's + suppliers, spend and cadence, which is commercially sensitive even + when no individual is identifiable. Transparency and counterparty + privacy are in genuine tension here, and this document resolves it in + favour of neither: retrieval is restricted by default + (Section 17.12), a Facilitator MAY publish aggregates, and SHOULD NOT + publish per-contract records identifying both parties without their + agreement. Outcome Records leak the same graph by construction, + since each names both parties and the counterparty retains a signed + copy indefinitely. Selective disclosure over Outcome Records, so + that a holder can prove a completed contract without revealing the + counterparty, is possible with mechanisms specified elsewhere and is + not specified here. + + + + + + + + +Sharma Expires 20 March 2027 [Page 55] + +Internet-Draft PACT September 2026 + + +18.3. Challenger Access + + An open challenge model requires that some party outside the contract + can obtain the deliverable and the input in order to build a fraud + proof. That is in direct conflict with confidentiality of both. The + conflict is real and this document does not dissolve it. What it + does is make the choice visible: a contract whose content cannot be + disclosed to a Challenger will receive no Challenge from outside its + parties, and a terms profile that counts on one has counted on + nothing. + +18.4. Retention + + Retention duties stated for dispute purposes can conflict with + erasure rights asserted by a data subject. Contracts SHOULD state a + retention period, and implementers should be aware that a hash + commitment survives deletion of the content it commits to, which is + usually the property they want and occasionally the one they must + explain. + +19. IANA Considerations + + This document asks IANA to register seven media types in the vendor + tree and one well-known URI. It creates no registry. It defines + problem types but does not ask for a registry of them (Section 19.3). + The -01 revision asked for two registries, one of verification + profiles and one of settlement bindings, and listed under the second + an identifier in another project's namespace that nobody had defined; + both requests are withdrawn. A profile of either kind is identified + by a URI under its definer's control and needs no registration. + +19.1. Media Types + + IANA is requested to register the following in the "Media Types" + registry, per [RFC6838], in the vendor tree. The template below is + given once in full; the seven registrations differ only in the + subtype name and the object they carry. + + Type name: application + + Subtype name: see Table 5 + + Required parameters: N/A + + Optional parameters: N/A + + Encoding considerations: binary; the content is JSON text as defined + in [RFC8259], encoded in UTF-8 + + + +Sharma Expires 20 March 2027 [Page 56] + +Internet-Draft PACT September 2026 + + + Security considerations: See Section 17 of this document. In + particular these media types carry signed objects whose signatures + MUST be verified under the constraints in Section 14.1; accepting + one without algorithm restriction permits signature forgery. + + Interoperability considerations: Objects MUST be canonicalized per + [RFC8785] before hashing or signing. Implementations that + canonicalize by sorting object keys on Unicode code point rather + than UTF-16 code unit will produce divergent digests for keys + outside the Basic Multilingual Plane. + + Published specification: This document + + Applications that use this media type: Services and autonomous + agents forming and recording task contracts under this + specification + + Fragment identifier considerations: As specified for application/ + json + + Additional information: Deprecated alias names: none. Magic + numbers: none. File extensions: .json. Macintosh file type code: + TEXT + + Person & email address to contact: Laxmikant Sharma + + + Intended usage: COMMON + + Restrictions on usage: None + + Author: Laxmikant Sharma + + Change controller: Laxmikant Sharma + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 57] + +Internet-Draft PACT September 2026 + + + +===========================+==========================+============+ + | Subtype name | Object | Defined in | + +===========================+==========================+============+ + | vnd.pact.contract+json | Verifiable | Section 5 | + | | Task Contract | | + +---------------------------+--------------------------+------------+ + | vnd.pact.delivery+json | Delivery | Section 6 | + +---------------------------+--------------------------+------------+ + | vnd.pact.verdict+json | Verdict | Section | + | | | 7.2 | + +---------------------------+--------------------------+------------+ + | vnd.pact.challenge+json | Challenge | Section | + | | | 7.3 | + +---------------------------+--------------------------+------------+ + | vnd.pact.status+json | Contract | Section 11 | + | | Status | | + +---------------------------+--------------------------+------------+ + | vnd.pact.outcome+json | Outcome | Section 12 | + | | Record | | + +---------------------------+--------------------------+------------+ + | vnd.pact.facilitator+json | Capability | Section 8 | + | | document | | + +---------------------------+--------------------------+------------+ + + Table 5: Media types registered by this document + + The -01 revision asked for these in the standards tree under the + names pact-contract+json and so on. Registration in that tree from + outside the IETF stream needs approval this document does not have + ([RFC6838], Section 3.1), and the vendor tree is where an + individual's specification belongs. + +19.2. Well-Known URI + + IANA is requested to register the following in the "Well-Known URIs" + registry, per [RFC8615]. + + URI suffix: pact-facilitator + + Change controller: Laxmikant Sharma + + Specification document(s): This document, Section 8 + + Status: provisional + + Related information: The resource is served with media type + application/vnd.pact.facilitator+json and MUST be signed. + + + + +Sharma Expires 20 March 2027 [Page 58] + +Internet-Draft PACT September 2026 + + +19.3. Problem Types + + This document creates no registry for its problem types. [RFC9457] + Section 4.2 establishes the "HTTP Problem Types" registry for types + intended for reuse across applications; the types below are specific + to this protocol and are identified by URIs in a namespace this + document defines, which that specification permits without + registration. Each is the identifier in the table appended to the + prefix tag:laxsharma79@gmail.com,2026:pact:problem:, a tag URI + [RFC4151] under the author's control. A tag URI is an identifier and + is not dereferenceable, which is why it was chosen over the -01 + revision's prefix on a code-hosting site: an identifier should not + change when hosting does. Documentation for every type is maintained + in the repository named in Section 16. Each entry carries the + identifier, the HTTP status it accompanies, and the section stating + the rule it reports. A terms profile that refuses a request defines + its own types under its own prefix and reports them as Section 13.3 + says. + + +=============================+========+===============+ + | Identifier | Status | Defined in | + +=============================+========+===============+ + | algorithm-not-permitted | 400 | Section 14.1 | + +-----------------------------+--------+---------------+ + | amount-invalid | 422 | Section 14.2 | + +-----------------------------+--------+---------------+ + | challenge-window-closed | 409 | Section 7.3 | + +-----------------------------+--------+---------------+ + | child-outcome-invalid | 422 | Section 10.2 | + +-----------------------------+--------+---------------+ + | deadline-invalid | 422 | Section 14.2 | + +-----------------------------+--------+---------------+ + | evidence-nonconformant | 422 | Section 6 | + +-----------------------------+--------+---------------+ + | facilitator-mismatch | 422 | Section 13.1 | + +-----------------------------+--------+---------------+ + | finality-ordering-violation | 422 | Section 10.3 | + +-----------------------------+--------+---------------+ + | flow-unsupported | 422 | Section 7.1 | + +-----------------------------+--------+---------------+ + | internal-error | 500 | Section 13 | + +-----------------------------+--------+---------------+ + | no-recorded-delivery | 409 | Section 7.2 | + +-----------------------------+--------+---------------+ + | object-conflict | 409 | Section 13.2 | + +-----------------------------+--------+---------------+ + | parent-unresolvable | 422 | Section 10.2 | + +-----------------------------+--------+---------------+ + + + +Sharma Expires 20 March 2027 [Page 59] + +Internet-Draft PACT September 2026 + + + | parties-not-distinct | 422 | Section 14.2 | + +-----------------------------+--------+---------------+ + | payload-too-large | 413 | Section 13 | + +-----------------------------+--------+---------------+ + | proof-nonconformant | 422 | Section 7.3 | + +-----------------------------+--------+---------------+ + | retrieval-restricted | 403 | Section 17.12 | + +-----------------------------+--------+---------------+ + | schema-invalid | 422 | Section 14.2 | + +-----------------------------+--------+---------------+ + | settlement-unsupported | 422 | Section 13.1 | + +-----------------------------+--------+---------------+ + | signature-invalid | 401 | Section 14.1 | + +-----------------------------+--------+---------------+ + | signature-missing | 401 | Section 14.2 | + +-----------------------------+--------+---------------+ + | signatures-unordered | 422 | Section 14.1 | + +-----------------------------+--------+---------------+ + | terms-parameters-invalid | 422 | Section 5.3 | + +-----------------------------+--------+---------------+ + | terms-unsupported | 422 | Section 5.3 | + +-----------------------------+--------+---------------+ + | unexpected-signer | 422 | Section 14.2 | + +-----------------------------+--------+---------------+ + | unknown-contract | 404 | Section 13 | + +-----------------------------+--------+---------------+ + | verdict-nonconformant | 422 | Section 7.2 | + +-----------------------------+--------+---------------+ + | verifier-not-independent | 422 | Section 9.1 | + +-----------------------------+--------+---------------+ + | wrong-state | 409 | Section 4.2 | + +-----------------------------+--------+---------------+ + + Table 6: Problem types defined by this document + + The table is generated from the reference implementation's own list, + so that every type an implementation of this document emits has a + line here. The -01 revision listed eight of the twenty-nine its + implementation used. + +20. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + + + + +Sharma Expires 20 March 2027 [Page 60] + +Internet-Draft PACT September 2026 + + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8785] Rundgren, A., Jordan, B., and S. Erdtman, "JSON + Canonicalization Scheme (JCS)", RFC 8785, + DOI 10.17487/RFC8785, June 2020, + . + + [RFC9457] Nottingham, M., Wilde, E., and S. Dalal, "Problem Details + for HTTP APIs", RFC 9457, DOI 10.17487/RFC9457, July 2023, + . + + [RFC7515] Jones, M., Bradley, J., and N. Sakimura, "JSON Web + Signature (JWS)", RFC 7515, DOI 10.17487/RFC7515, May + 2015, . + + [RFC7518] Jones, M., "JSON Web Algorithms (JWA)", RFC 7518, + DOI 10.17487/RFC7518, May 2015, + . + + [RFC8037] Liusvaara, I., "CFRG Elliptic Curve Diffie-Hellman (ECDH) + and Signatures in JSON Object Signing and Encryption + (JOSE)", RFC 8037, DOI 10.17487/RFC8037, January 2017, + . + + [RFC8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital + Signature Algorithm (EdDSA)", RFC 8032, + DOI 10.17487/RFC8032, January 2017, + . + + [RFC7517] Jones, M., "JSON Web Key (JWK)", RFC 7517, + DOI 10.17487/RFC7517, May 2015, + . + + [RFC8615] Nottingham, M., "Well-Known Uniform Resource Identifiers + (URIs)", RFC 8615, DOI 10.17487/RFC8615, May 2019, + . + + [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type + Specifications and Registration Procedures", BCP 13, + RFC 6838, DOI 10.17487/RFC6838, January 2013, + . + + [RFC8259] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data + Interchange Format", STD 90, RFC 8259, + DOI 10.17487/RFC8259, December 2017, + . + + + +Sharma Expires 20 March 2027 [Page 61] + +Internet-Draft PACT September 2026 + + + [RFC9162] Laurie, B., Messeri, E., and R. Stradling, "Certificate + Transparency Version 2.0", RFC 9162, DOI 10.17487/RFC9162, + December 2021, . + + [RFC3339] Klyne, G. and C. Newman, "Date and Time on the Internet: + Timestamps", RFC 3339, DOI 10.17487/RFC3339, July 2002, + . + + [RFC9110] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Semantics", STD 97, RFC 9110, + DOI 10.17487/RFC9110, June 2022, + . + + [RFC9325] Sheffer, Y., Saint-Andre, P., and T. Fossati, + "Recommendations for Secure Use of Transport Layer + Security (TLS) and Datagram Transport Layer Security + (DTLS)", BCP 195, RFC 9325, DOI 10.17487/RFC9325, November + 2022, . + + [RFC4151] Kindberg, T. and S. Hawke, "The 'tag' URI Scheme", + RFC 4151, DOI 10.17487/RFC4151, October 2005, + . + + [RFC1918] Rekhter, Y., Moskowitz, B., Karrenberg, D., de Groot, G. + J., and E. Lear, "Address Allocation for Private + Internets", BCP 5, RFC 1918, DOI 10.17487/RFC1918, + February 1996, . + + [RFC4193] Hinden, R. and B. Haberman, "Unique Local IPv6 Unicast + Addresses", RFC 4193, DOI 10.17487/RFC4193, October 2005, + . + + [I-D.bhutton-json-schema] + Wright, A., Andrews, H., Hutton, B., and G. Dennis, "JSON + Schema: A Media Type for Describing JSON Documents", Work + in Progress, Internet-Draft, draft-bhutton-json-schema-01, + 10 June 2022, . + + [DID-CORE] W3C, "Decentralized Identifiers (DIDs) v1.0", W3C + Recommendation, 19 July 2022, + . + + [DID-WEB] W3C Credentials Community Group, "did:web Method + Specification", 2026, + . + +21. Informative References + + + +Sharma Expires 20 March 2027 [Page 62] + +Internet-Draft PACT September 2026 + + + [RFC9334] Birkholz, H., Thaler, D., Richardson, M., Smith, N., and + W. Pan, "Remote ATtestation procedureS (RATS) + Architecture", RFC 9334, DOI 10.17487/RFC9334, January + 2023, . + + [RFC9711] Lundblade, L., Mandyam, G., O'Donoghue, J., and C. + Wallace, "The Entity Attestation Token (EAT)", RFC 9711, + DOI 10.17487/RFC9711, April 2025, + . + + [RFC9943] Birkholz, H., Delignat-Lavaud, A., Fournet, C., Deshpande, + Y., and S. Lasker, "An Architecture for Trustworthy and + Transparent Digital Supply Chains", RFC 9943, + DOI 10.17487/RFC9943, June 2026, + . + + [RFC9942] Steele, O., Birkholz, H., Delignat-Lavaud, A., and C. + Fournet, "CBOR Object Signing and Encryption (COSE) + Receipts", RFC 9942, DOI 10.17487/RFC9942, June 2026, + . + + [RFC8725] Sheffer, Y., Hardt, D., and M. Jones, "JSON Web Token Best + Current Practices", BCP 225, RFC 8725, + DOI 10.17487/RFC8725, February 2020, + . + + [RFC7942] Sheffer, Y. and A. Farrel, "Improving Awareness of Running + Code: The Implementation Status Section", BCP 205, + RFC 7942, DOI 10.17487/RFC7942, July 2016, + . + + [RFC8555] Barnes, R., Hoffman-Andrews, J., McCarney, D., and J. + Kasten, "Automatic Certificate Management Environment + (ACME)", RFC 8555, DOI 10.17487/RFC8555, March 2019, + . + + [RFC5280] Cooper, D., Santesson, S., Farrell, S., Boeyen, S., + Housley, R., and W. Polk, "Internet X.509 Public Key + Infrastructure Certificate and Certificate Revocation List + (CRL) Profile", RFC 5280, DOI 10.17487/RFC5280, May 2008, + . + + [RFC3647] Chokhani, S., Ford, W., Sabett, R., Merrill, C., and S. + Wu, "Internet X.509 Public Key Infrastructure Certificate + Policy and Certification Practices Framework", RFC 3647, + DOI 10.17487/RFC3647, November 2003, + . + + + + +Sharma Expires 20 March 2027 [Page 63] + +Internet-Draft PACT September 2026 + + + [RFC2801] Burdett, D., "Internet Open Trading Protocol - IOTP + Version 1.0", RFC 2801, DOI 10.17487/RFC2801, April 2000, + . + + [I-D.ietf-httpapi-idempotency-key-header] + Jena, J. and S. Dalal, "The Idempotency-Key HTTP Header + Field", Work in Progress, Internet-Draft, draft-ietf- + httpapi-idempotency-key-header-07, 15 October 2025, + . + + [I-D.ietf-satp-core] + Hargreaves, M., Hardjono, T., Belchior, R., Ramakrishna, + V., and A. Chiriac, "Secure Asset Transfer Protocol (SATP) + Core", Work in Progress, Internet-Draft, draft-ietf-satp- + core-16, 13 August 2026, + . + + [I-D.hood-agtp-commerce] + Hood, C., "AGTP-Commerce: Open Commerce Specification for + Agent-to-Agent Transactions", Work in Progress, Internet- + Draft, draft-hood-agtp-commerce-00, 28 June 2026, + . + + [I-D.ietf-wimse-aims] + Kasselman, P., Lombardo, J., Rosomakho, Y., Campbell, B., + Steele, N., and A. Parecki, "AI Identity Management + System", Work in Progress, Internet-Draft, draft-ietf- + wimse-aims-00, 15 September 2026, + . + + [I-D.stone-vcap-ap2-binding] + Stone, B. E. N. S. S. T. O. N., "VCAP-AP2 Binding: + Verified Delivery Settlement for the Agent Payments + Protocol", Work in Progress, Internet-Draft, draft-stone- + vcap-ap2-binding-01, 4 September 2026, + . + + [I-D.sahu-agent-action-receipts] + sahu, N., "Signed, Hash-Chained Action Receipts for AI + Agents", Work in Progress, Internet-Draft, draft-sahu- + agent-action-receipts-00, 16 August 2026, + . + + + +Sharma Expires 20 March 2027 [Page 64] + +Internet-Draft PACT September 2026 + + + [I-D.mih-sato-agent-accountability-composition] + Mih, S., Sato, Schrock, I., Bu, S., and A. Sokolov, "Agent + Accountability: Composition and Conformance", Work in + Progress, Internet-Draft, draft-mih-sato-agent- + accountability-composition-01, 16 August 2026, + . + + [I-D.asor-wimse-agent-delegation-chain] + Asor, R., "Verifiable Attenuated Delegation for AI Agent + Chains", Work in Progress, Internet-Draft, draft-asor- + wimse-agent-delegation-chain-01, 3 September 2026, + . + + [I-D.pinto-agent-authz-contestability] + Pinto, T., "Contestability Bindings for Authorized Agent + Actions", Work in Progress, Internet-Draft, draft-pinto- + agent-authz-contestability-01, 10 September 2026, + . + + [I-D.laxsharma-pact-01] + Sharma, L., "PACT: Liability and Settlement for Autonomous + Agent Contracts", Internet-Draft, draft-laxsharma-pact-01, + superseded by this document, 4 September 2026, + . + + [ASOKAN98] Asokan, N., Shoup, V., and M. Waidner, "Asynchronous + Protocols for Optimistic Fair Exchange", Proceedings of + the IEEE Symposium on Security and Privacy, 1998, + . + + [BELENKIY08] + Belenkiy, M., Chase, M., Erway, C.C., Jannotti, J., Kupcu, + A., and A. Lysyanskaya, "Incentivizing Outsourced + Computation", Proceedings of the 3rd International + Workshop on Economics of Networked Systems (NetEcon '08), + pp. 85-90, 2008, + . + + [POLINSKY99] + Polinsky, A.M. and S. Shavell, "Public Enforcement of + Law", Encyclopedia of Law and Economics, entry 8000, + Edward Elgar. The result is attributed therein to Bentham + (1789), 1999. + + + + +Sharma Expires 20 March 2027 [Page 65] + +Internet-Draft PACT September 2026 + + + [SP800-186] + National Institute of Standards and Technology, + "Recommendations for Discrete Logarithm-based + Cryptography: Elliptic Curve Domain Parameters", NIST + Special Publication 800-186, February 2023, + . + +Appendix A. An Example Terms Profile: bonded-restitution + + This appendix is not normative. It carries one terms profile, under + an example identifier and unregistered, so that the experiment in + Section 1.4 has something to run against and the vectors in the + reference repository have something to reproduce. It is the -01 + revision's settlement content written as a schedule over the events + of Section 4.2, with the choices the -01 revision left open now made, + and it is offered as an example of the form a profile takes, not as a + recommendation of these terms. What the figures below mean between + the parties to a contract that names this profile is a question this + document does not answer and its author is not qualified to answer; a + profile meant for use needs an owner who is. + +A.1. Identity and Bundle + + Identifier: tag:laxsharma79@gmail.com,2026:pact:bonded-restitution. + The bundle in the reference repository, under profiles/bonded- + restitution/, contains README.md (this text), parameters.schema.json + and vectors.json; profile_hash is the manifest digest over those + three files and Section 15 prints it. Problem types this profile + reports are under the prefix + tag:laxsharma79@gmail.com,2026:pact:bonded-restitution:problem:. + +A.2. Parameters + + seller_bond: amount, required. What the Seller posts before + performance. + + verification_fund: amount, required. What the Buyer posts to pay + for checking. + + cap: amount, required. The most that leaves the Seller's accounts + under this contract. + + restitution_basis: string, required. released or price. + + remainder_to: string, optional. buyer or sink; sink when absent. + + verifier_fee: amount, optional. Paid from the fund at each Verdict; + 0.00 when absent. + + + +Sharma Expires 20 March 2027 [Page 66] + +Internet-Draft PACT September 2026 + + + principal_on: string, required. The event at which the price moves + to the Seller: verdict (a PASS Verdict), delivered, or window- + closed. + + assurance: object, required. mode (certain, committed-sample or + open) and q_min (a number greater than zero and at most one). + + The -01 revision's four release modes map onto flow and principal_on + as Appendix B shows. + +A.3. Accounts + + Three internal accounts, opened empty: escrow, bond, fund. External + accounts, unbounded as sources and sinks: buyer, seller, verifier, + challenger: for each Challenger, and sink. Closure requires the + three internal accounts to hold zero after the last entry. + +A.4. Admission + + At accepted the profile evaluates, exactly and in the contract's + currency, with P the price, B seller_bond, q assurance.q_min, and E + equal to P when principal_on is delivered and zero otherwise: + + B >= P * (1 - q) / q + E + + and reports assurance-constraint-unsatisfied when it does not hold, + or when assurance.mode is open alone. The inequality is the + classical deterrence bound ([POLINSKY99]; [BELENKIY08] Theorem 1 for + outsourced computation), with E the one term the -01 revision added: + value that moved before a Verdict cannot be recovered by the + schedule, so it raises what the Seller must post one for one. A + contract whose seller_bond or verification_fund exceeds cap is + reported as parameters-inconsistent. + +A.5. Schedule + + For each event the schedule emits the entries below, in the order + listed, omitting any entry whose amount is zero. Every event of + Table 2 not named here emits nothing. Amounts are computed from the + contract and the trace prefix; "released" is the sum of principal + entries emitted so far. + + funded: buyer to escrow, P, lock; seller to bond, B, bond; buyer to + fund, verification_fund, fund. + + delivered: if principal_on is delivered: escrow to seller, the + escrow balance, principal. + + + + +Sharma Expires 20 March 2027 [Page 67] + +Internet-Draft PACT September 2026 + + + verdict: fund to verifier, the lesser of verifier_fee and the fund + balance, verification; then if the outcome is PASS, no Challenge + is answered, and principal_on is verdict: escrow to seller, the + escrow balance, principal. + + window-closed: if principal_on is window-closed and the standing + Verdict is not FAIL: escrow to seller, the escrow balance, + principal. + + terminal, FINAL: escrow to seller, the escrow balance, principal; + bond to seller, the bond balance, return; fund to buyer, the fund + balance, fund-return. + + terminal, ABANDONED: escrow to buyer, the escrow balance, reverse; + bond to seller, the bond balance, return; fund to buyer, the fund + balance, fund-return. The -01 revision said the bond was slashed + "to the extent of" the basis here and never said by how much; with + the price reversed the Buyer's loss is zero under either basis, so + nothing is slashed. + + terminal, SETTLED: in five ranks, each drawing only what remains. + (1) escrow to buyer, the escrow balance, reverse. (2) if + challenge_upheld: fund to the Challenger whose Challenge the + standing Verdict answers, the lesser of that Challenge's costs and + the fund balance, costs. (3) bond to buyer, the lesser of the bond + balance, cap, and the Buyer's loss, restitution; the loss is + "released" under basis released and P minus the rank-1 entry under + basis price, which differ only when the price moved in part. (4) + if challenge_upheld: bond to that Challenger, the bond balance, + bounty. (5) bond to buyer or sink per remainder_to, the bond + balance, remainder. Then fund to buyer, the fund balance, fund- + return. + + Ranks 2 and 4 pay one Challenger, the one whose Challenge the + standing Verdict answers. A Challenge that was not answered by the + standing Verdict, whether lapsed, rejected or superseded, receives + nothing. Rank 4 gives the whole remaining bond, because the -01 + revision forbade capping it at a fraction chosen for tidiness and + fixed no figure; a profile owner who wants a different rule changes + this line and the vectors with it. + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 68] + +Internet-Draft PACT September 2026 + + +A.6. Vectors + + With P 180.00, B 18.00, fund 0.50, cap 180.00, basis released, + remainder to sink, no verifier fee, principal_on verdict, assurance + certain with q 1.0, under the verdict-first flow, and a Challenge + claiming costs of 1.20. Amounts are in USDC. Trace indexes count + from zero. The lists below are what vectors.json carries for the two + paths in the figures of this document; the repository's file also + carries the SETTLED-by-Verifier and ABANDONED paths and the price + basis. + + trace 0 accepted 1 funded 2 delivered 3 verdict PASS + 4 window-opened 5 window-closed 6 children-final + 7 terminal FINAL + + event from to amount code + 1 buyer escrow 180.00 lock + 1 seller bond 18.00 bond + 1 buyer fund 0.50 fund + 3 escrow seller 180.00 principal + 7 bond seller 18.00 return + 7 fund buyer 0.50 fund-return + + Figure 14: FINAL: the path of Figure 1 + + trace 0 accepted 1 funded 2 delivered 3 verdict PASS + 4 window-opened 5 challenge 6 verdict FAIL (answers 5, + supersedes 3) 7 children-final 8 terminal SETTLED, + challenge_upheld true + + event from to amount code + 1 buyer escrow 180.00 lock + 1 seller bond 18.00 bond + 1 buyer fund 0.50 fund + 3 escrow seller 180.00 principal + 8 fund challenger: 0.50 costs + 8 bond buyer 18.00 restitution + + Figure 15: SETTLED on an upheld Challenge: the path of Figure 5 + + In the second vector rank 1 emits nothing because the escrow is + empty, rank 2 pays the lesser of 1.20 and the fund's 0.50, rank 3 + pays the whole bond because the Buyer's loss (180.00 released) + exceeds it, and ranks 4 and 5 and the fund return emit nothing + because nothing remains. Both lists satisfy closure: after the last + entry the three internal accounts hold zero. + + + + + +Sharma Expires 20 March 2027 [Page 69] + +Internet-Draft PACT September 2026 + + +Appendix B. Changes from -01 + + This revision separates the protocol from the meaning of its terms. + The -01 revision, in its title, abstract, Section 1.2 and throughout, + made who owed whom the subject of the document; two readers on the + IETF dispatch list observed in September 2026 that this placed it + outside what the IETF is placed to evaluate, and they were right. + What follows is the list of what changed, with the wire consequences + first. + + * The pact version is 0.2 and every committed digest changed + (Section 15). + + * The liability member is gone. A contract carries terms: a profile + URI, a digest over the profile's bundle, and an opaque parameter + object (Section 5.3). The -01 figures are the parameters of the + profile in Appendix A. assurance moved into that profile's + parameters; parent moved to the top level and gained facilitator. + + * The four release modes are replaced by three flows and a profile + parameter: on-verification is verdict-first with principal_on + verdict; on-window is delivery-first with window-closed; + optimistic is delivery-first with delivered; unsecured is no- + window with delivered (Section 7.1). + + * verification.max_verdict_seconds is added, with the verdict-lapsed + event, so a silent Verifier cannot hold a contract in DELIVERED + forever (Section 7.2). + + * The Work Attestation is the Outcome Record, with the RATS + collision explained (Section 1.3). Its subject, role, amounts and + outcome vocabulary are replaced by parties, an outcome object, the + full trace, and terms_result (Section 12). One record per + contract. + + * Every response is a signed Contract Status carrying the trace, + replacing the unsigned state member the -01 revision added to + echoed objects (Section 11). + + * The event trace and the state machine over it are new + (Section 4.2); RELEASING and PROPOSED are gone, AWAITING_CHILDREN + is added. + + * delivery_hash covers the Delivery's signature; every digest covers + the signature set (Section 2). Signature sets are sorted and + ECDSA is low-S (Section 14.1). Merkle leaves cover signatures + (Section 12.2). + + + + +Sharma Expires 20 March 2027 [Page 70] + +Internet-Draft PACT September 2026 + + + * A nonconformant Delivery is refused and recorded nowhere; the -01 + revision treated it as a FAIL Verdict (Section 6). The Buyer + countersignature sentence is withdrawn. + + * A Verdict may carry challenge_hash; a Challenge may carry costs; a + Seller-signed Challenge is refused (Section 7). + + * Contract trees work across Facilitators: child registration, child + outcome supply, child-unresolved, a finite latest finality instant + per contract and the rule L(child) before L(parent); the depth and + cycle rules are withdrawn with the reason (Section 10). The -01 + Section 10.2 is one sentence in Section 10.3. + + * Section 3 is a data dictionary and a role table (Section 3); no + sentence in it requires anything of a party. + + * Media types move to the vendor tree; the two registries and the + pact-escrow row are withdrawn; problem types move to a tag URI + namespace and the table lists every type the implementation emits + (Section 19). + + * Retrieval is restricted by default and fetch discipline is stated + (Section 17.12, Section 17.5). The threat model says plainly what + is enforced against a Facilitator, which is nothing, and what is + attributable (Section 17.1). + + * The experiment is restated over protocol observables + (Section 1.4). + +Acknowledgements + + Rich Salz and John C Klensin, on the IETF dispatch list in September + 2026, read the -01 revision as a document about who owes whom with a + protocol attached, and said so; this revision's split between records + and terms is the consequence, and the author is grateful for the + reading. The UTF-16 key-ordering vector that exposed a latent + canonicalization defect in the reference validator, and the + formulation of verifier independence as a relation the evaluator + derives rather than a field the record declares, came from Tersign + (wowlegend) on x402-foundation/x402 issue 3065. The observation that + verification tiers say how work is checked and never who checks it + came from msaleme on the same thread. Rich Smith's A2A Settlement + Extension was the clearest instance of the pattern the -01 revision + corrected, and he engaged with the critique on a2aproject/A2A + discussion 1576. + +Author's Address + + + + +Sharma Expires 20 March 2027 [Page 71] + +Internet-Draft PACT September 2026 + + + Laxmikant Sharma + Independent + Email: laxsharma79@gmail.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 72] diff --git a/draft/draft-laxsharma-pact-02.xml b/draft/draft-laxsharma-pact-02.xml new file mode 100644 index 0000000..5a97b86 --- /dev/null +++ b/draft/draft-laxsharma-pact-02.xml @@ -0,0 +1,2964 @@ + + + + PACT: Co-Signed Task Contracts, Delivery and Verdict Records, and Outcome Records for Autonomous Agents + + + Independent +
laxsharma79@gmail.com
+
+ + autonomous agents + agent commerce + signed records + verification + Merkle tree + + + Autonomous agents can already prove who they are, show whose + authority they act under, find one another, call one another, and pay. + What they cannot do with any existing specification is agree on a task + in a form a third party can check, deliver against it, have the + delivery judged by someone other than the performer, and carry away a + record of the outcome that a stranger can verify. This document + specifies PACT, a set of signed JSON records that closes that gap. + + PACT defines four things: a co-signed task contract whose digest + covers its signature set, so the commitment proves who agreed and not + only what was written; a Verdict record bound by digest to the Delivery + record it judges; a Facilitator-signed event trace and Outcome Record + for every contract, so what happened is recorded once, in one order, + by a party that is not the performer; and a Merkle commitment from a + parent contract's Outcome Record to the Outcome Records of its + subcontracts. + + Settlement terms are carried by reference to a profile defined + outside this document. This document specifies no escrow, custody or + release of value, and takes no position on the legal effect of any + record it defines. + +
+ + + +
Introduction + +
Motivation + By late 2026 an autonomous agent can prove who it is, show whose + authority it acts under, discover another agent, call it, record what + happened in a tamper-evident receipt, and pay for the call. Each of + those is the subject of active standardisation, and several are + specified in more detail than this document specifies anything. + + What none of them provides is interoperability at the level of the + task. Two agents built by different vendors have no common record of + what one asked the other to do, no common form for the result, no way + to have that result judged by a third implementation against criteria + fixed before the work began, and no record of the outcome that a + fourth implementation can verify without trusting any of the first + three. Receipts record that an action occurred. Audit records + establish whether behaviour matched intent. Payment schemes move value + on the payer's instruction. None of them says what was agreed, what + was delivered, or whether the one met the other. + + That gap is not an oversight in those documents; it is outside + their scope, and correctly so. It is the gap this document + addresses, and only that gap. +
+ +
What This Document Specifies, and What It Does Not + PACT specifies exactly four things: a co-signed contract record + whose digest covers its signature set (); a + Delivery record and the Verdict record bound to it by digest + (, ); an event + trace, signed by a Facilitator, from which one Outcome Record per + contract is produced (, + ); and a Merkle commitment from a parent's + Outcome Record to its children's (). + + A contract names its settlement terms by reference: a profile + identifier, a digest over the profile's bytes, and a parameter object + that this document does not read (). What those + terms mean, and everything about who holds or moves value under them, + is the profile's to say. This document specifies the records, their + digests, who signs each one, the order in which a Facilitator records + events, and a commitment across records. That is the whole of it. + + A deployment relies on other specifications, agreements or + arrangements for: the meaning of the terms a contract names; agent + identity and key distribution; delegation of authority from a human or + organisational principal; agent discovery; transport security beyond + ; an audit or accountability architecture; a + transparency service; a payment rail or settlement network; a + reputation system; and the resolution of any disagreement the records + do not settle. + + Carrying terms by reference is an old pattern in this series. + ACME carries a terms-of-service URL and + requires a client to assert agreement to it before an account is + created, without defining a single term. A certificate carries its + policy as an identifier whose rules live outside the IETF + (, Section 4.2.1.4), and the framework for + writing those rules says it does not aim to + provide legal advice. The Internet Open Trading Protocol + specified the messages of a trade and left + the trade's terms to the parties. PACT follows that line. + + Two mechanisms present in the -00 revision remain withdrawn: + contract channels, and the sealed-bid award procedure. The reasons are + recorded in and are not + repeated. The change from -01 to this revision is listed in + . +
+ +
Relationship to Existing Work + PACT's agree, perform, verify, record loop is an instance of + optimistic fair exchange , in which a third + party is contacted only when the exchange fails. What that literature + establishes is what a third party must be able to observe for an + exchange to be fair; the records in this document are that + observation, written down in a form two implementations can + compare. + + Two adjacent Internet-Drafts address agent commerce settlement + directly. carries Work + Completion Records and an audit-verified settlement timing; + binds verified commerce + settlement to the Agent Payments Protocol. Neither carries a co-signed + contract whose digest covers its signatures, and PACT is designed to + be usable alongside either. + + Five bodies of IETF work touch the same records, and the + relationship to each is stated here so that it is not left to the + reader. + +
+
RATS.
defines Verifier, + Evidence and Attestation Result as terms of art: a RATS Verifier + appraises Evidence about an Attester. PACT's Verifier evaluates a + Delivery against an instrument the parties committed to, and its + terminal record is an Outcome Record, not an attestation. The -01 + revision called that record a Work Attestation and used the RATS + words with other meanings; this revision renames the record and + defines its remaining shared vocabulary in + . Where a verification tier relies on + hardware attestation, the RATS architecture applies unchanged and + PACT consumes its results.
+
SCITT.
defines signed-statement + transparency and defines COSE receipts for + it. PACT does not define a transparency service; a Facilitator that + wants its Outcome Records to be publicly append-only can register + them with a SCITT transparency service, and + says which Facilitator + misbehaviour that closes. The Merkle commitment in + is not a transparency log: it is a fixed + commitment from one record to a known, finite set of other records, + and it uses the tree of for its + construction only.
+
HTTPAPI.
+ is the general mechanism for making a POST safe to retry. PACT does + not use it, because every object it carries is committed by the + digest of its own canonical form and that digest is the idempotency + key (). Error reporting follows + .
+
WIMSE.
gives + workload and agent identity a home. PACT does not define an identity + format; a kid resolves as says, and + that section is written so that an identity system defined + elsewhere can be named without changing this document.
+
SATP.
transfers a + digital asset between two gateways with evidence a third party can + verify. An Outcome Record is not an asset transfer and does not + move one; it is a signed statement that certain records were + received in a certain order, and what any of that means for an + asset is the terms profile's to say.
+
+ + Verification evidence formats for hardware-attested tiers are + specified in and . + Signed, hash-chained action receipts + , composition of + accountability records + , + delegation chains + , and + contestability bindings + are each + specified elsewhere, and PACT consumes rather than restates them. +
+ +
The Experiment + This document is Experimental. The question it tests is stated + over protocol observables only. Given the same sequence of posted + records and the same clock readings, two independent Facilitator + implementations should produce the same event trace + (). Given the same trace and the same terms + profile, they should produce the same Outcome Record body + (), byte for byte after canonicalization. + The experiment succeeds if two independent Facilitators, serving + Buyers and Sellers built by different implementers, reach every + terminal state in with Outcome Records + either can verify and that agree. It fails, and that would itself be + worth recording, if the trace turns out to under-determine the + outcome, which is to say if two honest implementations reading the + same records disagree about what happened. Experience should be + reported to the author and to the repository named in + . The non-normative profile in + exists so that the experiment can + be run before any other profile is written. +
+
+ +
Conventions and Definitions + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 when, and only + when, they appear in all capitals, as shown here. + + Canonical form. Every JSON object defined here is canonicalized with + JCS before hashing or signing. Implementations + MUST order object keys by UTF-16 code unit as + Section 3.2.3 requires. Sorting by Unicode code + point is a common substitution; it agrees with the required order + throughout the Basic Multilingual Plane and diverges above it. + + Object digest. The digest of an object is the string + sha256: followed by the lowercase hexadecimal SHA-256 of the + canonical form of the whole object, including every signature member it + carries. Every hash member in this document that names another object + (vtc_hash, delivery_hash, challenge_hash, + the object member of a trace entry, and the leaves of + ) is that object's digest. A digest that + excluded signatures would prove what was written and not who agreed to + it; the -00 revision had that defect and the -01 revision fixed it for + the contract only. This revision applies one construction + everywhere. + + Signing input. A signature over an object is computed over the + canonical form of the object with the signing member (signature + or signatures) removed, as + specifies. The digest of an object and the signing input of an object + are therefore different byte strings, and the difference is the + signature set. + + Version. The pact member carries a version of the form + major.minor; this document defines 0.2. Every object defined here is + hash-committed and signed, so a member an implementation does not + recognise is inside the commitment and cannot be ignored safely. An + implementation MUST reject an object whose pact version it + does not implement, and MUST reject an object carrying a member this + document does not define for it, with one exception: the contents of + terms.parameters () are defined by the + named profile and this document reads none of them. Extension is by a + new version, not by adding members. + + Time. Every timestamp is an RFC 3339 date-time + in UTC with the "Z" designator. The + Facilitator's clock governs every deadline and window in this + document: the instant at which the Facilitator records an event is the + instant that counts, that instant is what the trace carries, and + parties should allow for skew when acting near a boundary. + says what that clock can and cannot + prove. + + Amounts. An amount is a decimal string with no exponent and a + fractional part of two to eighteen digits; comparisons are exact and + no rounding is implied. A currency is an asset identifier whose + namespace is defined by the settlement binding named in + price.settlement, and need not be an ISO 4217 code. A network + is a ledger identifier in the form the same binding defines. This + document carries amounts; it does not say what any amount is for. Where + a record produced under this document lists amounts, as + terms_result does (), the meaning + of every entry is the named profile's. + + Identifiers. A party identifier is a URI. Two identifiers name the + same party when they are equal after the normalization in + , and every comparison of identifiers in + this document is made after that normalization. + +
Terminology + Four words in this document have meanings elsewhere that are + close enough to mislead, and are defined here once. +
+
Contract:
Used in this document for a co-signed JSON + object of the form in , and for nothing else. + This document takes no position on whether any such object is a + contract in law, in any jurisdiction, and defines no obligation + between the parties that sign one.
+
Verifier, Verdict:
A Verifier here is the party that + evaluates a Delivery against the instrument the contract committed + to, and a Verdict is its signed finding. This is not the Verifier + of , which appraises Evidence about an + Attester; the two roles may be played by the same software in a + hardware-attested tier, and are still different roles.
+
Evidence:
The evidence member of a Delivery is + the set of artefacts a Verifier evaluates, produced by the Seller. + It is not Evidence in the sense of . The + member name is kept from -01 because renaming it would change every + committed digest for no gain in clarity that this note does not + provide.
+
Facilitator:
The party that runs the state machine of + for a contract: it accepts or refuses the + records posted to it, records events in one order on its own clock, + and signs the trace and the Outcome Record. Nothing in this document + says that a Facilitator holds anything of value, and nothing in it + requires that it does.
+
+ The remaining roles are defined by what they sign and receive in + , and the objects by their members in + . +
+
+ +
Data Dictionary + This section lists every member this document defines, by the object + that carries it, with its type, whether it is required in that object, + and what it commits to. It is a dictionary and not a rulebook: the rule + that a record omitting a required member, or carrying one this document + does not define for it, does not conform is stated once in + ; the rules a Facilitator applies when it + accepts or refuses a record are in and in + the section that defines the record. No sentence in this section + requires anything of any party. Where a member's meaning is the named + terms profile's, the entry says so and says nothing more. + + Types are JSON types. A digest is a string of the form in + . An amount is a string of the form in + . A URI is a string. A timestamp is a + string of the form in . Cardinality is + written as required or optional. + +
Members Common to Every Record +
+
pact:
string, required. The protocol version; + 0.2 for objects defined by this document.
+
type:
string, required. The object's type name: + VerifiableTaskContract, Delivery, + Verdict, Challenge, ContractStatus, + OutcomeRecord, or FacilitatorCapabilities.
+
signature:
object, required in Delivery, + Verdict, Challenge, ContractStatus and the capability document. One + JWS entry of the form in , by the single + signer of that record.
+
signatures:
array of objects, required in the + contract and in the Outcome Record. JWS entries of the form in + , sorted as that section says. Commits, in + the contract, to who agreed; in the Outcome Record, to which + Facilitator issued it.
+
+
+ +
Contract Members + Carried in the Verifiable Task Contract (), + media type application/vnd.pact.contract+json. +
+
id:
string, required. Contract identifier, + unique among the contracts of the Facilitator named in + parties.facilitator.
+
parties:
object, required. The identifiers of + the parties, by role: buyer (URI, required), + seller (URI, required), facilitator (URI, + required), verifier (URI, optional). Commits to who plays + each role for this contract.
+
task:
object, required. spec_hash + (digest, required) commits to a TaskSpec (); + spec_uri (URI, optional) says where its bytes may be + fetched; deadline (timestamp, required) is the instant + after which the deadline-passed event may be recorded + ().
+
price:
object, required. amount + (amount, required), currency (string, required), + settlement (URI, required, naming a settlement binding), + network (string, required, in the form the binding + defines). Commits to a figure and a venue that both parties signed. + The meaning of the figure is the named terms profile's.
+
verification:
object, required. tier + (string, required), profile (string or URI, required; + ), criteria_hash (digest, + required; the manifest digest of the acceptance instrument per + ), max_verdict_seconds (integer, + required; the longest interval after delivered within + which a first Verdict is recorded before verdict-lapsed + may be), arbiter (URI, optional). Commits to how a + Delivery is judged and by what.
+
flow:
string, required. One of + verdict-first, delivery-first, no-window + (). Selects the shape of the state machine + for this contract.
+
terms:
object, required + (). profile (URI, required) names + a terms profile; profile_hash (digest, required) commits + to the profile's bytes as says; + parameters (object, required, may be empty) carries the + profile's parameters. This document reads no member of + parameters; every one of them means what the named + profile says.
+
challenge:
object, required. + window_seconds (integer, required, greater than zero) is the + duration of the challenge window; max_dispute_seconds + (integer, required) is the longest interval after a + challenge event within which a Verdict on that Challenge + is recorded before dispute-lapsed may be.
+
parent:
object, optional; present only in a + subcontract (). vtc_id (string, + required), vtc_hash (digest, required), and + facilitator (URI, required) identify the parent contract + and the Facilitator that holds it.
+
+
+ +
TaskSpec Members + The TaskSpec is the content committed by task.spec_hash + (). It is not transmitted over the + endpoints of this document. +
+
description:
string, required. A statement of + the work in natural language.
+
inputs:
object, optional. schema_uri + with schema_hash, and where a representative sample is + published, sample_uri with sample_hash; each URI + with its digest over the dereferenced bytes.
+
deliverable:
object, required. format + (string) and schema_uri with schema_hash.
+
acceptance:
object, required. The verification + instrument: harness_uri with harness_hash for + re-execution tiers, enclave and model policy for attestation tiers, + a proof statement with its verifying key for proving tiers, or + rubric_uri with rubric_hash for judgment tiers; + plus thresholds (object) in machine-readable form. + harness_hash equals the contract's + criteria_hash.
+
constraints:
object, optional. Tool + prohibitions, confidentiality and compliance conditions, in a form + this document does not define.
+
+
+ +
Delivery Members + Carried in the Delivery (), media type + application/vnd.pact.delivery+json. +
+
vtc_id, vtc_hash:
string and digest, + required. Identify and commit to the contract performed.
+
work_hash:
digest, required. Commits to the + delivered bytes, or to a manifest per where + the deliverable is a bundle.
+
work_uri:
URI, optional. Where the bytes may be + fetched, subject to .
+
input_hash:
digest, required for tiers whose + fraud proof re-executes. Commits to the production input actually + consumed.
+
evidence:
object, required. Members profiled by + verification.tier and verification.profile; for + the acceptance profile, profile, + instrument_hash, results_hash and + results_uri. Conformance to the profile is a validity + condition of the Delivery, not a judgement on the work.
+
+
+ +
Verdict Members + Carried in the Verdict (), media type + application/vnd.pact.verdict+json. +
+
vtc_id:
string, required.
+
delivery_hash:
digest, required. Commits to the + Delivery judged, including the Seller's signature over it.
+
challenge_hash:
digest, optional. Present when + the Verdict answers a Challenge; commits to that Challenge.
+
outcome:
string, required. PASS or + FAIL.
+
profile, instrument_hash:
string and + digest, required. The verification profile applied and the digest of + the instrument actually run, which equals the contract's + criteria_hash.
+
results_hash:
digest, required. Commits to the + Verifier's own results.
+
evaluated_at:
timestamp, required. The + Verifier's own clock; informational, since the trace carries the + Facilitator's.
+
+
+ +
Challenge Members + Carried in the Challenge (), media type + application/vnd.pact.challenge+json. +
+
vtc_id, delivery_hash:
string and + digest, required. Identify the contract and commit to the Delivery + challenged.
+
proof:
object, required. Members profiled by + verification.profile; for the acceptance profile, + profile, instrument_hash, results_hash, + results_uri and failing_checks (array of + strings).
+
costs:
object, optional. amount and + currency: a figure the Challenger asserts for producing the + proof. This document records it in the trace and reads it for + nothing; its meaning is the named terms profile's.
+
+
+ +
Contract Status Members + Carried in the Contract Status (), media + type application/vnd.pact.status+json, the Facilitator's + signed response to every accepted request. +
+
vtc_id, vtc_hash:
string and digest, + required.
+
state:
string, required. A state name from + .
+
trace:
array of objects, required. The event + trace so far, in the order recorded (). Each + entry carries event (string, required), at + (timestamp, required), object (digest, required where the + event was caused by a posted record), and the event-specific + members listed in .
+
issued_at:
timestamp, required. When this + status was signed.
+
+
+ +
Outcome Record Members + Carried in the Outcome Record (), media + type application/vnd.pact.outcome+json. +
+
vtc_id, vtc_hash:
string and digest, + required.
+
parties:
object, required. The contract's + parties object, copied, so that the record names its + subjects and which side of the contract each was on.
+
outcome:
object, required. state + (string, required; FINAL, SETTLED or + ABANDONED) and challenge_upheld (boolean, + required).
+
work_hash:
digest, required where a Delivery was + recorded. Binds the record to what was produced.
+
trace:
array of objects, required. The complete + event trace, ending with the terminal event.
+
terms_result:
object, required + (). profile and + profile_hash (copied from the contract), currency + (string), and transfers (array of objects), each with + from (string), to (string), amount + (amount) and code (string). The entries are the named + profile's output for the trace; this document defines their form + and two arithmetic invariants over them, and nothing about their + meaning.
+
children_merkle_root:
digest, required where the + contract has registered children and absent otherwise + ().
+
+
+ +
Capability Document Members + Carried in the Facilitator capability document + (), media type + application/vnd.pact.facilitator+json. +
+
facilitator:
URI, required. The identifier that + appears in parties.facilitator.
+
settlement_bindings:
array of objects, + required. Each with id (URI), networks and + assets (arrays of strings).
+
flows:
array of strings, required. The flows + of the Facilitator implements.
+
verification_profiles:
array of strings, + required.
+
terms_profiles:
array of objects, required, + with at least one entry. Each with id (URI) and + profile_hash (digest): the terms profiles, at the + revisions named, whose schedules this Facilitator evaluates.
+
max_contract_value:
object, optional. + amount and currency.
+
challenge_deposit:
object, optional. + amount and currency; see + .
+
endpoints:
object, required. Maps each endpoint + name in to an absolute URI.
+
+
+ +
Roles + A role is defined by where its identifier appears, what it signs, + and what it receives. Nothing else about a role is defined here. + + Roles, by what each signs and receives + + + + + + + + + + + + + + + + + + + + + +
RoleIdentifier appears inSignsReceives
Buyerparties.buyerthe contract; a child registration + ()Contract Status, Outcome Record
Sellerparties.sellerthe contract; the DeliveryContract Status, Outcome Record
Facilitatorparties.facilitator, + parent.facilitator, the capability documentContract Status, Outcome Record, the capability + documentevery posted record
Verifierparties.verifier, or the + kid of a Verdictthe Verdictthe Delivery and, on a Challenge, the Challenge
Challengerthe kid of a Challengethe ChallengeContract Status
+ One identifier may play more than one role across contracts, and + says which combinations within one + contract a Facilitator refuses. +
+
+ +
Protocol Overview + A contract passes through four phases. Propose establishes the + record. Agree co-signs it and a Facilitator accepts it. Complete + produces a Delivery and a Verdict on it. Record produces an Outcome + Record. Every step after Agree is an event the Facilitator records on + its own clock, in one order, and the sequence of those events is the + contract's trace. The trace is the protocol's central object: the state + machine is defined over it, every response a Facilitator gives carries + the prefix recorded so far, and the Outcome Record carries the whole of + it. + +
+ Message flow under the verdict-first flow, without a Challenge + | | + | | | | + |-- POST contract ->| | | + |<-- Status --------| [ accepted ] | | + | | [ funded ] | | + | | | | + | | [ Seller performs ] | + | |<-- POST Delivery --| | + | |-- Status --------->| [ delivered ] | + | | | | + | |---- Delivery, criteria_hash ------->| + | |<--- POST Verdict -------------------| + | |---- Status ------------------------>| + | | [ verdict PASS ] [ window-opened ] + | | [ window-closed ] [ children-final ] + | | [ terminal FINAL ] | + | | | | + |<-- Outcome Record-|-- Outcome Record ->| | + ]]> +
+ + Every accepted request is answered with a Contract Status + (), a Facilitator-signed object carrying the + state and the trace so far. Nothing in the figure moves value, and no + arrow in it is named for a movement of value. What a terms profile does + at each bracketed event is the profile's, and it is reported once, in + the Outcome Record, as a list the profile produced and the Facilitator + signed. + +
States +
+ Contract states + FUNDED -delivered-> DELIVERED + | | | + | deadline- | deadline- | window-opened + | passed | passed v + | | WINDOW_OPEN <------+ + | | | | | + | | challenge | | window | verdict PASS + | | v | closed | on it, or + | | DISPUTED----|--------+ dispute- + | | | | lapsed + | | verdict | | + | | FAIL | | + v v v v + +--------------------------------------------------+ + | AWAITING_CHILDREN | + +--------------------------------------------------+ + | children-final, then terminal + v + FINAL SETTLED ABANDONED + ]]> +
+ + The figure omits three arrows that the table carries: a FAIL + Verdict recorded in DELIVERED or in WINDOW_OPEN also leads to + AWAITING_CHILDREN; under the no-window flow DELIVERED leads + there directly; and a Verdict that is late (verdict-lapsed) + opens the window without one. FINAL, SETTLED and ABANDONED are + terminal and each produces exactly one Outcome Record. The -01 + revision named one of these states for a movement of value; no state + here is. + + The state named PROPOSED in earlier revisions is gone. Between the + parties' signatures and the Facilitator's acceptance a contract exists + only on the parties' side, so no Facilitator could observe that state + and the reference implementation never reported it. +
+ +
Events + A trace entry is a JSON object with event (one of the + names below), at (the Facilitator's clock when it was + recorded), object where the entry records a posted record + (that record's digest, ), and the members + listed for the event. A Facilitator MUST record the entries of one + contract in the order it recorded them and MUST NOT reorder, remove + or alter an entry once a Status carrying it has been issued + ( says what that rule does and does + not prove). + + + Events: the state each is recorded in, the state that follows, and what the entry carries + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EventRecorded in; thenMembers and condition
acceptednone; then ACCEPTEDobject is vtc_hash. The contract passed .
fundedACCEPTED; then FUNDEDref (string, optional, in the form the settlement binding defines). Recorded when every account the named terms profile requires shows finality on the settlement binding named in price.settlement; how a Facilitator observes that is the binding's to say, and this is the only sentence in this document that mentions an account.
deadline-passedACCEPTED or FUNDED; then AWAITING_CHILDRENtask.deadline has passed with no delivered entry.
deliveredFUNDED; then DELIVEREDobject is the Delivery's digest. The Delivery passed .
window-openedDELIVERED; then WINDOW_OPENUnder delivery-first, immediately after delivered; under verdict-first, immediately after a PASS verdict or after verdict-lapsed. closes_at (timestamp, required) is at plus challenge.window_seconds.
verdictDELIVERED, WINDOW_OPEN or DISPUTED; then see the conditionobject is the Verdict's digest; outcome (PASS or FAIL); answers (digest of the Challenge, when the Verdict carries challenge_hash); supersedes (digest of the Verdict it replaces, when one stood). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending.
verdict-lapsedDELIVERED; then WINDOW_OPENUnder verdict-first, verification.max_verdict_seconds have passed since delivered with no verdict. window-opened follows.
challengeWINDOW_OPEN or DISPUTED; then DISPUTEDobject is the Challenge's digest; costs copied from the Challenge when present. The Challenge passed before closes_at.
dispute-lapsedDISPUTED; then WINDOW_OPENobject is the Challenge's digest. challenge.max_dispute_seconds have passed since that challenge entry with no Verdict answering it. Leads to WINDOW_OPEN once no Challenge is pending; the earlier Verdict, if any, stands.
window-closedWINDOW_OPEN; then AWAITING_CHILDRENcloses_at has passed and no Challenge is pending. The window is never extended: a dispute that outlasts it delays this entry and does not move closes_at.
child-registeredany non-terminal; unchangedobject is the child contract's digest; facilitator (URI). .
child-finalany non-terminal; unchangedobject is the child's Outcome Record digest; child (the child contract's digest).
child-unresolvedany non-terminal; unchangedchild (the child contract's digest). The child's latest finality instant () has passed and no Outcome Record for it is held.
children-finalAWAITING_CHILDREN; then terminal followsEvery registered child has a child-final or child-unresolved entry. A contract with no registered children records this entry on entering AWAITING_CHILDREN.
terminalAWAITING_CHILDREN; then FINAL, SETTLED or ABANDONEDstate (the terminal state) and challenge_upheld (boolean). ABANDONED where deadline-passed was recorded; SETTLED where the standing Verdict is FAIL, with challenge_upheld true when that Verdict answers a Challenge; FINAL otherwise.
+ + The standing Verdict is the last verdict entry in the + trace that no later entry supersedes. A Challenge is pending from its + challenge entry until a verdict entry answers it or + a dispute-lapsed entry names it. + + Every instant in the table is read from the Facilitator's clock, + and an entry conditioned on an instant having passed is recorded at + the first opportunity after it, which need not be that instant. Two + Facilitators given the same posted records with the same clock + readings record the same trace; that is the determinism the + experiment in tests, and the reason + every condition above is stated over the trace and the clock and + nothing else. +
+
+ +
The Verifiable Task Contract + A VTC is a JSON object, media type + application/vnd.pact.contract+json, with the members in + . A VTC is valid only if every required member + is present, the parties are distinct, and both the Buyer and the Seller + have contributed exactly one signature that verifies against a key bound + to its identifier (). The Facilitator and any + Verifier do not sign the VTC; their assent is expressed by acting on it, + and a Facilitator that will not act on a contract refuses it at + . + + The settlement identifier, the network and the asset are all + carried inside price so that a co-signed VTC is bound to one + venue. The -00 revision omitted them, which made a signed contract + replayable against any facilitator, chain or token contract. + +
+ A Verifiable Task Contract, signatures abbreviated + ", + "deadline": "2026-11-14T00:00:00Z" + }, + "price": { + "amount": "180.00", + "currency": "USDC", + "settlement": "https://settle.example/bindings/ledger-1", + "network": "eip155:8453" + }, + "verification": { + "tier": "T0-reexec", + "profile": "acceptance", + "criteria_hash": "sha256:", + "max_verdict_seconds": 86400 + }, + "flow": "verdict-first", + "terms": { + "profile": + "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "profile_hash": "sha256:", + "parameters": { "...": "the profile's; not read here" } + }, + "challenge": { + "window_seconds": 3600, + "max_dispute_seconds": 86400 + }, + "signatures": [ { "protected": "...", "signature": "..." }, + { "protected": "...", "signature": "..." } ] +} + ]]> +
+ + Digests are elided here; the reference repository's values are in + . The parameters object is shown + elided on purpose: nothing in this document depends on what is in + it. + +
Hash Commitments and Content Conveyance + Every URI carried inside hash-committed content MUST be accompanied + by a sibling hash over the dereferenced bytes. The -00 revision + committed harness_uri as a string while leaving the bytes at + that URI uncommitted, which permitted a Buyer to substitute the + acceptance instrument after signature, run the substituted + instrument, and submit the failure as a valid fraud proof. The -01 + revision stated the rule and its own reference TaskSpec broke it for + three of four URIs; this revision's example carries all four + sibling hashes, and the validator checks each. + Where the committed content is a bundle of files rather than a + single octet stream, the commitment MUST be computed as + SHA-256(JCS(M)) where M is an object mapping each file's + path, relative to the bundle root and expressed with "/" separators, + to SHA-256 of its bytes, over every file in the bundle. A + manifest of per-file digests is specified rather than an archive + digest because archive formats carry ordering, timestamp and + permission metadata that is not stable across producers. The same + construction commits to a terms profile (). +
+ +
The Task Specification + The content committed by spec_hash is a TaskSpec: a JSON + object with the members in , + canonicalized per before hashing. It is not + transmitted over the endpoints of this document; the parties exchange + it before signing, and spec_uri may say where. + + The acceptance object MUST carry the members required for + the contract's tier: harness_uri and harness_hash + for re-execution tiers, enclave and model policy for attestation + tiers, a proof statement with its verifying key for proving tiers, or + rubric_uri and rubric_hash for judgment tiers. An + empty acceptance object MUST be rejected. The -00 revision's + schema permitted one, which made every fraud proof impossible. + + Thresholds MUST be stated so that they cannot be satisfied by + returning almost nothing. A threshold expressed only as a rate over + returned rows is satisfied by returning one correct row out of + millions; a completeness condition relative to the committed input is + therefore required wherever the deliverable is a transformation of + that input. +
+ +
Terms + The terms member names the settlement terms both parties + signed, by reference. It carries a profile URI, a + profile_hash, and a parameters object. This document + defines no obligation between parties and takes no position on the + legal effect of any object it defines; what the named profile says the + parties have agreed to, and what any of it means between them, is the + profile's and its authors' to say. + + profile_hash is the manifest digest of + over the profile's bundle. A bundle usable + with this document contains at least three files: the profile's prose, + parameters.schema.json, a JSON Schema + for the parameters object, and + vectors.json, whose form + gives. A digest over prose alone would commit the parties to bytes and + not to behaviour; the schema and the vectors are what make two + implementations of the profile checkable against each other. + + A Facilitator MUST refuse a contract whose terms.profile + and terms.profile_hash do not match an entry in the + terms_profiles array of its own capability document + (), so that no party signs terms the + Facilitator will not evaluate, and MUST refuse a contract whose + parameters do not validate against the named profile's + parameters.schema.json. It reads parameters for no + other purpose. The rule of that an + undefined member is rejected does not apply inside + parameters; the profile's schema governs there. + + A profile usable with this document defines, in its prose, a + schedule: a total, deterministic function from a contract and a trace + prefix () to the list of entries the profile + emits at the last event of that prefix, in the form of + . Total means every event in + has a defined result, including the ones a + profile author would rather not think about: a lapsed dispute, an + unresolved child, a contract abandoned before it was funded. + Deterministic means the result depends on the contract, the trace and + nothing else, so that any party holding those can recompute it. The + prose also names the accounts the schedule uses and how each one's + opening amount is computed from the contract. This document does not + register profiles and defines none normatively; + carries one for the experiment. + + Everything the -01 revision said in its Section 5.3, and everything + it said in its Section 7 about what is posted, released, + returned or forfeited and when, is now the content of a profile. The + member that carried those figures inside the contract is gone; the + figures a profile needs are in parameters, and the -01 + figures in particular are the parameters of the profile in + . +
+
+ +
The Delivery Record + The Delivery is the record a contract is judged against. It is a + JSON object, media type application/vnd.pact.delivery+json, + with the members in , signed once by the + Seller. + + A Facilitator MUST refuse a Delivery, with the problem type named, + when: its vtc_hash does not match the contract + (object-conflict); the contract is not in FUNDED + (wrong-state); its signature does not verify against a key + bound to parties.seller (signature-invalid, + unexpected-signer); its evidence member is absent or + does not conform to the verification profile named in the contract + (evidence-nonconformant); or input_hash is absent + where the tier re-executes (evidence-nonconformant). A refused + Delivery is recorded in no trace; the contract stays in FUNDED and a + conformant Delivery may follow before the deadline. The -01 revision + treated a nonconformant Delivery as a FAIL Verdict, which decided a + question about value inside a rule about shape; the consequence of a + Seller reaching the deadline with nothing conformant recorded is now + the deadline-passed event, and what that event costs anyone is + the profile's. + + Conformance of evidence is a check on shape, not on + substance: the Facilitator confirms that the members the profile + requires are present and well formed, and nothing about whether the + work is any good. That is why the check stays on the right side of the line + drawn in . Where task.deadline passes + with no delivered entry, the Facilitator records + deadline-passed (). No window opens, + because there is nothing to challenge. + +
+ A Delivery for a T0-reexec contract, acceptance profile + ", + "work_hash": "sha256:9c1f...", + "work_uri": "https://cdn.dataforge.example/o/9c1f", + "input_hash": "sha256:41ab...", + "evidence": { + "profile": "acceptance", + "instrument_hash":"sha256:", + "results_hash": "sha256:7e02...", + "results_uri": "https://cdn.dataforge.example/o/7e02" + }, + "signature": { "protected": "...", "signature": "..." } +} + ]]> +
+ + The -01 revision said that a Buyer countersignature over the + Delivery constituted a receipt. The Delivery's signing member is a + single object, so no second signature could be carried, and the + sentence is withdrawn. A Buyer that wants a record of receipt has one: + the Status the Facilitator returns for the Delivery carries the + delivered entry and the Facilitator's signature over it. +
+ +
Verdicts, Challenges and the Window + +
Flows + The flow member selects one of three shapes for the state + machine of . A conformant Facilitator MUST + implement verdict-first; the others are OPTIONAL, and a + Facilitator MUST refuse a contract naming a flow it does not advertise + (flow-unsupported). +
+
verdict-first:
A Verdict is recorded before the + window opens. The window opens on a PASS Verdict or on + verdict-lapsed; a FAIL Verdict ends the contract without a + window.
+
delivery-first:
The window opens at + delivered. A Verdict MAY be recorded inside the window + without a Challenge; a FAIL ends the contract, a PASS changes + nothing.
+
no-window:
No window opens and no Verdict is + accepted; delivered is followed by the terminal + path.
+
+ The -01 revision had four release modes, named for when value + moved. Two of them, on-window and optimistic, + produce the same trace and differed only in which event a profile + acts on, which is a profile parameter and not a protocol matter. The + mapping is in . + + The window opens at the instant of the window-opened entry + and closes at that instant plus challenge.window_seconds, + carried in the entry as closes_at. A Facilitator MUST NOT + accept a Challenge after closes_at, MUST NOT extend the + window for any reason, and MUST NOT record window-closed + while a Challenge is pending. +
+ +
Verdicts + A Verdict is a signed statement that a Delivery was evaluated + against the committed instrument, and with what outcome. It is a JSON + object, media type application/vnd.pact.verdict+json, with + the members in , signed once. + +
+ A Verdict + ", + "outcome": "PASS", + "profile": "acceptance", + "instrument_hash": "sha256:", + "results_hash": "sha256:7e02...", + "evaluated_at": "2026-11-10T09:14:22Z", + "signature": { "protected": "...", "signature": "..." } +} + ]]> +
+ + The Verifier is the party identified by the kid of the + Verdict's signature. Where the contract names + parties.verifier, a Facilitator MUST refuse a Verdict signed + by any other party; otherwise it MUST refuse a Verdict whose signer + does not satisfy + (verifier-not-independent). It MUST refuse a Verdict for a + contract with no delivered entry + (no-recorded-delivery); one whose delivery_hash + does not match that entry, or whose profile or + instrument_hash does not match the contract + (verdict-nonconformant); one received in a state the table + in does not list for it, or under the + no-window flow (wrong-state); and one carrying + challenge_hash that names no pending Challenge, or omitting + it while the contract is DISPUTED (verdict-nonconformant). + A Verdict that answers a Challenge supersedes the Verdict that stood + before it, and both stay in the trace. + + A Verdict commits to the instrument it ran and to the results it + produced. Without instrument_hash a Verifier could run + something other than the committed instrument and the contract would + have no way to tell; that is the substitution attack of + , arriving from the verification + side. + + Under verdict-first a Verifier that never answers would + leave a contract in DELIVERED forever, and the -01 revision had no + rule for it. verification.max_verdict_seconds bounds the + wait: when it passes with no Verdict, the Facilitator records + verdict-lapsed and opens the window, so that the contract can + still be challenged and can still end. What a lapsed Verdict costs + anyone is the profile's. +
+ +
Challenges + A Challenge is a JSON object, media type + application/vnd.pact.challenge+json, with the members in + , by which a party submits a fraud + proof inside the window. A Facilitator MUST refuse a Challenge + received when the contract is not in WINDOW_OPEN or DISPUTED, or + after closes_at (challenge-window-closed); one + whose delivery_hash does not match the delivered + entry (object-conflict); one whose proof does not + conform to the verification profile (proof-nonconformant); + one whose signer it cannot resolve (signature-invalid); and + one signed by the contract's Seller (unexpected-signer), + since a performer's statement against its own Delivery is not a + fraud proof and the -01 revision left the case open. A Facilitator + MUST NOT refuse a Challenge on the ground that its signer is the + contract's Buyer. + + A Challenge that is accepted is evaluated by a party satisfying + , whose finding is a Verdict carrying + challenge_hash; the Challenger's own assertion is not a + finding. The Challenger is the party identified by the kid of + the Challenge's signature. + + A Facilitator MAY require that a Challenge be accompanied by a + deposit in the amount its capability document advertises as + challenge_deposit. How a deposit is posted is the settlement + binding's, what becomes of it is the terms profile's, and this + document says nothing further about it. + discusses what a deposit does and does not prevent. + +
+ A Challenge under the acceptance profile + ", + "proof": { + "profile": "acceptance", + "instrument_hash": "sha256:", + "results_hash": "sha256:a91e...", + "results_uri": "https://watch.example/o/a91e", + "failing_checks": ["schema_valid_rate", "row_count_min"] + }, + "costs": { "amount": "1.20", "currency": "USDC" }, + "signature": { "protected": "...", "signature": "..." } +} + ]]> +
+
+ +
Disputes and Lapses + A contract with a pending Challenge is DISPUTED. It leaves that + state when a Verdict answers the Challenge, or when + challenge.max_dispute_seconds pass with none and the + Facilitator records dispute-lapsed. A lapsed Challenge + changes no Verdict: the Verdict that stood before it stands after it. + A Facilitator MAY accept further Challenges while DISPUTED, each of + which is pending on its own account, and MUST NOT record + window-closed until none is pending. + +
+ The dispute path: a Challenge answered by a FAIL Verdict + | + | | [ challenge ] | + | |-- Challenge + Delivery -------->| + | |<-- POST Verdict | | + | | [ verdict FAIL, answers, | + | | supersedes ] | + | | [ children-final ] | + | | [ terminal SETTLED, | + | | challenge_upheld true ] | + |<- Outcome ---| | + ]]> +
+ + The figure carries no rank, no waterfall and no amount. The -01 + revision drew five numbered transfers on this diagram; every one of + them is now a line in a profile's schedule, keyed to the + terminal entry, and reported in terms_result. +
+
+ +
Facilitator Capability Discovery + Before a Buyer and Seller can co-sign a VTC they must agree on a + Facilitator and know what it implements. This document registers one + well-known URI for that purpose, per . + + This is deliberately narrower than agent discovery, which is the + subject of separate work and is not restated here. What is discovered + is one service's capabilities, not an agent's identity, skills or + endpoints. + + A Facilitator SHOULD publish a JSON document, media type + application/vnd.pact.facilitator+json, with the members in + , at the path + /.well-known/pact-facilitator of its origin. The document MUST + be served over HTTPS. It MUST be signed, and the signature MUST verify + against a key bound to the identifier in facilitator. An + unsigned capability document is not usable for contract formation, + because terms_profiles determines which terms a party can name + and expect to be evaluated. + +
+ https://settle.example/.well-known/pact-facilitator + " } + ], + "max_contract_value": { "amount": "50000.00", + "currency": "USDC" }, + "endpoints": { + "contract": "https://settle.example/pact/v2/contracts", + "delivery": "https://settle.example/pact/v2/deliveries", + "verdict": "https://settle.example/pact/v2/verdicts", + "challenge": "https://settle.example/pact/v2/challenges", + "outcome": "https://settle.example/pact/v2/outcomes" + }, + "signature": { "protected": "...", "signature": "..." } +} + ]]> +
+ + A client MUST NOT infer any capability from the absence of a member. + A Facilitator that does not publish a capability document can still be + named in a VTC by prior arrangement; discovery is a convenience, not a + precondition. A Facilitator MUST NOT list a terms profile whose + vectors () its own implementation does not + reproduce. +
+ +
Verification Profiles + A contract names both a tier, which says what class of + evidence is produced, and a profile, which says what is + actually done to check it. Four tier labels are used in this document: + T0-reexec, deterministic re-execution; T1-tee, + hardware attestation per ; T2-zkml, a + proof of inference; and T3-jury, staked arbitration. Tiers are + a vocabulary. Three profiles are defined below by name; any other is + identified by a URI under its definer's control, and this document + creates no registry for them. The distinction matters because the tier + name does not determine how much checking a contract gets and the + profile largely does. + + Consider one task, a bulk data transformation, under two profiles at + the same nominal tier. Re-executing the whole computation and comparing + outputs costs approximately what performing it cost. Running a committed + acceptance instrument against the delivered artifact costs a small + fraction of a percent. Those two differ by more than two orders of + magnitude in what checking costs relative to the price. A terms profile + may make that ratio matter; this document requires only that a + verification profile state an order-of-magnitude estimate of its cost + relative to the work, since a figure nobody can estimate is a figure + nobody can use. + + Implementations SHOULD select the cheapest profile that detects the + failures they actually care about, rather than the strongest-sounding + one. A committed acceptance instrument that is adequate is worth more + than a re-execution profile that nobody can afford to run. + +
+
acceptance:
Run the instrument committed by + criteria_hash against the Delivery. The fraud proof is a + failing evaluation. Deterministic by construction, since the + instrument is fixed before work begins. Cost: a small fraction of a + percent of the work for a data transformation.
+
bisection:
Interactive narrowing to a single + disputed step, which is then checked directly. Cost grows + logarithmically in the size of the computation rather than + linearly.
+
full-reexec:
Re-execute and compare byte for + byte. Sound only where the computation is deterministic and the + environment is pinned; see . Cost: + approximately the work.
+
+ +
Verifier Independence and Identifier Normalization + Independence is a relation between the party that signs a Verdict + and the parties to the contract. It MUST be derived by the evaluator + and MUST NOT be satisfied by a field in which a record declares + itself independent. A Facilitator MUST refuse a Verdict whose signer + is, after normalization, the contract's Buyer, Seller or Facilitator, + and MUST refuse a contract whose parties.verifier is any of + those three (verifier-not-independent). The last case is the + rule the -01 revision stated as a prohibition on the Facilitator's + conduct; it is an identifier comparison and is stated as one. + + Party identifiers MUST be normalized before comparison, and the + normalization MUST fold toward identifying the same party: strip + leading and trailing whitespace; lower-case the scheme and, for + did:web and https identifiers, the host; remove any + fragment (a "#" and everything after it) and any trailing "/" or + ".". The path of a did:web identifier is case sensitive and + MUST NOT be folded. Percent-encoding MUST NOT be decoded, since an + open-ended decoder is its own attack surface. An identifier that does + not parse after normalization is not evaluable and MUST NOT be + treated as outside the parties. An independence claim reaches + exactly as far as the record's own commitments. +
+
+ +
Contract Trees + An agent that accepts work may subcontract part of it. The + subcontract is an ordinary PACT contract whose Buyer is the parent's + Seller. What this section adds is the binding between the two, in both + directions and across Facilitators, so that a parent's Outcome Record + can commit to its children's and a reader of the parent's record can + find and check them. + +
+ A contract tree. B is Seller above and Buyer below. + +
+ +
Binding a Child to Its Parent + A subcontract carries parent, a top-level member with the + parent's vtc_id, vtc_hash and + facilitator. Because parent is inside the bytes both + parties sign, the child's Buyer signature is itself the authorisation + to attach that child to that parent. The -01 revision carried this + member inside the member it has since removed; it is structural and + is now where structure is. + + The child's Facilitator need not resolve the parent, and across + Facilitators it often cannot. It MUST record parent as + signed, and MUST allow the identifier in parent.facilitator + to retrieve the child's Status and Outcome Record + (). The check that the child's Buyer is + the parent's Seller is made where the parent is: at registration. + + The -01 revision required a Facilitator to reject a child whose + parent chain contained the child's own identifier and to enforce a + maximum depth. Neither rule survives, because neither is needed. A + child commits to its parent's digest, and the parent's digest exists + before the child is signed, so no contract can commit to a descendant + and a cycle cannot be formed; depth is bounded by whatever a + Facilitator is willing to register, and no Facilitator sees more + than one level. +
+ +
Registration and Children Final + The parent's Facilitator learns of a child when the parent's Seller + registers it: a POST of the child's co-signed contract to the + parent's contract resource (). The + registering party is the child's Buyer, which is why it holds the + child's contract and why it is authorised: it is a party to both. + + A Facilitator MUST refuse a registration, with the problem type + named, when: the body is not a valid contract + (); its parent.vtc_hash is not the + parent's digest or its parent.facilitator is not this + Facilitator (parent-unresolvable); its + parties.buyer is not the parent's parties.seller + after normalization (parent-unresolvable); its latest + finality instant is not earlier than the parent's + (, finality-ordering-violation); + or the parent is terminal (wrong-state). An accepted + registration is recorded as child-registered. + + + + Without the first check any party may name any contract as its + parent. The attack is cheap and asymmetric: name a competitor's + contract as parent, subcontract a trivial task to yourself, fail it, + and put a failed child under the competitor's record. The -00 + revision carried the parent as a bare string with no hash and no + check, so the attack cost one signature. + + A child becomes final for its parent when the parent's Facilitator + holds the child's Outcome Record. It may obtain that record itself, + by retrieving it from the child's Facilitator, or receive it from the + parent's Seller by a POST to the same resource + (). Either way the Facilitator MUST verify + the record's Facilitator signature against a key bound to the + identifier the registration recorded, and MUST verify that its + vtc_hash is the registered child's digest, before recording + child-final. Where the child's latest finality instant passes + with no record held, the Facilitator records + child-unresolved. children-final follows when every + registered child has one entry or the other, and the parent's + terminal entry follows that. + + A child that is never registered does not exist for the parent. + Nothing in this document compels a parent's Seller to register a + child, and says what that means. +
+ +
Finality Is Bottom-Up + A parent's Outcome Record MUST carry + children_merkle_root over the Outcome Records of its + registered children (), so a parent cannot be + recorded until its children have been, and the parent waits in + AWAITING_CHILDREN until they are. For that wait to be bounded, every + child must be able to reach a terminal state, or be declared + unresolved, before its parent needs it. + + The latest finality instant L of a contract is computed from its + own members and nothing else: + + + + Every wait in is bounded by one of + those members, and a Challenge can only be received before + closes_at, so no sequence of events carries a contract past + its L except waiting for its own children. A Facilitator MUST refuse + to register a child unless L(child) is earlier than L(parent), and + MUST record child-unresolved for a registered child no later + than the first opportunity after L(child) if it holds no Outcome + Record for it by then. + + The -01 revision compared the child's latest finality with the + parent's earliest window close, and bounded neither: under its + default mode the first Verdict could take forever, so the inequality + guaranteed nothing. max_verdict_seconds is what makes L + finite, and the waiting state is what makes the rule honest about + the case where a child is late anyway. + +
+ Bottom-up finality + +
+ + What a child's outcome means for its parent is not stated here. + No entry in a parent's schedule depends on any child's outcome unless + the named terms profile says so; what this document guarantees is + that the parent's Outcome Record commits to whichever child records + exist when it is issued and names, in its trace, every child that + does not. +
+
+ +
The Contract Status + A Contract Status is a JSON object, media type + application/vnd.pact.status+json, with the members in + , signed once by the Facilitator. It is the + body of every successful response to a POST in + and of a GET on a contract resource. It + carries the contract's state and the trace recorded so far. + +
+ A Contract Status after the Verdict of Figure 1 + ", + "state": "WINDOW_OPEN", + "trace": [ + { "event": "accepted", "at": "2026-11-01T10:00:00Z", + "object": "sha256:" }, + { "event": "funded", "at": "2026-11-01T10:00:00Z" }, + { "event": "delivered", "at": "2026-11-10T08:30:12Z", + "object": "sha256:" }, + { "event": "verdict", "at": "2026-11-10T09:14:30Z", + "object": "sha256:", "outcome": "PASS" }, + { "event": "window-opened", "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" } + ], + "issued_at": "2026-11-10T09:14:30Z", + "signature": { "protected": "...", "signature": "..." } +} + ]]> +
+ + Two rules make a Status worth keeping. A Facilitator MUST issue a + Status for every request it accepts, carrying the entry that request + caused, so that the requester holds a signed receipt of what was + recorded and when. And the trace in every Status a Facilitator issues + for a contract MUST be a prefix of the trace in every later one; two + Statuses for one contract that violate that are evidence of + equivocation, and says what a holder + can do with it. The Outcome Record's trace is the last such + sequence. + + The -01 revision returned the posted object with a state + member added to it, which no schema admitted and no signature covered. + The Status replaces that: the posted object is not echoed, and + everything in the response is inside the Facilitator's signature. +
+ +
Outcome Records + An Outcome Record records what a contract did. It is a JSON object, + media type application/vnd.pact.outcome+json, with the members + in . It is the input to any reputation + system built on PACT, though this document defines no such system and + takes no position on how the records should be weighed. + + A Facilitator MUST issue exactly one Outcome Record for every + contract that reaches a terminal state, including SETTLED and + ABANDONED, MUST sign it, and MUST NOT require the signature of any + other party on it. The -00 revision's record needed the signature of + the party it recorded against, which made a reputation layer built on + it structurally incapable of recording a loss. The Facilitator + signature is what makes the record evidence: without it the record is + a claim by interested parties about themselves, and with it a + fabricated history requires a Facilitator's key rather than two + identities. + +
+ An Outcome Record for a contract that reached SETTLED on an upheld Challenge + ", + "parties": { + "buyer": "did:web:acme.example", + "seller": "did:web:dataforge.example", + "facilitator": "did:web:settle.example", + "verifier": "did:web:audit.example" + }, + "outcome": { "state": "SETTLED", "challenge_upheld": true }, + "work_hash": "sha256:9c1f...", + "trace": [ + { "event": "accepted", "at": "...", + "object": "sha256:" }, + { "event": "funded", "at": "..." }, + { "event": "delivered", "at": "...", + "object": "sha256:" }, + { "event": "verdict", "at": "...", + "object": "sha256:", "outcome": "PASS" }, + { "event": "window-opened", "at": "...", "closes_at": "..." }, + { "event": "challenge", "at": "...", + "object": "sha256:" }, + { "event": "verdict", "at": "...", + "object": "sha256:", "outcome": "FAIL", + "answers": "sha256:", + "supersedes": "sha256:" }, + { "event": "children-final", "at": "..." }, + { "event": "terminal", "at": "...", "state": "SETTLED", + "challenge_upheld": true } + ], + "terms_result": { + "profile": + "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "profile_hash": "sha256:", + "currency": "USDC", + "transfers": [ + { "event": 8, "from": "...", "to": "...", "amount": "...", + "code": "..." } + ] + }, + "signatures": [ { "protected": "...", "signature": "..." } ] +} + ]]> +
+ + The record carries one signature, the Facilitator's. The Seller did + not consent to this record and its consent is not required. The + transfers entries are elided here because their content is the + profile's; shows them filled in for + its own profile. + +
The Terms Result + terms_result reports what the named profile's schedule + produced over the whole trace. It carries the profile identifier and + hash copied from the contract, the currency, and transfers: + an array of entries, in the order the schedule produced them, each + with event (the zero-based index of the trace entry at which + the schedule emitted it), from and to (account + names as the profile defines them), amount, and + code (a string the profile defines, naming the schedule + line that produced the entry). + + This document defines the form of the list and two arithmetic + facts about it, and nothing about what any entry means. Over the + accounts and opening amounts the profile declares for the contract + (): no entry takes from an account more than + that account holds at that point in the list; and after the last + entry every account the profile marks internal holds zero. A + Facilitator MUST NOT sign an Outcome Record whose list breaks either + fact, and MUST NOT sign one whose list differs from what the + profile's schedule produces for the record's own trace. Any party + holding the contract, the trace and the profile's bundle can + recompute the list; that is the property the experiment in + depends on. + + vectors.json in a profile's bundle is an array of + objects, each with name, contract (a VTC, or the + members of one the schedule reads), trace (a complete + trace), and transfers (the list the schedule produces for + it). A Facilitator MUST reproduce every vector of a profile before + listing that profile in its capability document + (), which is the only conformance + requirement this document places on a profile implementation. +
+ +
The Children Merkle Root + Let D be the list of 32-byte SHA-256 digests of the Outcome Record + of each registered child for which the Facilitator holds one, each + computed over the record's canonical form including its + signatures member, sorted ascending as byte strings. + children_merkle_root is MTH(D) exactly as defined in + Section 2.1.1, with SHA-256 as the hash: a + leaf is SHA-256(0x00 || d), an interior node is SHA-256(0x01 || left + || right), and for n greater than one the list is split at k, the + largest power of two smaller than n. The shape is therefore fixed by + n alone, and two implementations that agree on D agree on the + root. + The domain separation is not optional. Without distinct prefixes an + attacker can present an interior node as though it were a leaf, and so + claim an inclusion proof for a subtree that never existed. + The member is present when at least one child is registered and + absent otherwise; it MUST NOT be present with an empty or zero value, + which would be indistinguishable from a tree whose children were + withheld. Where every registered child is unresolved D is empty and + the root is MTH of the empty list, SHA-256 of the empty string; the + child-unresolved entries in the trace say which records the + root does not cover. The -01 revision computed leaves over records + with their signatures removed, which let a record be re-signed + without changing the root. +
+
+ +
Protocol Endpoints + This section specifies the operations a Facilitator exposes. Base + URIs are not fixed by this document; they are discovered from the + endpoints member of the capability document + (), so a Facilitator may mount them anywhere + on its origin. + +
+
Propose a contract:
POST {contract}; body, a + contract; 201 with a Status.
+
Retrieve a contract's status:
GET + {contract}/{id}; 200 with a Status.
+
Register a child:
POST + {contract}/{id}/children; body, the child's contract; 201 + with a Status.
+
Supply a child's outcome:
POST + {contract}/{id}/children/{child_id}; body, the child's + Outcome Record; 200 with a Status.
+
Submit a Delivery:
POST {delivery}; body, a + Delivery; 202 with a Status.
+
Record a Verdict:
POST {verdict}; body, a + Verdict; 201 with a Status.
+
Open a Challenge:
POST {challenge}; body, a + Challenge; 202 with a Status.
+
Retrieve an Outcome Record:
GET + {outcome}/{id}; 200 with the Outcome Record.
+
+ + All requests and responses use the media types defined in + . All requests MUST be made over HTTPS, following + the recommendations of . Status codes are as + defined in . A Delivery and a Challenge are + answered 202 (Accepted) rather than 201 because + acceptance of the bytes is not acceptance of the work; what follows + depends on a Verdict the Facilitator does not itself produce. + + A Facilitator authenticates the sender of a POST by the signature on + the body, and by nothing else in this document: it MUST reject a + Delivery not signed by the contract's Seller, a Verdict not signed by a + party admissible under , a Challenge whose + signer it cannot resolve, and a child registration or child outcome + whose body does not verify as + requires. A Facilitator MAY require an HTTP-layer authentication in + addition. Retrieval is discussed in . + +
Proposing a Contract + The request body is a VTC carrying the signatures of both parties + required to sign it. A Facilitator MUST perform the checks in + , and + before creating the resource, MUST + refuse a contract whose parties.facilitator is not itself or + whose price.settlement, network or asset it does not + advertise (facilitator-mismatch, + settlement-unsupported), and MUST refuse otherwise with the + problem type that names the rule. + + + + +
+ +
Idempotency + Every object this protocol carries is committed by the digest of + its own canonical form, so no separate idempotency key is needed and + none is defined; the general mechanism of + solves a + problem this protocol does not have. A Facilitator MUST treat a POST + whose body has a digest it has already accepted as a request for the + existing resource, and MUST respond 200 (OK) with the + current Status rather than creating a second resource or reporting a + conflict. + + Where a POST carries the same object id as an existing + resource but a different digest, the Facilitator MUST respond + 409 (Conflict) (object-conflict). Retrying a + submission is therefore always safe, and altering one never is. +
+ +
Error Responses + A Facilitator MUST report failures using + problem details, media type + application/problem+json, with a type from + for a rule in this document, or from + the profile's own namespace for a rule in a terms profile. A problem + arising from a rule in this document MUST carry section, the + number of the section stating the rule. A problem arising from a + rule in a terms profile MUST carry profile and + profile_section instead, since section cannot name + a rule outside this document. Error responses name the rule that was + violated, because a conformance failure a caller cannot locate is a + failure of the specification. + + +
+ +
Exchange +
+ HTTP exchange for the flow in Figure 1 + | | + |<-- 201 Status --------| | + | | | + |-- POST {delivery} --->| | + |<-- 202 Status --------| | + | | | + | |-- GET work_uri ----->| + | |<-- POST {verdict} ---| + | |-- 201 Status ------->| + | | | + |-- GET {contract}/id ->| | + |<-- 200 Status --------| | + | | | + |-- GET {outcome}/id -->| | + |<-- 200 Outcome -------| | + ]]> +
+
+
+ +
Conformance + Every rule a PACT conformance checker enforces is stated in this + document as normative text. This section collects the rules that a + schema language cannot express, so that an implementation built from + this document alone passes a conformance suite built from it. A rule + that lives only in a test suite is not a requirement, and an implementer + who cannot find it in the specification will not implement it. + +
Signatures + Every signature carried by a VTC, Delivery, Verdict, Challenge, + Status, Outcome Record or capability document is a JWS + in the General JSON Serialization of + Section 7.2.1 of that document, with the payload detached as its + Appendix F describes. The payload is BASE64URL of the JCS-canonical + bytes of the object with the signing member removed, so the JWS + Signing Input is ASCII(BASE64URL(UTF8(protected)) || "." || + BASE64URL(JCS(object))) exactly as Section 5.1 of + defines it. The payload is never + transmitted; a verifier reconstructs it from the object it holds, and + verifies over the protected header exactly as transmitted, never + over a header it re-serialized. The following constraints apply. +
    +
  • The protected header MUST carry alg, kid and + typ.
  • +
  • alg MUST be ES256 or ES384 + , or EdDSA + with an Ed25519 key; a verifier MAY also accept Ed448. A verifier + MUST reject any other value, and MUST reject none. Absent + an allowlist an attacker selects the algorithm, which permits both + unsigned acceptance and confusion of a public key for a symmetric + secret.
  • +
  • kid MUST appear inside the protected header and MUST + NOT be carried as a sibling of it. A key identifier outside the + signed bytes is rewritable in transit, which allows an attacker who + can publish a key document to re-attribute a genuine signature to + itself.
  • +
  • typ MUST be the full media type of the object signed, + including the application/ prefix, so that a signature + over one object type cannot be replayed as a signature over + another. Section 4.1.9 of recommends + omitting the prefix; this document requires the full form so that + typ equals the registered media type character for + character. Explicit typing follows Section 3.11 of + .
  • +
  • A signatures array MUST be sorted by the normalized + kid of its entries (), ties + broken by the unnormalized kid, both compared as + sequences of Unicode code points; a verifier MUST reject an + unsorted array (signatures-unordered). Two clients that + each attach their own entry and exchange the object would + otherwise produce two arrays, and since the digest covers the + array, two digests for one agreement.
  • +
  • An ECDSA signature MUST have its s value in the low + half of the curve order, that is s at most n/2 for the + order n of the curve , and a verifier + MUST reject one that does not. fixes the + encoding and not which of the two valid s values is + accepted; accepting both lets anyone holding a valid signature + produce a second one over the same bytes without the key, and a + second signature is a second digest. EdDSA verification per + already rejects a non-canonical + S, so the rule is stated for ECDSA only.
  • +
+ +
Key Resolution + A kid is a URI naming a public key. A verifier MUST + resolve it as follows, and MUST reject a signature whose + kid it cannot resolve. +
    +
  • A did: identifier is a DID URL + . The verifier resolves the DID document + by the method the identifier names and selects the verification + method its fragment identifies. Examples in this document use + did:web ; no method is + required or excluded.
  • +
  • An https: identifier dereferences, over TLS, to a + JWK Set ; the verifier selects the key + whose kid member equals the fragment.
  • +
+ The part of a kid before its fragment MUST equal, after + the normalization in , the party + identifier the signature is attributed to. Verifying a signature + establishes that the holder of that key signed; that the key + belongs to the party is a property of the identity method, and this + document does not add to it. An identity system for agents defined + elsewhere, such as , is used by + naming its identifiers here and resolving them by its rules. +
+
+ +
Rules Not Expressible in a Schema +
    +
  • parties.buyer and parties.seller MUST be + distinct after the normalization in + (parties-not-distinct).
  • +
  • A contract MUST carry exactly one verifying signature whose + kid covers parties.buyer, exactly one whose + kid covers parties.seller, and no other + (signature-missing, unexpected-signer). A count + of signatures is not sufficient: two signatures covering one + identifier MUST be rejected.
  • +
  • challenge.window_seconds MUST be greater than zero, + and task.deadline MUST be later than the instant of + acceptance (deadline-invalid).
  • +
  • Every URI member inside hash-committed content MUST have a + sibling hash member, and a validator MUST reject content carrying + harness_uri, rubric_uri, schema_uri or + sample_uri without its hash.
  • +
  • An acceptance object MUST carry the members required + for the contract's tier. An empty acceptance object MUST be + rejected.
  • +
  • terms.profile and terms.profile_hash MUST + match an entry the Facilitator advertises, and + terms.parameters MUST validate against that profile's + schema (terms-unsupported, + terms-parameters-invalid).
  • +
  • Every amount MUST have the form in + (amount-invalid), and every + object MUST validate against the schema published for its media + type (schema-invalid).
  • +
+
+ +
Test Vectors + Each rule above has an accepting and a rejecting form. A + conformance suite built from this section alone, with no reference to + any implementation, should reach the same verdicts. Rejecting vectors + name the rule they violate. + + + Conformance vectors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDMutation from a valid objectExpect
V-01unmodified valid VTCaccept
V-02alg set to nonereject
V-03alg set to HS256reject
V-04kid moved outside the protected + headerreject
V-05typ of a Delivery on a VTC + signaturereject
V-06buyer and seller set to the same + identifierreject
V-07buyer and seller differing only by trailing + "/"reject
V-08two signatures, both from the buyerreject
V-09window_seconds of 0reject
V-10acceptance as an empty objectreject
V-11harness_uri with + harness_hash removedreject
V-12terms.profile_hash not advertised + by the Facilitatorreject
V-13terms.parameters failing the + profile's schemareject
V-14Delivery with evidence absentreject, no entry
V-15child whose buyer is not the parent's + sellerreject
V-16child with L(child) not earlier than + L(parent)reject
V-17Verdict signed by the sellerreject
V-18object keys ordered by code point, with a + supplementary-plane keydigest mismatch
V-19buyer and seller differing only in the + case of a did:web pathaccept
V-20object carrying a member this document + does not define for itreject
V-21signatures not sorted by + normalized kidreject
V-22ECDSA signature with s above + n/2reject
V-23Verdict with delivery_hash computed + over the Delivery without its signaturereject
V-24Outcome Record whose transfers + overdraw an account of the profilereject
+ + V-07 and V-18 are the two most often got wrong. V-07 fails wherever + party comparison is a string equality on unnormalized identifiers. + V-18 fails wherever canonicalization sorts keys by Unicode code point, + which agrees with the required UTF-16 order for every ASCII key and so + passes every vector an implementer would think to write. V-19 is the + opposite mistake, folding more than + allows, and the -01 reference validator made it. +
+
+ +
Worked Example + The tables and digests below are the reference repository's, at the + tag named in . The object figures in earlier + sections use short illustrative identifiers for page width; the + repository examples carry the full ones, and the digests here are + computed over those. The figures that the -01 revision printed here + about a bond and a required detection rate are now the profile's, and + carries them. + + A buyer commissions a data transformation at a price of 180.00 USDC + under the verdict-first flow, the acceptance + verification profile, and the terms profile of + with the parameters shown there. The + digests carried by the reference TaskSpec, contract and profile + are: + + + criteria_hash sha256: + profile_hash sha256: + vtc_hash sha256: + delivery_hash sha256: + ]]> + + criteria_hash is the manifest digest of + over the acceptance instrument bundle, and the + same value appears as acceptance.harness_hash inside the + TaskSpec, so the instrument is committed both by the contract and from + within the specification it belongs to. profile_hash is the + same construction over the profile bundle. vtc_hash is the + digest of the signed contract, and delivery_hash of the signed + Delivery, both per . + + Every value above changed from the -01 revision, for four reasons + that are each recorded so that a reader comparing the two documents can + account for the difference: spec_hash because the TaskSpec + now carries the sibling hashes always required; + vtc_hash because the contract's members changed + () and because spec_hash did; + delivery_hash because it now covers the Delivery's signature; + and profile_hash because it did not exist. + + The trace the reference implementation records for this contract + on the path of , and on the dispute path of + , together with the transfer lists the + profile produces for each, are the vectors in the profile's bundle, + and prints them. +
+ +
Implementation Status + This section records the status of known implementations of this + document per , and is to be removed before + publication as an RFC. + One implementation is known to the author, and the author wrote it: + https://github.com/pact-spec/spec, under the Revised BSD licence. At + tag v0.2.0 it comprises the object schemas, the examples whose digests + prints, a conformance validator that runs + <NN> checks including every vector of + , a Facilitator serving the endpoints of + with the profile of + , and clients for the other roles. Its + previous tag, v0.1.0, implemented the -01 revision and is the source + of the measurements the author has published about it. No second + implementation exists, so nothing in has + been tested, and this document claims no interoperability. +
+ +
Security Considerations + Most of what follows was found by adversarial review of earlier + revisions rather than anticipated when they were written. Each + subsection states the attack, why it worked, and the requirement in + this document that closes it. Where a threat is only mitigated rather + than closed, that is said. The table first: for each party, what the + protocol enforces against it, what it records about it, and who can + check the record without trusting the Facilitator. + + + What the protocol enforces, records and lets others check, by party + + + + + + + + + + + + + + + + + + + + + + +
PartyEnforced against itRecorded about itCheckable by
Buyercannot alter the task, instrument or terms after signing; + cannot attach a child to a contract it is not party toits signature on the contract; any Challenge it + signsanyone holding the contract
Sellercannot deliver against a substituted instrument or input; + cannot judge its own Delivery; cannot re-sign a record without + changing every digest over itits signature on the contract and the Delivery; the + Verdicts and Challenges on its Deliveryanyone holding the contract and the Delivery
Verifiercannot be a party to the contract; must commit to the + instrument it ran and its resultsits Verdicts, superseded ones includedanyone holding the Delivery and the instrument
Facilitatornothingwhat it chose to sign, in the order it chose, on a clock + that is its ownany holder of two of its Statuses, for equivocation; + nobody, for omission or for time, without a witness outside + this document
+ +
Trust in the Facilitator + The Facilitator row is the honest one. This protocol enforces + nothing against a Facilitator; it makes some kinds of misbehaviour + attributable and says plainly which ones it does not. + Equivocation, issuing two inconsistent histories for one contract, + is attributable: every Status is signed, every Status's trace is a + prefix of every later one, and two Statuses that break that rule are + proof, checkable by anyone holding both, that the Facilitator signed + contradictory records. A Facilitator that wants to make its records + publicly append-only can register its Outcome Records with a SCITT + transparency service and hand the receipt + to the parties; this document does not + require it and defines no log of its own. + Omission is not attributable. A Facilitator that declines to + record a Delivery, or records it late, produces no signed evidence of + having declined, and a Status it does not issue proves nothing. A + client SHOULD retain every Status it receives, and a party that + submitted a record and holds no Status for it has a claim it can + make only outside this protocol. Making omission attributable needs + a witness the Facilitator does not control, such as a monitor with a + gossip path of the kind assumes, and this + document specifies none. + Time is the Facilitator's. Every instant in a trace is read from + its clock, and nothing in this document lets a party prove that a + recorded instant is wrong. This document therefore states the + assumption rather than hiding it: the Facilitator is a trusted + timekeeper, and a deployment that cannot accept that should look to + an external timestamping service, which this document does not + specify and does not preclude. + Whatever a Facilitator does with anything of value under a terms + profile is the profile's subject and is not addressed here. +
+ +
Verifier Capture + A verification tier states how strongly work is checked. It does + not state who checked it, and those fail separately. A re-execution + transcript produced by the Seller and the same transcript produced by + an independent Challenger are the same method and different evidence. + Where a proof is generated and verified entirely inside one party, + the tier is satisfied and the contract is unprotected. + requires that independence be derived + by the evaluator from the parties named in the contract, and forbids + satisfying it with a self-asserted field. +
+ +
Algorithm, Key and Encoding Confusion + Absent an algorithm allowlist an attacker chooses the algorithm. + The two consequences are alg of none, which makes + every signature check vacuous, and presenting an ECDSA public key as + an HMAC secret, which lets anyone holding the public key forge. + fixes the permitted set. + A kid carried as a sibling of the protected header rather + than inside it is outside the signed bytes and is rewritable in + transit. An attacker who can publish a key document can then + re-attribute a victim's genuine signature to an identifier it + controls, without breaking any cryptography. + requires kid inside the protected + header. + Because every digest in this document covers a signature set, a + second valid encoding of one signature is a second digest for one + record. ECDSA has two valid s values per signature and JWS + does not choose between them; the low-S rule in + does. The order of a signature set is a + second source of the same problem, and the sorting rule closes + it. +
+ +
Substitution of Committed Content + The -00 revision committed harness_uri as a string. The + bytes at that URI were covered by nothing. A Buyer could therefore + sign a contract, replace the acceptance instrument afterwards, run the + replacement, and submit its failure as a textbook-valid fraud proof. + Cost of the attack: one file overwrite. The mirror attack works + against a Seller that hosts the input sample. + requires a sibling hash over the dereferenced + bytes for every URI inside committed content, and + requires a Verdict to commit to the + instrument it actually ran, which closes the same attack from the + verification side. +
+ +
Fetching Committed Content + A work_uri, results_uri or any other URI in a + record is supplied by a counterparty and points wherever that + counterparty chose. An implementation that fetches it MUST fetch over + HTTPS only, MUST NOT follow a redirect to a scheme other than HTTPS, + MUST refuse to connect to a private, loopback or link-local address + (the ranges of , and + their loopback and link-local counterparts), and MUST verify the + sibling hash over the full received bytes before any byte is used + for anything. A fetcher that acts on partial or unverified content + has handed its counterparty a way to make it execute, store or judge + something that was never committed to. +
+ +
Children: Attachment and Omission + Naming a parent contract cost one signature in the -00 revision + and was checked against nothing. + requires the child's Buyer to be the parent's Seller, checked by the + Facilitator that holds the parent against the parent's own bytes. + The converse gap is stated rather than closed: a parent's Seller + that never registers a failing child keeps it out of the parent's + record, since this document compels no registration. A profile that + wants children visible must make registration worth the Seller's + while, or a Buyer that wants them visible must ask for the child's + Status directly, which this document does not require the child's + Facilitator to give it. +
+ +
Buying Silence from a Challenger + Wherever what a discoverer gains by reporting is less than what a + performer loses by being reported, there is a private payment that + leaves both better off than reporting, and silence dominates whatever + reward a profile designed. This document cannot close that, because + every figure involved is the profile's. What it does is record every + Challenge, in order, whoever signed it, so that a profile can act on + each independently, and it forbids a Facilitator from refusing a + Challenge because the Buyer signed it + (), so that the party with the most to + recover is always admissible. +
+ +
Non-Delivery + Under the -00 revision a contract in which nothing was ever + delivered had no path to an end: the deadline carried no stated + consequence and no window opened because there was nothing to + challenge. makes the deadline an event and + ABANDONED a terminal state that every contract can reach. What + reaching it costs anyone is the profile's, and a profile that makes + delivering nothing cheaper than delivering something wrong has + recreated the -00 incentive. +
+ +
Cross-Venue Replay + A VTC that does not name its Facilitator, network and asset is a + signed instrument replayable against any of them; + requires all three inside the signed content. A digest computed over + a contract excluding its signatures proves what was written and not + who agreed to it, so entries can be appended or stripped without + invalidating the commitment; defines + every digest over the signature set. A Delivery, Verdict or Challenge + replayed against a different contract fails because each carries + vtc_id and a hash that binds it to one contract and one + Delivery, and the typ rule of + stops a signature over one object type standing for another. +
+ +
Nondeterminism as Shield and as Weapon + A re-execution profile that does not state what determinism it + assumes cuts both ways. An honest Seller doing model-assisted work is + convicted by a re-execution that differs for ordinary reasons. A + cheating Seller escapes any fraud proof by asserting nondeterminism, + unfalsifiably. A verification profile MUST state whether it is + deterministic and what tolerance applies, and a contract naming one + that does not is not safely enforceable by anyone. +
+ +
Fabricated History + The argument for reputation derived from Outcome Records is that + faking a history requires running real contracts. That argument fails + if records do not name the parties or carry no Facilitator signature, + since two cooperating identities can then manufacture history at the + cost of two signatures. requires both. It + fails in the other direction if a negative outcome requires the + signature of the party it records against; reputation that is + structurally incapable of recording a loss is not evidence of + anything. +
+ +
Retrieval + A GET on a contract's Status or Outcome Record MUST be refused + unless the requester is a party named in the contract's + parties, the identifier in the contract's + parent.facilitator, or a party the Facilitator has chosen to + admit; a Facilitator MAY open retrieval more widely and SHOULD say so + in its capability document. How a requester proves which identifier + it is, on a GET with no body to sign, is an HTTP-layer matter this + document leaves to the deployment. The -01 revision left retrieval + unauthenticated by default, which published every contract graph a + Facilitator held to anyone who could guess an identifier. +
+ +
Key Compromise and Rotation + A signature here is a long-lived commitment, and a compromised key + signs contracts the party never agreed to. Rotation and revocation + belong to the identity method behind the kid + (), and this document does not restate them. + Two things it does require: a Facilitator MUST record, with each + record it accepts, the key material or its digest as resolved at the + time of acceptance, so that a later rotation does not make an earlier + signature unverifiable; and a Facilitator MUST NOT accept a record + whose kid resolves to a key the identity method marks as + revoked at the time of acceptance. +
+ +
Denial of Service by Challenge + Every accepted Challenge costs an independent evaluation. Without a + cost to the Challenger, a party can exhaust a Verifier's or a + Facilitator's capacity by challenging every Delivery. The deposit of + is one defence, and it is a MAY because a + deposit also deters the honest challenger an open model relies on. A + Facilitator that requires no deposit SHOULD rate-limit Challenges per + Challenger and per contract, and SHOULD publish that it does so. +
+
+ +
Privacy Considerations + PACT moves contracts and evidence about work, and both leak. + +
Input Disclosure Before Contract Formation + Publishing a representative input sample so that a counterparty can + price the work discloses production data to parties with whom no + contract exists and who may be in unknown jurisdictions. Samples + SHOULD be synthetic or de-identified. Where a real sample is + necessary, it SHOULD be disclosed only after a confidentiality + undertaking, and the constraints member SHOULD carry the + retention and deletion terms. This document cannot enforce any of + that and does not pretend to. +
+ +
The Contract Graph + A Facilitator that publishes its Outcome Records makes the + contract graph public. From it a reader can reconstruct an + organisation's suppliers, spend and cadence, which is commercially + sensitive even when no individual is identifiable. Transparency and + counterparty privacy are in genuine tension here, and this document + resolves it in favour of neither: retrieval is restricted by default + (), a Facilitator MAY publish + aggregates, and SHOULD NOT publish per-contract records identifying + both parties without their agreement. Outcome Records leak the same + graph by construction, since each names both parties and the + counterparty retains a signed copy indefinitely. Selective + disclosure over Outcome Records, so that a holder can prove a + completed contract without revealing the counterparty, is possible + with mechanisms specified elsewhere and is not specified here. +
+ +
Challenger Access + An open challenge model requires that some party outside the + contract can obtain the deliverable and the input in order to build a + fraud proof. That is in direct conflict with confidentiality of both. + The conflict is real and this document does not dissolve it. What it + does is make the choice visible: a contract whose content cannot be + disclosed to a Challenger will receive no Challenge from outside its + parties, and a terms profile that counts on one has counted on + nothing. +
+ +
Retention + Retention duties stated for dispute purposes can conflict with + erasure rights asserted by a data subject. Contracts SHOULD state a + retention period, and implementers should be aware that a hash + commitment survives deletion of the content it commits to, which is + usually the property they want and occasionally the one they must + explain. +
+
+ +
IANA Considerations + This document asks IANA to register seven media types in the vendor + tree and one well-known URI. It creates no registry. It defines + problem types but does not ask for a registry of them + (). The -01 revision asked for two + registries, one of verification profiles and one of settlement + bindings, and listed under the second an identifier in another + project's namespace that nobody had defined; both requests are + withdrawn. A profile of either kind is identified by a URI under its + definer's control and needs no registration. + +
Media Types + IANA is requested to register the following in the "Media Types" + registry, per , in the vendor tree. The + template below is given once in full; the seven registrations differ + only in the subtype name and the object they carry. + +
+
Type name:
application
+
Subtype name:
see
+
Required parameters:
N/A
+
Optional parameters:
N/A
+
Encoding considerations:
binary; the content is JSON + text as defined in , encoded in UTF-8
+
Security considerations:
See + of this document. In particular these + media types carry signed objects whose signatures MUST be verified + under the constraints in ; accepting one + without algorithm restriction permits signature forgery.
+
Interoperability considerations:
Objects MUST be + canonicalized per before hashing or + signing. Implementations that canonicalize by sorting object keys + on Unicode code point rather than UTF-16 code unit will produce + divergent digests for keys outside the Basic Multilingual + Plane.
+
Published specification:
This document
+
Applications that use this media type:
Services and + autonomous agents forming and recording task contracts under this + specification
+
Fragment identifier considerations:
As specified for + application/json
+
Additional information:
Deprecated alias names: none. + Magic numbers: none. File extensions: .json. Macintosh file type + code: TEXT
+
Person & email address to contact:
Laxmikant Sharma + <laxsharma79@gmail.com>
+
Intended usage:
COMMON
+
Restrictions on usage:
None
+
Author:
Laxmikant Sharma
+
Change controller:
Laxmikant Sharma
+
+ + + Media types registered by this document + + + + + + + + + + + + + + + + + + + +
Subtype nameObjectDefined in
vnd.pact.contract+jsonVerifiable Task Contract
vnd.pact.delivery+jsonDelivery
vnd.pact.verdict+jsonVerdict
vnd.pact.challenge+jsonChallenge
vnd.pact.status+jsonContract Status
vnd.pact.outcome+jsonOutcome Record
vnd.pact.facilitator+jsonCapability document
+ + The -01 revision asked for these in the standards tree under the + names pact-contract+json and so on. Registration in that + tree from outside the IETF stream needs approval this document does + not have (, Section 3.1), and the vendor + tree is where an individual's specification belongs. +
+ +
Well-Known URI + IANA is requested to register the following in the "Well-Known + URIs" registry, per . +
+
URI suffix:
pact-facilitator
+
Change controller:
Laxmikant Sharma
+
Specification document(s):
This document, +
+
Status:
provisional
+
Related information:
The resource is served with media + type application/vnd.pact.facilitator+json and MUST be + signed.
+
+
+ +
Problem Types + This document creates no registry for its problem types. + Section 4.2 establishes the "HTTP Problem + Types" registry for types intended for reuse across applications; + the types below are specific to this protocol and are identified by + URIs in a namespace this document defines, which that specification + permits without registration. Each is the identifier in the table + appended to the prefix + tag:laxsharma79@gmail.com,2026:pact:problem:, a tag URI + under the author's control. A tag URI is + an identifier and is not dereferenceable, which is why it was + chosen over the -01 revision's prefix on a code-hosting site: an + identifier should not change when hosting does. Documentation for + every type is maintained in the repository named in + . Each entry carries the identifier, the + HTTP status it accompanies, and the section stating the rule it + reports. A terms profile that refuses a request defines its own + types under its own prefix and reports them as + says. + + + Problem types defined by this document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierStatusDefined in
algorithm-not-permitted400
amount-invalid422
challenge-window-closed409
child-outcome-invalid422
deadline-invalid422
evidence-nonconformant422
facilitator-mismatch422
finality-ordering-violation422
flow-unsupported422
internal-error500
no-recorded-delivery409
object-conflict409
parent-unresolvable422
parties-not-distinct422
payload-too-large413
proof-nonconformant422
retrieval-restricted403
schema-invalid422
settlement-unsupported422
signature-invalid401
signature-missing401
signatures-unordered422
terms-parameters-invalid422
terms-unsupported422
unexpected-signer422
unknown-contract404
verdict-nonconformant422
verifier-not-independent422
wrong-state409
+ + The table is generated from the reference implementation's own + list, so that every type an implementation of this document emits + has a line here. The -01 revision listed eight of the twenty-nine its + implementation used. +
+
+ +
+ + + Normative References + + + + + + + + + + + + + + + + + + + + + + Decentralized Identifiers (DIDs) v1.0 + W3C + + W3C Recommendation + + + did:web Method Specification + W3C Credentials Community Group + + + + + Informative References + + + + + + + + + + + + + + + + + + + + + + PACT: Liability and Settlement for Autonomous Agent Contracts + + + + Internet-Draft, draft-laxsharma-pact-01, superseded by this document + + + + Asynchronous Protocols for Optimistic Fair Exchange + + + + + + Proceedings of the IEEE Symposium on Security and Privacy + + + + Incentivizing Outsourced Computation + + + + + + + + + Proceedings of the 3rd International Workshop on Economics + of Networked Systems (NetEcon '08), pp. 85-90 + + + + Public Enforcement of Law + + + + + Encyclopedia of Law and Economics, entry 8000, + Edward Elgar. The result is attributed therein to Bentham (1789) + + + + Recommendations for Discrete Logarithm-based Cryptography: Elliptic Curve Domain Parameters + National Institute of Standards and Technology + + + NIST Special Publication 800-186 + + + +
An Example Terms Profile: bonded-restitution + This appendix is not normative. It carries one terms profile, + under an example identifier and unregistered, so that the experiment + in has something to run against and the + vectors in the reference repository have something to reproduce. It + is the -01 revision's settlement content written as a schedule over + the events of , with the choices the -01 + revision left open now made, and it is offered as an example of the + form a profile takes, not as a recommendation of these terms. What the + figures below mean between the parties to a contract that names this + profile is a question this document does not answer and its author is + not qualified to answer; a profile meant for use needs an owner who + is. + +
Identity and Bundle + Identifier: + tag:laxsharma79@gmail.com,2026:pact:bonded-restitution. + The bundle in the reference repository, under + profiles/bonded-restitution/, contains + README.md (this text), parameters.schema.json and + vectors.json; profile_hash is the manifest digest + over those three files and prints it. + Problem types this profile reports are under the prefix + tag:laxsharma79@gmail.com,2026:pact:bonded-restitution:problem:. +
+ +
Parameters +
+
seller_bond:
amount, required. What the + Seller posts before performance.
+
verification_fund:
amount, required. What + the Buyer posts to pay for checking.
+
cap:
amount, required. The most that leaves + the Seller's accounts under this contract.
+
restitution_basis:
string, required. + released or price.
+
remainder_to:
string, optional. + buyer or sink; sink when absent.
+
verifier_fee:
amount, optional. Paid from + the fund at each Verdict; 0.00 when absent.
+
principal_on:
string, required. The event at + which the price moves to the Seller: verdict (a PASS + Verdict), delivered, or window-closed.
+
assurance:
object, required. mode + (certain, committed-sample or open) and + q_min (a number greater than zero and at most one).
+
+ The -01 revision's four release modes map onto flow and + principal_on as shows. +
+ +
Accounts + Three internal accounts, opened empty: escrow, + bond, fund. External accounts, unbounded as sources + and sinks: buyer, seller, verifier, + challenger:<kid> for each Challenger, and + sink. Closure requires the three internal accounts to hold + zero after the last entry. +
+ +
Admission + At accepted the profile evaluates, exactly and in the + contract's currency, with P the price, B seller_bond, q + assurance.q_min, and E equal to P when principal_on + is delivered and zero otherwise: + = P * (1 - q) / q + E + ]]> + and reports assurance-constraint-unsatisfied when it does + not hold, or when assurance.mode is open alone. The + inequality is the classical deterrence bound + (; Theorem 1 + for outsourced computation), with E the one term the -01 revision + added: value that moved before a Verdict cannot be recovered by the + schedule, so it raises what the Seller must post one for one. A + contract whose seller_bond or verification_fund + exceeds cap is reported as + parameters-inconsistent. +
+ +
Schedule + For each event the schedule emits the entries below, in the order + listed, omitting any entry whose amount is zero. Every event of + not named here emits nothing. Amounts + are computed from the contract and the trace prefix; "released" is + the sum of principal entries emitted so far. +
+
funded:
buyer to escrow, P, lock; + seller to bond, B, bond; buyer to fund, + verification_fund, fund.
+
delivered:
if principal_on is + delivered: escrow to seller, the escrow balance, + principal.
+
verdict:
fund to verifier, the lesser of + verifier_fee and the fund balance, + verification; then if the outcome is PASS, no Challenge is + answered, and principal_on is verdict: escrow to + seller, the escrow balance, principal.
+
window-closed:
if principal_on is + window-closed and the standing Verdict is not FAIL: escrow + to seller, the escrow balance, principal.
+
terminal, FINAL:
escrow to seller, the escrow + balance, principal; bond to seller, the bond balance, + return; fund to buyer, the fund balance, + fund-return.
+
terminal, ABANDONED:
escrow to buyer, the + escrow balance, reverse; bond to seller, the bond balance, + return; fund to buyer, the fund balance, + fund-return. The -01 revision said the bond was slashed + "to the extent of" the basis here and never said by how much; with + the price reversed the Buyer's loss is zero under either basis, so + nothing is slashed.
+
terminal, SETTLED:
in five ranks, each drawing + only what remains. (1) escrow to buyer, the escrow balance, + reverse. (2) if challenge_upheld: fund to the + Challenger whose Challenge the standing Verdict answers, the lesser + of that Challenge's costs and the fund balance, + costs. (3) bond to buyer, the lesser of the bond balance, + cap, and the Buyer's loss, restitution; the loss + is "released" under basis released and P minus the rank-1 + entry under basis price, which differ only when the price + moved in part. (4) if challenge_upheld: bond to that + Challenger, the bond balance, bounty. (5) bond to buyer or + sink per remainder_to, the bond balance, + remainder. Then fund to buyer, the fund balance, + fund-return.
+
+ Ranks 2 and 4 pay one Challenger, the one whose Challenge the + standing Verdict answers. A Challenge that was not answered by the + standing Verdict, whether lapsed, rejected or superseded, receives + nothing. Rank 4 gives the whole remaining bond, because the -01 + revision forbade capping it at a fraction chosen for tidiness and + fixed no figure; a profile owner who wants a different rule changes + this line and the vectors with it. +
+ +
Vectors + With P 180.00, B 18.00, fund 0.50, cap 180.00, basis + released, remainder to sink, no verifier fee, + principal_on verdict, assurance certain + with q 1.0, under the verdict-first flow, and a Challenge + claiming costs of 1.20. Amounts are in USDC. Trace indexes count + from zero. The lists below are what vectors.json carries + for the two paths in the figures of this document; the repository's + file also carries the SETTLED-by-Verifier and ABANDONED paths and + the price basis. + +
+ FINAL: the path of Figure 1 + +
+ +
+ SETTLED on an upheld Challenge: the path of Figure 5 + 0.50 costs + 8 bond buyer 18.00 restitution + ]]> +
+ + In the second vector rank 1 emits nothing because the escrow is + empty, rank 2 pays the lesser of 1.20 and the fund's 0.50, rank 3 + pays the whole bond because the Buyer's loss (180.00 released) exceeds + it, and ranks 4 and 5 and the fund return emit nothing because + nothing remains. Both lists satisfy closure: after the last entry the + three internal accounts hold zero. +
+
+ +
Changes from -01 + This revision separates the protocol from the meaning of its terms. + The -01 revision, in its title, abstract, Section 1.2 and throughout, + made who owed whom the subject of the document; two readers on the + IETF dispatch list observed in September 2026 that this placed it + outside what the IETF is placed to evaluate, and they were right. What + follows is the list of what changed, with the wire consequences + first. +
    +
  • The pact version is 0.2 and every committed digest + changed ().
  • +
  • The liability member is gone. A contract carries + terms: a profile URI, a digest over the profile's bundle, + and an opaque parameter object (). The -01 + figures are the parameters of the profile in + . assurance moved into + that profile's parameters; parent moved to the top level + and gained facilitator.
  • +
  • The four release modes are replaced by three flows and a + profile parameter: on-verification is + verdict-first with principal_on verdict; + on-window is delivery-first with + window-closed; optimistic is + delivery-first with delivered; + unsecured is no-window with delivered + ().
  • +
  • verification.max_verdict_seconds is added, with the + verdict-lapsed event, so a silent Verifier cannot hold a + contract in DELIVERED forever ().
  • +
  • The Work Attestation is the Outcome Record, with the RATS + collision explained (). Its subject, + role, amounts and outcome vocabulary are + replaced by parties, an outcome object, the full + trace, and terms_result (). One + record per contract.
  • +
  • Every response is a signed Contract Status carrying the trace, + replacing the unsigned state member the -01 revision added + to echoed objects ().
  • +
  • The event trace and the state machine over it are new + (); RELEASING and PROPOSED are gone, + AWAITING_CHILDREN is added.
  • +
  • delivery_hash covers the Delivery's signature; every + digest covers the signature set (). + Signature sets are sorted and ECDSA is low-S + (). Merkle leaves cover signatures + ().
  • +
  • A nonconformant Delivery is refused and recorded nowhere; the + -01 revision treated it as a FAIL Verdict + (). The Buyer countersignature sentence is + withdrawn.
  • +
  • A Verdict may carry challenge_hash; a Challenge may + carry costs; a Seller-signed Challenge is refused + ().
  • +
  • Contract trees work across Facilitators: child registration, + child outcome supply, child-unresolved, a finite latest + finality instant per contract and the rule L(child) before + L(parent); the depth and cycle rules are withdrawn with the reason + (). The -01 Section 10.2 is one sentence in + .
  • +
  • Section 3 is a data dictionary and a role table + (); no sentence in it requires anything of + a party.
  • +
  • Media types move to the vendor tree; the two registries and + the pact-escrow row are withdrawn; problem types move to a + tag URI namespace and the table lists every type the implementation + emits ().
  • +
  • Retrieval is restricted by default and fetch discipline is + stated (, ). + The threat model says plainly what is enforced against a + Facilitator, which is nothing, and what is attributable + ().
  • +
  • The experiment is restated over protocol observables + ().
  • +
+
+ +
Acknowledgements + Rich Salz and John C Klensin, on the IETF dispatch list in + September 2026, read the -01 revision as a document about who owes + whom with a protocol attached, and said so; this revision's split + between records and terms is the consequence, and the author is + grateful for the reading. The UTF-16 key-ordering vector that exposed + a latent canonicalization defect in the reference validator, and the + formulation of verifier independence as a relation the evaluator + derives rather than a field the record declares, came from Tersign + (wowlegend) on x402-foundation/x402 issue 3065. The observation that + verification tiers say how work is checked and never who checks it + came from msaleme on the same thread. Rich Smith's A2A Settlement + Extension was the clearest instance of the pattern the -01 revision + corrected, and he engaged with the critique on a2aproject/A2A + discussion 1576. +
+
+
From d05a526deccb1b7026321593ec46d5b111abd15f Mon Sep 17 00:00:00 2001 From: Laxmikant Sharma Date: Wed, 16 Sep 2026 15:06:08 -0700 Subject: [PATCH 3/4] v0.2.0: examples, terms profile and reference implementation on pact 0.2 Schemas for every -02 object (Status and Outcome Record new, attestation gone), the bonded-restitution profile bundle with seven vectors, examples minted from public seeds with real Ed25519 signatures, a Facilitator on the signed event trace with trees and lapses, the validator at 103 checks and the harness at ten paths, 40 refusals and 5 acceptances. Every digest in Section 15 is read from examples/ by the build. pactcore.jcs now formats numbers as RFC 8785 requires; it printed the float one as 1.0, so the -01 spec_hash and vtc_hash were never the digests a conforming canonicalizer computes. Vector V-25 pins it. Figures wider than 72 columns are folded per RFC 8792. --- .github/workflows/ci.yml | 2 +- README.md | 226 +-- draft/draft-laxsharma-pact-02.html | 169 ++- draft/draft-laxsharma-pact-02.txt | 1314 ++++++++--------- draft/draft-laxsharma-pact-02.xml | 137 +- examples/acceptance-harness/README.md | 2 +- .../acceptance-harness/test_acceptance.py | 2 +- examples/attestation.json | 29 - examples/challenge.json | 16 +- examples/delivery.json | 21 +- examples/keys/README.md | 7 + examples/keys/public-keys.json | 32 + examples/outcome.json | 129 ++ examples/status.json | 40 + examples/task-content/README.md | 8 + examples/task-content/customers.schema.json | 12 + examples/task-content/output.schema.json | 12 + examples/task-content/sample-10k.csv | 4 + examples/taskspec.json | 14 +- examples/verdict-on-challenge.json | 16 + examples/verdict.json | 14 +- examples/vtc.json | 47 +- examples/well-known/pact-facilitator.json | 50 +- profiles/bonded-restitution/README.md | 107 ++ .../bonded-restitution/parameters.schema.json | 82 + profiles/bonded-restitution/vectors.json | 773 ++++++++++ schemas/attestation.schema.json | 116 -- schemas/challenge.schema.json | 10 +- schemas/common.schema.json | 301 +++- schemas/delivery.schema.json | 10 +- schemas/facilitator.schema.json | 90 +- schemas/outcome.schema.json | 81 + schemas/status.schema.json | 49 + schemas/taskspec.schema.json | 25 +- schemas/verdict.schema.json | 15 +- schemas/vtc.schema.json | 80 +- tools/README.md | 232 +-- tools/agents.py | 174 ++- tools/facilitator.py | 1215 ++++++++------- tools/measure.py | 1188 ++++++++------- tools/mint_examples.py | 273 ++++ tools/pactcore.py | 159 +- tools/profile.py | 404 +++++ tools/validate.py | 1015 ++++++------- 44 files changed, 5493 insertions(+), 3209 deletions(-) delete mode 100644 examples/attestation.json create mode 100644 examples/keys/README.md create mode 100644 examples/keys/public-keys.json create mode 100644 examples/outcome.json create mode 100644 examples/status.json create mode 100644 examples/task-content/README.md create mode 100644 examples/task-content/customers.schema.json create mode 100644 examples/task-content/output.schema.json create mode 100644 examples/task-content/sample-10k.csv create mode 100644 examples/verdict-on-challenge.json create mode 100644 profiles/bonded-restitution/README.md create mode 100644 profiles/bonded-restitution/parameters.schema.json create mode 100644 profiles/bonded-restitution/vectors.json delete mode 100644 schemas/attestation.schema.json create mode 100644 schemas/outcome.schema.json create mode 100644 schemas/status.schema.json create mode 100644 tools/mint_examples.py create mode 100644 tools/profile.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b75138a..7925b5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: { python-version: "3.12" } - - run: pip install jsonschema referencing + - run: pip install jsonschema referencing cryptography - run: python3 tools/validate.py draft: runs-on: ubuntu-latest diff --git a/README.md b/README.md index a5f0ed4..3160d68 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,149 @@ -# PACT: a contract layer for autonomous agent commerce +# PACT: a task contract record for autonomous agents **Propose · Agree · Complete · Trust** -Today's agent protocols move tasks (A2A, unpriced), money (x402, -unconditional) and payment authorization (AP2, unverified). Nothing binds -work to money to proof. PACT is a proposed contract layer that closes that -gap, and it specifies exactly four things: - -- **liability** as a required member of a co-signed Verifiable Task - Contract (VTC): a seller bond, a verification fund, a cap and a - restitution basis, agreed before any work starts; -- a **Delivery** object against which the contract is judged; -- a **settlement** procedure whose release of escrow is conditioned on a - declared assurance level, with a challenge path that pays the challenger - from a fund the contract itself provisions; -- a **subcontract tree** through which liability cascades upward as - recovery and never downward as discharge. - -Everything else PACT needs (identity, delegation, discovery, transport, -audit, the payment rail, reputation, a dispute forum) it composes from -existing work and cites. Settlement emits a Work Attestation signed by the -Facilitator, so a seller cannot veto its own negative record. +By late 2026 an agent can prove who it is, act under a delegated authority, +discover another agent, call it, receipt the call and pay for it. None of +those records what one agent asked another to do, what came back, or +whether the one met the other. PACT specifies exactly four things to close +that gap: + +- a co-signed **Verifiable Task Contract** whose digest covers its + signature set, naming its settlement terms by reference (a profile + identifier, a digest over the profile's bundle, and an opaque parameter + object the document never reads); +- a **Delivery** record and the **Verdict** record bound to it by digest, + with a challenge window in which a **Challenge** can be answered by a + second Verdict; +- an **event trace**, signed by a Facilitator, from which one **Outcome + Record** per contract is produced and against which any party can check + what the named terms profile computed; +- a **Merkle commitment** from a parent contract's Outcome Record to its + children's, so a tree of subcontracts settles in a verifiable order. + +What the terms mean, and everything about who holds or moves value under +them, is the profile's to say and is outside this document. An example +profile is printed as an appendix so that the protocol can be exercised, +and it is normative nowhere. ## Status -- **Revision -01 is current.** Posted to the IETF Datatracker on - 4 September 2026, expires 8 March 2027: - [draft-laxsharma-pact](https://datatracker.ietf.org/doc/draft-laxsharma-pact/). -- Read it: [rendered HTML](https://www.ietf.org/archive/id/draft-laxsharma-pact-01.html) - · [plain text](draft/draft-laxsharma-pact-01.txt) - · [XML source](draft/draft-laxsharma-pact-01.xml) +- Source of record for **revision -02** is `draft/draft-laxsharma-pact-02.*` + at tag `v0.2.0`. The revision the IETF Datatracker shows as current is at + [draft-laxsharma-pact](https://datatracker.ietf.org/doc/draft-laxsharma-pact/); + -01 was posted 4 September 2026 and stays checkable at its archive URL. - An individual submission with no formal standing in the standards - process. Not endorsed by the IETF. -- The -00 of 27 July 2026 is superseded. Its sources stay in `draft/` so - that citations of it remain checkable. The review that led from -00 to - -01, with dispositions, is in - [issue #1](https://github.com/pact-spec/spec/issues/1). - -### What -01 changed - -External review and two adversarial audit rounds found that the -00's -settlement economics did not close. A defrauded buyer recovered nothing -from the bond, optimistic release let a seller walk away with more than -the bond, and challenger reimbursement was capped by a bond too small to -cover re-execution. The -01 reworks the settlement core around those -findings and narrows the document to what only PACT can specify: - -- The sealed-bid award procedure and contract channels are gone. How a - contract is awarded is out of scope. -- Release is no longer optimistic by default. A contract declares an - assurance mode and a release mode, and a Facilitator must refuse a - contract whose bond cannot cover the declared detection probability. -- Recovery follows a five-rank waterfall that pays the buyer's restitution - before anything is burned. -- A Delivery object and a Verifier role are defined; the Challenge object - the -00 named but never specified now exists. -- `vtc_hash` is computed over the contract including its signature set, - so the digest proves who agreed and not only what was written. -- The A2A and AP2 bindings are gone. The -00 carried an AP2 mandate binding - and an A2A skill identifier; -01 removes both, and the contract schema has - no member for an external task or mandate reference. Section 1.3 says only - that PACT is designed to be usable alongside the adjacent drafts that do - carry them. `price.settlement` names a settlement binding instead, and the - draft reserves one identifier, `pact-escrow`, in a registry it asks IANA to - create. The x402 scheme itself, its payload and its verify and settle - procedures, is not written and is the first item of future work. + process. Not adopted by any working group, not endorsed by the IETF. +- The -00 (27 July 2026) and -01 sources stay in `draft/` so that citations + remain checkable. The review that led from -00 to -01, with dispositions, + is in [issue #1](https://github.com/pact-spec/spec/issues/1). + +### What -02 changed + +Two readers on the IETF dispatch list observed in September 2026 that the +-01 made who owed whom the subject of the document, which placed it outside +what the IETF is placed to evaluate. They were right, and -02 separates the +protocol from the meaning of its terms. Appendix B of the draft lists every +change; the ones with wire consequences: + +- `pact` is `0.2` and every committed digest changed. +- The `liability` member is gone. A contract carries `terms`: a profile + URI, `profile_hash` over the profile's bundle, and `parameters` the + document does not read. The -01 figures are the parameters of the example + profile in `profiles/bonded-restitution/`. +- The four release modes are replaced by three flows (`verdict-first`, + `delivery-first`, `no-window`) and a profile parameter. +- `verification.max_verdict_seconds` is added, with a `verdict-lapsed` + event, so a silent Verifier cannot hold a contract forever. +- The Work Attestation is the **Outcome Record**: parties, an outcome + object, the full trace, and `terms_result`. One per contract, signed by + the Facilitator alone. +- Every accepted request is answered with a signed **Contract Status** + carrying the trace, and every later Status extends it as a prefix, so a + Facilitator that reorders or rewrites events is attributable. +- `delivery_hash` covers the Delivery's signature; signature sets are + sorted; ECDSA is low-S; Merkle leaves cover signatures. +- A nonconformant Delivery is refused and recorded nowhere; a Seller-signed + Challenge is refused; a Buyer's Challenge cannot be refused. +- Contract trees work across Facilitators, with a finite latest finality + instant per contract and the rule L(child) before L(parent). +- Media types move to the vendor tree; the two registries the -01 requested + are withdrawn; problem types use a `tag:` URI namespace. +- The -01 digests were computed by a canonicalizer that printed the float + one as `1.0`, which RFC 8785 does not allow, so the `spec_hash` and `vtc_hash` the -01 + printed are not what a conforming implementation computes. The -02 + examples were minted after the fix and vector V-25 pins the rule. ## Repository layout | Path | Contents | |---|---| -| `draft/` | The Internet-Draft, -01 and -00 (XML source, plain text, HTML) | -| `schemas/` | JSON Schema (2020-12) for every -01 protocol object | -| `examples/` | Worked examples whose hash commitments verify (see below) | +| `draft/` | The Internet-Draft, -02, -01 and -00 (XML source, plain text, HTML) | +| `schemas/` | JSON Schema (2020-12) for every -02 object | +| `examples/` | The worked example the draft prints: contract, Delivery, two Verdicts, Challenge, Status, Outcome Record, capability document, with real signatures | +| `examples/keys/` | The public keys that verify those signatures; the private keys derive from public seeds and are not secrets | +| `examples/task-content/` | The three files the TaskSpec commits to by sibling hash | +| `examples/acceptance-harness/` | The instrument `criteria_hash` commits to, as a manifest | | `examples/legacy-00/` | The -00 call-for-bids, bid and capability objects, kept so the published -00 stays checkable; not part of the conformance surface | -| `tools/validate.py` | Validates the examples against the schemas and checks every rule the draft states | +| `profiles/bonded-restitution/` | The example terms profile bundle: README, parameter schema, vectors. `profile_hash` is the manifest digest over these three files | +| `tools/` | The validator, the example minter, and the reference implementation; see `tools/README.md` | | `diagrams/` | Protocol diagrams | -## The examples are self-consistent - -`examples/` is not illustrative pseudo-JSON. The commitments verify: - -- `vtc.json` `spec_hash` is SHA-256 over the JCS-canonicalized (RFC 8785) - `taskspec.json`; -- `criteria_hash`, and `taskspec.acceptance.harness_hash`, is SHA-256 over - the JCS-canonicalized manifest of `examples/acceptance-harness/`, which - maps each file's relative path to the SHA-256 of its bytes; -- `vtc_hash` in `delivery.json`, `verdict.json`, `challenge.json` and - `attestation.json` is SHA-256 over the JCS-canonicalized contract - **including** its `signatures` member (-01 Section 6); -- the Merkle root of the subcontract tree follows RFC 9162: leaves hashed - with a `0x00` prefix, nodes with `0x01`, split at the largest power of - two less than the count. - -The validator runs 71 checks: 7 schema, 2 canonicalization, 10 hash, -11 rule, 9 assurance-constraint, 6 Merkle, 22 negative vectors from -the draft's conformance table, and 4 on signature sets and ECDSA -encoding (two prove the curve orders behind the low-S rule, two are -the vectors V-21 and V-22). The rules JSON Schema cannot express are -checked in code: parties distinct after normalization, one signature per -named party, protected headers carrying `alg`, `kid` and `typ` with an -allowed algorithm, and the assurance constraint of -01 Section 7.2 against -the worked figures of Section 14. +## The examples are what the draft prints, and they recompute + +Every digest in the draft's worked example (Section 15) is read out of +`examples/` by the build, and `tools/validate.py` recomputes each one: + +- `spec_hash` is the digest over `taskspec.json`; the three URIs inside it + carry sibling hashes over `examples/task-content/`; +- `criteria_hash` is the manifest digest over `examples/acceptance-harness/`; +- `profile_hash` is the manifest digest over `profiles/bonded-restitution/`; +- `vtc_hash` is the digest over the signed contract, `delivery_hash` over + the signed Delivery, and every trace entry's `object` over the signed + object it names; +- the transfers in the Outcome Record are the profile's schedule over its + own trace, and satisfy no-overdraft and closure. + +The signatures are real Ed25519 over the Section 14.1 signing input, minted +by `tools/mint_examples.py` from public seeds so that anyone can reproduce +the bytes. The validator verifies all of them with `examples/keys/`. ``` -pip install jsonschema referencing +pip install jsonschema referencing cryptography python3 tools/validate.py ``` -Two honest caveats. Signature values are illustrative placeholders, since -producing real JWS signatures requires party keys. And `jcs()` in -`tools/validate.py` is a restricted RFC 8785 implementation that is -correct for the value types these examples use but is not a conforming -general one, so a green run evidences self-consistency of these examples -rather than canonicalization interoperability with another -implementation. What it does get right, and pins with a vector, is the -key order: RFC 8785 section 3.2.3 sorts object keys by UTF-16 code unit, -which `json.dumps(sort_keys=True)` does not, the two agreeing throughout -the Basic Multilingual Plane and diverging above it. What remains -unimplemented is the ECMAScript number serialization over the full float -range. +103 checks: 10 schema, 4 canonicalization, 18 hash commitments, 20 rules +the schemas cannot express, 9 signature verifications, 10 on the terms +profile including the two transfer lists printed in Appendix A.6, 6 Merkle +per RFC 9162, and 26 conformance vectors of Section 14.3 run through the +reference Facilitator. `pactcore.jcs` is a full RFC 8785 canonicalizer for +the JSON value types, including UTF-16 key order and ECMAScript number +formatting; both are pinned by vectors because both were got wrong once. ## Building the draft ``` pip install xml2rfc -xml2rfc --text --html draft/draft-laxsharma-pact-01.xml +xml2rfc --text --html draft/draft-laxsharma-pact-02.xml ``` +Figures wider than the page are folded per RFC 8792 and say so in their +first line; the digests in them are whole once the fold is removed. + ## Relationship to other work PACT composes JWS (RFC 7515) with keys resolved through DID Core, did:web or a JWK Set, JCS (RFC 8785), the RFC 9162 Merkle tree, RATS/EAT evidence formats (RFC 9334/9711) for the TEE verification tier, and RFC 9457 problem details for errors. It names a settlement binding rather than assuming a -rail, so it is chain-agnostic, and it is designed to sit alongside the agent -transport and payment protocols rather than to bind them: -01 specifies no -binding to any of them. Its settlement is an optimistic fair exchange -in the sense of Asokan, Shoup and Waidner (1998). The bond-sizing rule it -relies on is prior art (Polinsky and Shavell; Belenkiy et al.; -Mamageishvili and Felten) that the draft cites rather than reintroduces. -The introduction relates PACT to the adjacent drafts on AP2 binding, -transport negotiation, action receipts, accountability composition, -delegation chains and contestability. Lineage: the Contract Net Protocol -(Smith, 1980), finally runnable among untrusting parties. +rail, and it binds none of the agent transport or payment protocols; the +introduction relates it to the adjacent drafts on AP2 binding, transport +negotiation, action receipts, accountability composition, delegation chains +and contestability. Carrying terms by reference follows ACME's +terms-of-service URL, X.509 policy identifiers and the Internet Open +Trading Protocol. Lineage: the Contract Net Protocol (Smith, 1980), +finally runnable among untrusting parties. ## License diff --git a/draft/draft-laxsharma-pact-02.html b/draft/draft-laxsharma-pact-02.html index ad428ef..3c87d0c 100644 --- a/draft/draft-laxsharma-pact-02.html +++ b/draft/draft-laxsharma-pact-02.html @@ -1867,8 +1867,16 @@

MUST order object keys by UTF-16 code unit as [RFC8785] Section 3.2.3 requires. Sorting by Unicode code point is a common substitution; it agrees with the required order - throughout the Basic Multilingual Plane and diverges above it.

-

Object digest. The digest of an object is the string + throughout the Basic Multilingual Plane and diverges above it. Numbers + MUST be serialized as [RFC8785] Section 3.2.2.3 + requires, which is how ECMAScript prints them: the number one is + 1, whatever type held it, and never 1.0.

+

Figures. A figure line that would exceed the page width is folded + with the single backslash strategy of [RFC8792], and a + figure that contains a fold begins with the header line that RFC + requires. The figure is read after unfolding; the digests it prints + are whole once the fold is removed.

+

Object digest. The digest of an object is the string sha256: followed by the lowercase hexadecimal SHA-256 of the canonical form of the whole object, including every signature member it carries. Every hash member in this document that names another object @@ -1878,14 +1886,14 @@

excluded signatures would prove what was written and not who agreed to it; the -00 revision had that defect and the -01 revision fixed it for the contract only. This revision applies one construction - everywhere.

-

Signing input. A signature over an object is computed over the + everywhere.

+

Signing input. A signature over an object is computed over the canonical form of the object with the signing member (signature or signatures) removed, as Section 14.1 specifies. The digest of an object and the signing input of an object are therefore different byte strings, and the difference is the - signature set.

-

Version. The pact member carries a version of the form + signature set.

+

Version. The pact member carries a version of the form major.minor; this document defines 0.2. Every object defined here is hash-committed and signed, so a member an implementation does not recognise is inside the commitment and cannot be ignored safely. An @@ -1894,16 +1902,16 @@

document does not define for it, with one exception: the contents of terms.parameters (Section 5.3) are defined by the named profile and this document reads none of them. Extension is by a - new version, not by adding members.

-

Time. Every timestamp is an RFC 3339 date-time + new version, not by adding members.

+

Time. Every timestamp is an RFC 3339 date-time [RFC3339] in UTC with the "Z" designator. The Facilitator's clock governs every deadline and window in this document: the instant at which the Facilitator records an event is the instant that counts, that instant is what the trace carries, and parties should allow for skew when acting near a boundary. Section 17.1 says what that clock can and cannot - prove.

-

Amounts. An amount is a decimal string with no exponent and a + prove.

+

Amounts. An amount is a decimal string with no exponent and a fractional part of two to eighteen digits; comparisons are exact and no rounding is implied. A currency is an asset identifier whose namespace is defined by the settlement binding named in @@ -1912,11 +1920,11 @@

document carries amounts; it does not say what any amount is for. Where a record produced under this document lists amounts, as terms_result does (Section 12.1), the meaning - of every entry is the named profile's.

-

Identifiers. A party identifier is a URI. Two identifiers name the + of every entry is the named profile's.

+

Identifiers. A party identifier is a URI. Two identifiers name the same party when they are equal after the normalization in Section 9.1, and every comparison of identifiers in - this document is made after that normalization.

+ this document is made after that normalization.

@@ -2727,7 +2735,7 @@

DELIVERED, WINDOW_OPEN or DISPUTED; then see the condition - object is the Verdict's digest; outcome (PASS or FAIL); answers (digest of the Challenge, when the Verdict carries challenge_hash); supersedes (digest of the Verdict it replaces, when one stood). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending. + object is the Verdict's digest; signer (the kid of its signature); outcome (PASS or FAIL); answers (digest of the Challenge, when the Verdict carries challenge_hash); supersedes (digest of the Verdict it replaces, when one stood). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending. @@ -2742,7 +2750,7 @@

WINDOW_OPEN or DISPUTED; then DISPUTED - object is the Challenge's digest; costs copied from the Challenge when present. The Challenge passed Section 7.3 before closes_at. + object is the Challenge's digest; signer (the kid of its signature); costs copied from the Challenge when present. The Challenge passed Section 7.3 before closes_at. @@ -2840,6 +2848,8 @@

+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
 {
   "pact": "0.2",
   "type": "VerifiableTaskContract",
@@ -2851,7 +2861,8 @@ 

"verifier": "did:web:audit.example" }, "task": { - "spec_hash": "sha256:<spec_hash>", + "spec_hash": "sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef11\ + 1d27ff6bee37f05531823b72", "deadline": "2026-11-14T00:00:00Z" }, "price": { @@ -2863,14 +2874,16 @@

"verification": { "tier": "T0-reexec", "profile": "acceptance", - "criteria_hash": "sha256:<criteria_hash>", + "criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c539375\ + 6ae95c10b8351c9c55eb9316416265fc1b", "max_verdict_seconds": 86400 }, "flow": "verdict-first", "terms": { "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:<profile_hash>", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ + c1147743326dabd45bda546dc62", "parameters": { "...": "the profile's; not read here" } }, "challenge": { @@ -3033,17 +3046,21 @@

+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
 {
   "pact": "0.2",
   "type": "Delivery",
   "vtc_id":   "vtc_7f3a91",
-  "vtc_hash": "sha256:<vtc_hash>",
+  "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\
+   5fbbd4ebace4fb980f1c2",
   "work_hash":  "sha256:9c1f...",
   "work_uri":   "https://cdn.dataforge.example/o/9c1f",
   "input_hash": "sha256:41ab...",
   "evidence": {
     "profile":        "acceptance",
-    "instrument_hash":"sha256:<criteria_hash>",
+    "instrument_hash":"sha256:0bdde1ab6b081d2b4bda580c5393756ae95\
+   c10b8351c9c55eb9316416265fc1b",
     "results_hash":   "sha256:7e02...",
     "results_uri":    "https://cdn.dataforge.example/o/7e02"
   },
@@ -3129,14 +3146,18 @@ 

+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
 {
   "pact": "0.2",
   "type": "Verdict",
   "vtc_id":        "vtc_7f3a91",
-  "delivery_hash": "sha256:<delivery_hash>",
+  "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\
+   f8aaaf2f186702504fba242ffb",
   "outcome":       "PASS",
   "profile":       "acceptance",
-  "instrument_hash": "sha256:<criteria_hash>",
+  "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\
+   10b8351c9c55eb9316416265fc1b",
   "results_hash":    "sha256:7e02...",
   "evaluated_at":  "2026-11-10T09:14:22Z",
   "signature": { "protected": "...", "signature": "..." }
@@ -3214,14 +3235,18 @@ 

+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
 {
   "pact": "0.2",
   "type": "Challenge",
   "vtc_id":        "vtc_7f3a91",
-  "delivery_hash": "sha256:<delivery_hash>",
+  "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\
+   f8aaaf2f186702504fba242ffb",
   "proof": {
     "profile":         "acceptance",
-    "instrument_hash": "sha256:<criteria_hash>",
+    "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\
+   5c10b8351c9c55eb9316416265fc1b",
     "results_hash":    "sha256:a91e...",
     "results_uri":     "https://watch.example/o/a91e",
     "failing_checks":  ["schema_valid_rate", "row_count_min"]
@@ -3309,6 +3334,8 @@ 

+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
 {
   "pact": "0.2",
   "type": "FacilitatorCapabilities",
@@ -3323,7 +3350,8 @@ 

"terms_profiles": [ { "id": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:<profile_hash>" } + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f\ + 5fc1147743326dabd45bda546dc62" } ], "max_contract_value": { "amount": "50000.00", "currency": "USDC" }, @@ -3628,20 +3656,26 @@

+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
 {
   "pact": "0.2",
   "type": "ContractStatus",
   "vtc_id":   "vtc_7f3a91",
-  "vtc_hash": "sha256:<vtc_hash>",
+  "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\
+   5fbbd4ebace4fb980f1c2",
   "state":    "WINDOW_OPEN",
   "trace": [
     { "event": "accepted",  "at": "2026-11-01T10:00:00Z",
-      "object": "sha256:<vtc_hash>" },
+      "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\
+   225fbbd4ebace4fb980f1c2" },
     { "event": "funded",    "at": "2026-11-01T10:00:00Z" },
     { "event": "delivered", "at": "2026-11-10T08:30:12Z",
-      "object": "sha256:<delivery_hash>" },
+      "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\
+   aaf2f186702504fba242ffb" },
     { "event": "verdict",   "at": "2026-11-10T09:14:30Z",
-      "object": "sha256:<verdict_hash>", "outcome": "PASS" },
+      "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\
+   e09452a74bac15e6752ca81", "outcome": "PASS" },
     { "event": "window-opened", "at": "2026-11-10T09:14:30Z",
       "closes_at": "2026-11-10T10:14:30Z" }
   ],
@@ -3691,13 +3725,16 @@ 

identities.

-
+
+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
 {
   "pact": "0.2",
   "type": "OutcomeRecord",
   "vtc_id":   "vtc_7f3a91",
-  "vtc_hash": "sha256:<vtc_hash>",
+  "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\
+   5fbbd4ebace4fb980f1c2",
   "parties": {
     "buyer":       "did:web:acme.example",
     "seller":      "did:web:dataforge.example",
@@ -3708,19 +3745,26 @@ 

"work_hash": "sha256:9c1f...", "trace": [ { "event": "accepted", "at": "...", - "object": "sha256:<vtc_hash>" }, + "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ + 225fbbd4ebace4fb980f1c2" }, { "event": "funded", "at": "..." }, { "event": "delivered", "at": "...", - "object": "sha256:<delivery_hash>" }, + "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ + aaf2f186702504fba242ffb" }, { "event": "verdict", "at": "...", - "object": "sha256:<verdict_hash>", "outcome": "PASS" }, + "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ + e09452a74bac15e6752ca81", "outcome": "PASS" }, { "event": "window-opened", "at": "...", "closes_at": "..." }, { "event": "challenge", "at": "...", - "object": "sha256:<challenge_hash>" }, + "object": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df\ + 6cfef12d12d1340c15e5d85" }, { "event": "verdict", "at": "...", - "object": "sha256:<verdict2_hash>", "outcome": "FAIL", - "answers": "sha256:<challenge_hash>", - "supersedes": "sha256:<verdict_hash>" }, + "object": "sha256:012bab194f444012f87ff8986419537f9de9f3dfe\ + 845c6623ac6a7488fb98b73", "outcome": "FAIL", + "answers": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4d\ + f6cfef12d12d1340c15e5d85", + "supersedes": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51\ + cee8e09452a74bac15e6752ca81" }, { "event": "children-final", "at": "..." }, { "event": "terminal", "at": "...", "state": "SETTLED", "challenge_upheld": true } @@ -3728,7 +3772,8 @@

"terms_result": { "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:<profile_hash>", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ + c1147743326dabd45bda546dc62", "currency": "USDC", "transfers": [ { "event": 8, "from": "...", "to": "...", "amount": "...", @@ -4344,6 +4389,13 @@

overdraw an account of the profile reject + + V-25 + a number serialized by the host language's + default formatter, such as 1.0 for the float + one + digest mismatch +

@@ -4353,7 +4405,12 @@

which agrees with the required UTF-16 order for every ASCII key and so passes every vector an implementer would think to write. V-19 is the opposite mistake, folding more than Section 9.1 - allows, and the -01 reference validator made it.

+ allows, and the -01 reference validator made it. V-25 is the number + half of the V-18 mistake: [RFC8785] prints numbers as + ECMAScript does, so the float one is 1 and never + 1.0. The -02 reference canonicalizer printed 1.0 + until this vector caught it, and every digest in + Section 15 changed when it was fixed.

@@ -4378,11 +4435,18 @@

are:

- spec_hash       sha256:<spec_hash>
- criteria_hash   sha256:<criteria_hash>
- profile_hash    sha256:<profile_hash>
- vtc_hash        sha256:<vtc_hash>
- delivery_hash   sha256:<delivery_hash>
+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
+ spec_hash       sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef111\
+   d27ff6bee37f05531823b72
+ criteria_hash   sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b83\
+   51c9c55eb9316416265fc1b
+ profile_hash    sha256:00d71829f6f9192b43b929d0154a6eb409f5fc114\
+   7743326dabd45bda546dc62
+ vtc_hash        sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\
+   225fbbd4ebace4fb980f1c2
+ delivery_hash   sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\
+   aaf2f186702504fba242ffb
 

criteria_hash is the manifest digest of @@ -4420,7 +4484,7 @@

https://github.com/pact-spec/spec, under the Revised BSD licence. At tag v0.2.0 it comprises the object schemas, the examples whose digests Section 15 prints, a conformance validator that runs - <NN> checks including every vector of + 103 checks including every vector of Section 14.3, a Facilitator serving the endpoints of Section 13 with the profile of Appendix A, and clients for the other roles. Its @@ -5415,6 +5479,10 @@

Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", BCP 205, RFC 7942, DOI 10.17487/RFC7942, , <https://www.rfc-editor.org/info/rfc7942>.
+
[RFC8792]
+
+Watsen, K., Auerswald, E., Farrel, A., and Q. Wu, "Handling Long Lines in Content of Internet-Drafts and RFCs", RFC 8792, DOI 10.17487/RFC8792, , <https://www.rfc-editor.org/info/rfc8792>.
+
[RFC8555]
Barnes, R., Hoffman-Andrews, J., McCarney, D., and J. Kasten, "Automatic Certificate Management Environment (ACME)", RFC 8555, DOI 10.17487/RFC8555, , <https://www.rfc-editor.org/info/rfc8555>.
@@ -5793,7 +5861,14 @@

first.

  • The pact version is 0.2 and every committed digest - changed (Section 15). + changed (Section 15). + The -01 digests were computed by a canonicalizer that serialized + the number one as 1.0, which [RFC8785] + does not allow, so the spec_hash and vtc_hash + the -01 revision printed are not what a conforming implementation + computes over the -01 example objects. V-25 in + Section 14.3 pins the rule and the -02 examples + were minted after the correction.
  • The liability member is gone. A contract carries terms: a profile URI, a digest over the profile's bundle, diff --git a/draft/draft-laxsharma-pact-02.txt b/draft/draft-laxsharma-pact-02.txt index 05654be..de549ff 100644 --- a/draft/draft-laxsharma-pact-02.txt +++ b/draft/draft-laxsharma-pact-02.txt @@ -90,14 +90,14 @@ Table of Contents 2.1. Terminology . . . . . . . . . . . . . . . . . . . . . . . 9 3. Data Dictionary . . . . . . . . . . . . . . . . . . . . . . . 10 3.1. Members Common to Every Record . . . . . . . . . . . . . 10 - 3.2. Contract Members . . . . . . . . . . . . . . . . . . . . 10 - 3.3. TaskSpec Members . . . . . . . . . . . . . . . . . . . . 11 + 3.2. Contract Members . . . . . . . . . . . . . . . . . . . . 11 + 3.3. TaskSpec Members . . . . . . . . . . . . . . . . . . . . 12 3.4. Delivery Members . . . . . . . . . . . . . . . . . . . . 12 - 3.5. Verdict Members . . . . . . . . . . . . . . . . . . . . . 12 + 3.5. Verdict Members . . . . . . . . . . . . . . . . . . . . . 13 3.6. Challenge Members . . . . . . . . . . . . . . . . . . . . 13 - 3.7. Contract Status Members . . . . . . . . . . . . . . . . . 13 + 3.7. Contract Status Members . . . . . . . . . . . . . . . . . 14 3.8. Outcome Record Members . . . . . . . . . . . . . . . . . 14 - 3.9. Capability Document Members . . . . . . . . . . . . . . . 14 + 3.9. Capability Document Members . . . . . . . . . . . . . . . 15 3.10. Roles . . . . . . . . . . . . . . . . . . . . . . . . . . 15 4. Protocol Overview . . . . . . . . . . . . . . . . . . . . . . 16 4.1. States . . . . . . . . . . . . . . . . . . . . . . . . . 17 @@ -117,32 +117,32 @@ Internet-Draft PACT September 2026 6. The Delivery Record . . . . . . . . . . . . . . . . . . . . . 25 7. Verdicts, Challenges and the Window . . . . . . . . . . . . . 26 7.1. Flows . . . . . . . . . . . . . . . . . . . . . . . . . . 26 - 7.2. Verdicts . . . . . . . . . . . . . . . . . . . . . . . . 26 + 7.2. Verdicts . . . . . . . . . . . . . . . . . . . . . . . . 27 7.3. Challenges . . . . . . . . . . . . . . . . . . . . . . . 28 7.4. Disputes and Lapses . . . . . . . . . . . . . . . . . . . 29 - 8. Facilitator Capability Discovery . . . . . . . . . . . . . . 29 + 8. Facilitator Capability Discovery . . . . . . . . . . . . . . 30 9. Verification Profiles . . . . . . . . . . . . . . . . . . . . 31 9.1. Verifier Independence and Identifier Normalization . . . 32 - 10. Contract Trees . . . . . . . . . . . . . . . . . . . . . . . 32 + 10. Contract Trees . . . . . . . . . . . . . . . . . . . . . . . 33 10.1. Binding a Child to Its Parent . . . . . . . . . . . . . 33 - 10.2. Registration and Children Final . . . . . . . . . . . . 33 - 10.3. Finality Is Bottom-Up . . . . . . . . . . . . . . . . . 34 - 11. The Contract Status . . . . . . . . . . . . . . . . . . . . . 35 - 12. Outcome Records . . . . . . . . . . . . . . . . . . . . . . . 36 + 10.2. Registration and Children Final . . . . . . . . . . . . 34 + 10.3. Finality Is Bottom-Up . . . . . . . . . . . . . . . . . 35 + 11. The Contract Status . . . . . . . . . . . . . . . . . . . . . 36 + 12. Outcome Records . . . . . . . . . . . . . . . . . . . . . . . 38 12.1. The Terms Result . . . . . . . . . . . . . . . . . . . . 39 - 12.2. The Children Merkle Root . . . . . . . . . . . . . . . . 39 - 13. Protocol Endpoints . . . . . . . . . . . . . . . . . . . . . 40 - 13.1. Proposing a Contract . . . . . . . . . . . . . . . . . . 41 - 13.2. Idempotency . . . . . . . . . . . . . . . . . . . . . . 41 - 13.3. Error Responses . . . . . . . . . . . . . . . . . . . . 42 - 13.4. Exchange . . . . . . . . . . . . . . . . . . . . . . . . 42 - 14. Conformance . . . . . . . . . . . . . . . . . . . . . . . . . 43 - 14.1. Signatures . . . . . . . . . . . . . . . . . . . . . . . 43 - 14.1.1. Key Resolution . . . . . . . . . . . . . . . . . . . 44 + 12.2. The Children Merkle Root . . . . . . . . . . . . . . . . 40 + 13. Protocol Endpoints . . . . . . . . . . . . . . . . . . . . . 41 + 13.1. Proposing a Contract . . . . . . . . . . . . . . . . . . 42 + 13.2. Idempotency . . . . . . . . . . . . . . . . . . . . . . 42 + 13.3. Error Responses . . . . . . . . . . . . . . . . . . . . 43 + 13.4. Exchange . . . . . . . . . . . . . . . . . . . . . . . . 43 + 14. Conformance . . . . . . . . . . . . . . . . . . . . . . . . . 44 + 14.1. Signatures . . . . . . . . . . . . . . . . . . . . . . . 44 + 14.1.1. Key Resolution . . . . . . . . . . . . . . . . . . . 45 14.2. Rules Not Expressible in a Schema . . . . . . . . . . . 45 14.3. Test Vectors . . . . . . . . . . . . . . . . . . . . . . 46 - 15. Worked Example . . . . . . . . . . . . . . . . . . . . . . . 47 - 16. Implementation Status . . . . . . . . . . . . . . . . . . . . 48 + 15. Worked Example . . . . . . . . . . . . . . . . . . . . . . . 48 + 16. Implementation Status . . . . . . . . . . . . . . . . . . . . 49 17. Security Considerations . . . . . . . . . . . . . . . . . . . 49 17.1. Trust in the Facilitator . . . . . . . . . . . . . . . . 51 17.2. Verifier Capture . . . . . . . . . . . . . . . . . . . . 51 @@ -186,7 +186,7 @@ Internet-Draft PACT September 2026 A.6. Vectors . . . . . . . . . . . . . . . . . . . . . . . . . 69 Appendix B. Changes from -01 . . . . . . . . . . . . . . . . . . 70 Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 71 - Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 71 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 72 1. Introduction @@ -399,7 +399,15 @@ Internet-Draft PACT September 2026 object keys by UTF-16 code unit as [RFC8785] Section 3.2.3 requires. Sorting by Unicode code point is a common substitution; it agrees with the required order throughout the Basic Multilingual Plane and - diverges above it. + diverges above it. Numbers MUST be serialized as [RFC8785] + Section 3.2.2.3 requires, which is how ECMAScript prints them: the + number one is 1, whatever type held it, and never 1.0. + + Figures. A figure line that would exceed the page width is folded + with the single backslash strategy of [RFC8792], and a figure that + contains a fold begins with the header line that RFC requires. The + figure is read after unfolding; the digests it prints are whole once + the fold is removed. Object digest. The digest of an object is the string sha256: followed by the lowercase hexadecimal SHA-256 of the canonical form @@ -429,6 +437,19 @@ Internet-Draft PACT September 2026 this document reads none of them. Extension is by a new version, not by adding members. + + + + + + + + +Sharma Expires 20 March 2027 [Page 8] + +Internet-Draft PACT September 2026 + + Time. Every timestamp is an RFC 3339 date-time [RFC3339] in UTC with the "Z" designator. The Facilitator's clock governs every deadline and window in this document: the instant at which the Facilitator @@ -442,14 +463,6 @@ Internet-Draft PACT September 2026 namespace is defined by the settlement binding named in price.settlement, and need not be an ISO 4217 code. A network is a ledger identifier in the form the same binding defines. This - - - -Sharma Expires 20 March 2027 [Page 8] - -Internet-Draft PACT September 2026 - - document carries amounts; it does not say what any amount is for. Where a record produced under this document lists amounts, as terms_result does (Section 12.1), the meaning of every entry is the @@ -485,6 +498,14 @@ Internet-Draft PACT September 2026 gain in clarity that this note does not provide. Facilitator: The party that runs the state machine of Section 4 for + + + +Sharma Expires 20 March 2027 [Page 9] + +Internet-Draft PACT September 2026 + + a contract: it accepts or refuses the records posted to it, records events in one order on its own clock, and signs the trace and the Outcome Record. Nothing in this document says that a @@ -494,18 +515,6 @@ Internet-Draft PACT September 2026 The remaining roles are defined by what they sign and receive in Section 3.10, and the objects by their members in Section 3. - - - - - - - -Sharma Expires 20 March 2027 [Page 9] - -Internet-Draft PACT September 2026 - - 3. Data Dictionary This section lists every member this document defines, by the object @@ -542,6 +551,17 @@ Internet-Draft PACT September 2026 as that section says. Commits, in the contract, to who agreed; in the Outcome Record, to which Facilitator issued it. + + + + + + +Sharma Expires 20 March 2027 [Page 10] + +Internet-Draft PACT September 2026 + + 3.2. Contract Members Carried in the Verifiable Task Contract (Section 5), media type @@ -555,13 +575,6 @@ Internet-Draft PACT September 2026 required), verifier (URI, optional). Commits to who plays each role for this contract. - - -Sharma Expires 20 March 2027 [Page 10] - -Internet-Draft PACT September 2026 - - task: object, required. spec_hash (digest, required) commits to a TaskSpec (Section 5.2); spec_uri (URI, optional) says where its bytes may be fetched; deadline (timestamp, required) is the @@ -594,6 +607,17 @@ Internet-Draft PACT September 2026 named profile says. challenge: object, required. window_seconds (integer, required, + + + + + + +Sharma Expires 20 March 2027 [Page 11] + +Internet-Draft PACT September 2026 + + greater than zero) is the duration of the challenge window; max_dispute_seconds (integer, required) is the longest interval after a challenge event within which a Verdict on that Challenge @@ -610,14 +634,6 @@ Internet-Draft PACT September 2026 (Section 5.2). It is not transmitted over the endpoints of this document. - - - -Sharma Expires 20 March 2027 [Page 11] - -Internet-Draft PACT September 2026 - - description: string, required. A statement of the work in natural language. @@ -650,6 +666,14 @@ Internet-Draft PACT September 2026 work_hash: digest, required. Commits to the delivered bytes, or to a manifest per Section 5.1 where the deliverable is a bundle. + + + +Sharma Expires 20 March 2027 [Page 12] + +Internet-Draft PACT September 2026 + + work_uri: URI, optional. Where the bytes may be fetched, subject to Section 17.5. @@ -667,13 +691,6 @@ Internet-Draft PACT September 2026 Carried in the Verdict (Section 7.2), media type application/ vnd.pact.verdict+json. - - -Sharma Expires 20 March 2027 [Page 12] - -Internet-Draft PACT September 2026 - - vtc_id: string, required. delivery_hash: digest, required. Commits to the Delivery judged, @@ -706,6 +723,13 @@ Internet-Draft PACT September 2026 for the acceptance profile, profile, instrument_hash, results_hash, results_uri and failing_checks (array of strings). + + +Sharma Expires 20 March 2027 [Page 13] + +Internet-Draft PACT September 2026 + + costs: object, optional. amount and currency: a figure the Challenger asserts for producing the proof. This document records it in the trace and reads it for nothing; its meaning is the named @@ -722,14 +746,6 @@ Internet-Draft PACT September 2026 state: string, required. A state name from Figure 2. trace: array of objects, required. The event trace so far, in the - - - -Sharma Expires 20 March 2027 [Page 13] - -Internet-Draft PACT September 2026 - - order recorded (Section 4.2). Each entry carries event (string, required), at (timestamp, required), object (digest, required where the event was caused by a posted record), and the event- @@ -758,6 +774,18 @@ Internet-Draft PACT September 2026 with the terminal event. terms_result: object, required (Section 12.1). profile and + + + + + + + +Sharma Expires 20 March 2027 [Page 14] + +Internet-Draft PACT September 2026 + + profile_hash (copied from the contract), currency (string), and transfers (array of objects), each with from (string), to (string), amount (amount) and code (string). The entries are the @@ -779,13 +807,6 @@ Internet-Draft PACT September 2026 settlement_bindings: array of objects, required. Each with id (URI), networks and assets (arrays of strings). - - -Sharma Expires 20 March 2027 [Page 14] - -Internet-Draft PACT September 2026 - - flows: array of strings, required. The flows of Section 7.1 the Facilitator implements. @@ -816,27 +837,6 @@ Internet-Draft PACT September 2026 - - - - - - - - - - - - - - - - - - - - - Sharma Expires 20 March 2027 [Page 15] Internet-Draft PACT September 2026 @@ -1010,54 +1010,54 @@ Sharma Expires 20 March 2027 [Page 18] Internet-Draft PACT September 2026 - +==========+====================+===================================+ - |Event | Recorded in; then | Members and condition | - +==========+====================+===================================+ - |accepted | none; then | object is vtc_hash. The | - | | ACCEPTED | contract passed Section 13.1. | - +----------+--------------------+-----------------------------------+ - |funded | ACCEPTED; then | ref (string, optional, in the | - | | FUNDED | form the settlement binding | - | | | defines). Recorded when every | - | | | account the named terms profile | - | | | requires shows finality on the | - | | | settlement binding named in | - | | | price.settlement; how a | - | | | Facilitator observes that is the | - | | | binding's to say, and this is | - | | | the only sentence in this | - | | | document that mentions an | - | | | account. | - +----------+--------------------+-----------------------------------+ - |deadline- | ACCEPTED or | task.deadline has passed with no | - |passed | FUNDED; then | delivered entry. | - | | AWAITING_CHILDREN | | - +----------+--------------------+-----------------------------------+ - |delivered | FUNDED; then | object is the Delivery's digest. | - | | DELIVERED | The Delivery passed Section 6. | - +----------+--------------------+-----------------------------------+ - |window- | DELIVERED; then | Under delivery-first, | - |opened | WINDOW_OPEN | immediately after delivered; | - | | | under verdict-first, immediately | - | | | after a PASS verdict or after | - | | | verdict-lapsed. closes_at | - | | | (timestamp, required) is at plus | - | | | challenge.window_seconds. | - +----------+--------------------+-----------------------------------+ - |verdict | DELIVERED, | object is the Verdict's digest; | - | | WINDOW_OPEN or | outcome (PASS or FAIL); answers | - | | DISPUTED; then see | (digest of the Challenge, when | - | | the condition | the Verdict carries | - | | | challenge_hash); supersedes | - | | | (digest of the Verdict it | - | | | replaces, when one stood). | - | | | Then: FAIL leads to | - | | | AWAITING_CHILDREN; PASS in | - | | | DELIVERED leads to window- | - | | | opened; PASS in WINDOW_OPEN | - | | | changes nothing; PASS in | - | | | DISPUTED leads to WINDOW_OPEN | - | | | once no Challenge is pending. | + +==========+====================+==================================+ + |Event | Recorded in; then | Members and condition | + +==========+====================+==================================+ + |accepted | none; then | object is vtc_hash. The | + | | ACCEPTED | contract passed Section 13.1. | + +----------+--------------------+----------------------------------+ + |funded | ACCEPTED; then | ref (string, optional, in the | + | | FUNDED | form the settlement binding | + | | | defines). Recorded when every | + | | | account the named terms profile | + | | | requires shows finality on the | + | | | settlement binding named in | + | | | price.settlement; how a | + | | | Facilitator observes that is the | + | | | binding's to say, and this is | + | | | the only sentence in this | + | | | document that mentions an | + | | | account. | + +----------+--------------------+----------------------------------+ + |deadline- | ACCEPTED or | task.deadline has passed with no | + |passed | FUNDED; then | delivered entry. | + | | AWAITING_CHILDREN | | + +----------+--------------------+----------------------------------+ + |delivered | FUNDED; then | object is the Delivery's digest. | + | | DELIVERED | The Delivery passed Section 6. | + +----------+--------------------+----------------------------------+ + |window- | DELIVERED; then | Under delivery-first, | + |opened | WINDOW_OPEN | immediately after delivered; | + | | | under verdict-first, immediately | + | | | after a PASS verdict or after | + | | | verdict-lapsed. closes_at | + | | | (timestamp, required) is at plus | + | | | challenge.window_seconds. | + +----------+--------------------+----------------------------------+ + |verdict | DELIVERED, | object is the Verdict's digest; | + | | WINDOW_OPEN or | signer (the kid of its | + | | DISPUTED; then see | signature); outcome (PASS or | + | | the condition | FAIL); answers (digest of the | + | | | Challenge, when the Verdict | + | | | carries challenge_hash); | + | | | supersedes (digest of the | + | | | Verdict it replaces, when one | + | | | stood). Then: FAIL leads to | + | | | AWAITING_CHILDREN; PASS in | + | | | DELIVERED leads to window- | + | | | opened; PASS in WINDOW_OPEN | + | | | changes nothing; PASS in | + | | | DISPUTED leads to WINDOW_OPEN | @@ -1066,54 +1066,54 @@ Sharma Expires 20 March 2027 [Page 19] Internet-Draft PACT September 2026 - +----------+--------------------+-----------------------------------+ - |verdict- | DELIVERED; then | Under verdict-first, | - |lapsed | WINDOW_OPEN | verification.max_verdict_seconds | - | | | have passed since delivered with | - | | | no verdict. window-opened | - | | | follows. | - +----------+--------------------+-----------------------------------+ - |challenge | WINDOW_OPEN or | object is the Challenge's | - | | DISPUTED; then | digest; costs copied from the | - | | DISPUTED | Challenge when present. The | - | | | Challenge passed Section 7.3 | - | | | before closes_at. | - +----------+--------------------+-----------------------------------+ - |dispute- | DISPUTED; then | object is the Challenge's | - |lapsed | WINDOW_OPEN | digest. | - | | | challenge.max_dispute_seconds | - | | | have passed since that challenge | - | | | entry with no Verdict answering | - | | | it. Leads to WINDOW_OPEN once | - | | | no Challenge is pending; the | - | | | earlier Verdict, if any, stands. | - +----------+--------------------+-----------------------------------+ - |window- | WINDOW_OPEN; then | closes_at has passed and no | - |closed | AWAITING_CHILDREN | Challenge is pending. The | - | | | window is never extended: a | - | | | dispute that outlasts it delays | - | | | this entry and does not move | - | | | closes_at. | - +----------+--------------------+-----------------------------------+ - |child- | any non-terminal; | object is the child contract's | - |registered| unchanged | digest; facilitator (URI). | - | | | Section 10.2. | - +----------+--------------------+-----------------------------------+ - |child- | any non-terminal; | object is the child's Outcome | - |final | unchanged | Record digest; child (the child | - | | | contract's digest). | - +----------+--------------------+-----------------------------------+ - |child- | any non-terminal; | child (the child contract's | - |unresolved| unchanged | digest). The child's latest | - | | | finality instant (Section 10.3) | - | | | has passed and no Outcome Record | - | | | for it is held. | - +----------+--------------------+-----------------------------------+ - |children- | AWAITING_CHILDREN; | Every registered child has a | - |final | then terminal | child-final or child-unresolved | - | | follows | entry. A contract with no | - | | | registered children records this | - | | | entry on entering | + | | | once no Challenge is pending. | + +----------+--------------------+----------------------------------+ + |verdict- | DELIVERED; then | Under verdict-first, | + |lapsed | WINDOW_OPEN | verification.max_verdict_seconds | + | | | have passed since delivered with | + | | | no verdict. window-opened | + | | | follows. | + +----------+--------------------+----------------------------------+ + |challenge | WINDOW_OPEN or | object is the Challenge's | + | | DISPUTED; then | digest; signer (the kid of its | + | | DISPUTED | signature); costs copied from | + | | | the Challenge when present. The | + | | | Challenge passed Section 7.3 | + | | | before closes_at. | + +----------+--------------------+----------------------------------+ + |dispute- | DISPUTED; then | object is the Challenge's | + |lapsed | WINDOW_OPEN | digest. | + | | | challenge.max_dispute_seconds | + | | | have passed since that challenge | + | | | entry with no Verdict answering | + | | | it. Leads to WINDOW_OPEN once | + | | | no Challenge is pending; the | + | | | earlier Verdict, if any, stands. | + +----------+--------------------+----------------------------------+ + |window- | WINDOW_OPEN; then | closes_at has passed and no | + |closed | AWAITING_CHILDREN | Challenge is pending. The | + | | | window is never extended: a | + | | | dispute that outlasts it delays | + | | | this entry and does not move | + | | | closes_at. | + +----------+--------------------+----------------------------------+ + |child- | any non-terminal; | object is the child contract's | + |registered| unchanged | digest; facilitator (URI). | + | | | Section 10.2. | + +----------+--------------------+----------------------------------+ + |child- | any non-terminal; | object is the child's Outcome | + |final | unchanged | Record digest; child (the child | + | | | contract's digest). | + +----------+--------------------+----------------------------------+ + |child- | any non-terminal; | child (the child contract's | + |unresolved| unchanged | digest). The child's latest | + | | | finality instant (Section 10.3) | + | | | has passed and no Outcome Record | + | | | for it is held. | + +----------+--------------------+----------------------------------+ + |children- | AWAITING_CHILDREN; | Every registered child has a | + |final | then terminal | child-final or child-unresolved | + | | follows | entry. A contract with no | @@ -1122,20 +1122,22 @@ Sharma Expires 20 March 2027 [Page 20] Internet-Draft PACT September 2026 - | | | AWAITING_CHILDREN. | - +----------+--------------------+-----------------------------------+ - |terminal | AWAITING_CHILDREN; | state (the terminal state) and | - | | then FINAL, | challenge_upheld (boolean). | - | | SETTLED or | ABANDONED where deadline-passed | - | | ABANDONED | was recorded; SETTLED where the | - | | | standing Verdict is FAIL, with | - | | | challenge_upheld true when that | - | | | Verdict answers a Challenge; | - | | | FINAL otherwise. | - +----------+--------------------+-----------------------------------+ + | | | registered children records this | + | | | entry on entering | + | | | AWAITING_CHILDREN. | + +----------+--------------------+----------------------------------+ + |terminal | AWAITING_CHILDREN; | state (the terminal state) and | + | | then FINAL, | challenge_upheld (boolean). | + | | SETTLED or | ABANDONED where deadline-passed | + | | ABANDONED | was recorded; SETTLED where the | + | | | standing Verdict is FAIL, with | + | | | challenge_upheld true when that | + | | | Verdict answers a Challenge; | + | | | FINAL otherwise. | + +----------+--------------------+----------------------------------+ - Table 2: Events: the state each is recorded in, the state that - follows, and what the entry carries + Table 2: Events: the state each is recorded in, the state that + follows, and what the entry carries The standing Verdict is the last verdict entry in the trace that no later entry supersedes. A Challenge is pending from its challenge @@ -1171,13 +1173,13 @@ Internet-Draft PACT September 2026 - - Sharma Expires 20 March 2027 [Page 21] Internet-Draft PACT September 2026 + ========== NOTE: '\' line wrapping per RFC 8792 =========== + { "pact": "0.2", "type": "VerifiableTaskContract", @@ -1189,7 +1191,8 @@ Internet-Draft PACT September 2026 "verifier": "did:web:audit.example" }, "task": { - "spec_hash": "sha256:", + "spec_hash": "sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef11\ + 1d27ff6bee37f05531823b72", "deadline": "2026-11-14T00:00:00Z" }, "price": { @@ -1201,14 +1204,16 @@ Internet-Draft PACT September 2026 "verification": { "tier": "T0-reexec", "profile": "acceptance", - "criteria_hash": "sha256:", + "criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c539375\ + 6ae95c10b8351c9c55eb9316416265fc1b", "max_verdict_seconds": 86400 }, "flow": "verdict-first", "terms": { "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ + c1147743326dabd45bda546dc62", "parameters": { "...": "the profile's; not read here" } }, "challenge": { @@ -1221,11 +1226,6 @@ Internet-Draft PACT September 2026 Figure 3: A Verifiable Task Contract, signatures abbreviated - Digests are elided here; the reference repository's values are in - Section 15. The parameters object is shown elided on purpose: - nothing in this document depends on what is in it. - - @@ -1234,6 +1234,10 @@ Sharma Expires 20 March 2027 [Page 22] Internet-Draft PACT September 2026 + Digests are elided here; the reference repository's values are in + Section 15. The parameters object is shown elided on purpose: + nothing in this document depends on what is in it. + 5.1. Hash Commitments and Content Conveyance Every URI carried inside hash-committed content MUST be accompanied @@ -1281,10 +1285,6 @@ Internet-Draft PACT September 2026 - - - - Sharma Expires 20 March 2027 [Page 23] Internet-Draft PACT September 2026 @@ -1375,17 +1375,48 @@ Internet-Draft PACT September 2026 entry, the Facilitator records deadline-passed (Section 4.2). No window opens, because there is nothing to challenge. + + + + + + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 25] + +Internet-Draft PACT September 2026 + + + ========== NOTE: '\' line wrapping per RFC 8792 =========== + { "pact": "0.2", "type": "Delivery", "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ + 5fbbd4ebace4fb980f1c2", "work_hash": "sha256:9c1f...", "work_uri": "https://cdn.dataforge.example/o/9c1f", "input_hash": "sha256:41ab...", "evidence": { "profile": "acceptance", - "instrument_hash":"sha256:", + "instrument_hash":"sha256:0bdde1ab6b081d2b4bda580c5393756ae95\ + c10b8351c9c55eb9316416265fc1b", "results_hash": "sha256:7e02...", "results_uri": "https://cdn.dataforge.example/o/7e02" }, @@ -1394,14 +1425,6 @@ Internet-Draft PACT September 2026 Figure 4: A Delivery for a T0-reexec contract, acceptance profile - - - -Sharma Expires 20 March 2027 [Page 25] - -Internet-Draft PACT September 2026 - - The -01 revision said that a Buyer countersignature over the Delivery constituted a receipt. The Delivery's signing member is a single object, so no second signature could be carried, and the sentence is @@ -1427,6 +1450,14 @@ Internet-Draft PACT September 2026 contract, a PASS changes nothing. no-window: No window opens and no Verdict is accepted; delivered is + + + +Sharma Expires 20 March 2027 [Page 26] + +Internet-Draft PACT September 2026 + + followed by the terminal path. The -01 revision had four release modes, named for when value moved. @@ -1447,25 +1478,18 @@ Internet-Draft PACT September 2026 object, media type application/vnd.pact.verdict+json, with the members in Section 3.5, signed once. - - - - - - -Sharma Expires 20 March 2027 [Page 26] - -Internet-Draft PACT September 2026 - + ========== NOTE: '\' line wrapping per RFC 8792 =========== { "pact": "0.2", "type": "Verdict", "vtc_id": "vtc_7f3a91", - "delivery_hash": "sha256:", + "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ + f8aaaf2f186702504fba242ffb", "outcome": "PASS", "profile": "acceptance", - "instrument_hash": "sha256:", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\ + 10b8351c9c55eb9316416265fc1b", "results_hash": "sha256:7e02...", "evaluated_at": "2026-11-10T09:14:22Z", "signature": { "protected": "...", "signature": "..." } @@ -1482,6 +1506,14 @@ Internet-Draft PACT September 2026 not match that entry, or whose profile or instrument_hash does not match the contract (verdict-nonconformant); one received in a state the table in Section 4.2 does not list for it, or under the no-window + + + +Sharma Expires 20 March 2027 [Page 27] + +Internet-Draft PACT September 2026 + + flow (wrong-state); and one carrying challenge_hash that names no pending Challenge, or omitting it while the contract is DISPUTED (verdict-nonconformant). A Verdict that answers a Challenge @@ -1501,19 +1533,6 @@ Internet-Draft PACT September 2026 window, so that the contract can still be challenged and can still end. What a lapsed Verdict costs anyone is the profile's. - - - - - - - - -Sharma Expires 20 March 2027 [Page 27] - -Internet-Draft PACT September 2026 - - 7.3. Challenges A Challenge is a JSON object, media type application/ @@ -1542,14 +1561,27 @@ Internet-Draft PACT September 2026 document says nothing further about it. Section 17.14 discusses what a deposit does and does not prevent. + + + + +Sharma Expires 20 March 2027 [Page 28] + +Internet-Draft PACT September 2026 + + + ========== NOTE: '\' line wrapping per RFC 8792 =========== + { "pact": "0.2", "type": "Challenge", "vtc_id": "vtc_7f3a91", - "delivery_hash": "sha256:", + "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ + f8aaaf2f186702504fba242ffb", "proof": { "profile": "acceptance", - "instrument_hash": "sha256:", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\ + 5c10b8351c9c55eb9316416265fc1b", "results_hash": "sha256:a91e...", "results_uri": "https://watch.example/o/a91e", "failing_checks": ["schema_valid_rate", "row_count_min"] @@ -1560,17 +1592,7 @@ Internet-Draft PACT September 2026 Figure 6: A Challenge under the acceptance profile - - - - - -Sharma Expires 20 March 2027 [Page 28] - -Internet-Draft PACT September 2026 - - -7.4. Disputes and Lapses +7.4. Disputes and Lapses A contract with a pending Challenge is DISPUTED. It leaves that state when a Verdict answers the Challenge, or when @@ -1581,6 +1603,29 @@ Internet-Draft PACT September 2026 its own account, and MUST NOT record window-closed until none is pending. + + + + + + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 29] + +Internet-Draft PACT September 2026 + + Buyer Facilitator Verifier Challenger | | | | | |<-- POST Verdict | | @@ -1616,24 +1661,28 @@ Internet-Draft PACT September 2026 discovered is one service's capabilities, not an agent's identity, skills or endpoints. + A Facilitator SHOULD publish a JSON document, media type application/ + vnd.pact.facilitator+json, with the members in Section 3.9, at the + path /.well-known/pact-facilitator of its origin. The document MUST + be served over HTTPS. It MUST be signed, and the signature MUST + verify against a key bound to the identifier in facilitator. An + unsigned capability document is not usable for contract formation, + because terms_profiles determines which terms a party can name and + expect to be evaluated. + -Sharma Expires 20 March 2027 [Page 29] + + +Sharma Expires 20 March 2027 [Page 30] Internet-Draft PACT September 2026 - A Facilitator SHOULD publish a JSON document, media type application/ - vnd.pact.facilitator+json, with the members in Section 3.9, at the - path /.well-known/pact-facilitator of its origin. The document MUST - be served over HTTPS. It MUST be signed, and the signature MUST - verify against a key bound to the identifier in facilitator. An - unsigned capability document is not usable for contract formation, - because terms_profiles determines which terms a party can name and - expect to be evaluated. + ========== NOTE: '\' line wrapping per RFC 8792 =========== { "pact": "0.2", @@ -1649,7 +1698,8 @@ Internet-Draft PACT September 2026 "terms_profiles": [ { "id": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:" } + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f\ + 5fc1147743326dabd45bda546dc62" } ], "max_contract_value": { "amount": "50000.00", "currency": "USDC" }, @@ -1672,16 +1722,6 @@ Internet-Draft PACT September 2026 whose vectors (Section 12.1) its own implementation does not reproduce. - - - - - -Sharma Expires 20 March 2027 [Page 30] - -Internet-Draft PACT September 2026 - - 9. Verification Profiles A contract names both a tier, which says what class of evidence is @@ -1690,6 +1730,14 @@ Internet-Draft PACT September 2026 deterministic re-execution; T1-tee, hardware attestation per [RFC9334]; T2-zkml, a proof of inference; and T3-jury, staked arbitration. Tiers are a vocabulary. Three profiles are defined + + + +Sharma Expires 20 March 2027 [Page 31] + +Internet-Draft PACT September 2026 + + below by name; any other is identified by a URI under its definer's control, and this document creates no registry for them. The distinction matters because the tier name does not determine how much @@ -1725,19 +1773,6 @@ Internet-Draft PACT September 2026 the computation is deterministic and the environment is pinned; see Section 17.10. Cost: approximately the work. - - - - - - - - -Sharma Expires 20 March 2027 [Page 31] - -Internet-Draft PACT September 2026 - - 9.1. Verifier Independence and Identifier Normalization Independence is a relation between the party that signs a Verdict and @@ -1750,6 +1785,15 @@ Internet-Draft PACT September 2026 revision stated as a prohibition on the Facilitator's conduct; it is an identifier comparison and is stated as one. + + + + +Sharma Expires 20 March 2027 [Page 32] + +Internet-Draft PACT September 2026 + + Party identifiers MUST be normalized before comparison, and the normalization MUST fold toward identifying the same party: strip leading and trailing whitespace; lower-case the scheme and, for @@ -1785,15 +1829,6 @@ Internet-Draft PACT September 2026 Figure 9: A contract tree. B is Seller above and Buyer below. - - - - -Sharma Expires 20 March 2027 [Page 32] - -Internet-Draft PACT September 2026 - - 10.1. Binding a Child to Its Parent A subcontract carries parent, a top-level member with the parent's @@ -1803,6 +1838,18 @@ Internet-Draft PACT September 2026 carried this member inside the member it has since removed; it is structural and is now where structure is. + + + + + + + +Sharma Expires 20 March 2027 [Page 33] + +Internet-Draft PACT September 2026 + + The child's Facilitator need not resolve the parent, and across Facilitators it often cannot. It MUST record parent as signed, and MUST allow the identifier in parent.facilitator to retrieve the @@ -1840,16 +1887,6 @@ Internet-Draft PACT September 2026 child.parties.buyer == parent.parties.seller child.parent.vtc_hash == digest(parent) - - - - - -Sharma Expires 20 March 2027 [Page 33] - -Internet-Draft PACT September 2026 - - Without the first check any party may name any contract as its parent. The attack is cheap and asymmetric: name a competitor's contract as parent, subcontract a trivial task to yourself, fail it, @@ -1861,6 +1898,14 @@ Internet-Draft PACT September 2026 holds the child's Outcome Record. It may obtain that record itself, by retrieving it from the child's Facilitator, or receive it from the parent's Seller by a POST to the same resource (Section 13). Either + + + +Sharma Expires 20 March 2027 [Page 34] + +Internet-Draft PACT September 2026 + + way the Facilitator MUST verify the record's Facilitator signature against a key bound to the identifier the registration recorded, and MUST verify that its vtc_hash is the registered child's digest, @@ -1898,14 +1943,6 @@ Internet-Draft PACT September 2026 Challenge can only be received before closes_at, so no sequence of events carries a contract past its L except waiting for its own children. A Facilitator MUST refuse to register a child unless - - - -Sharma Expires 20 March 2027 [Page 34] - -Internet-Draft PACT September 2026 - - L(child) is earlier than L(parent), and MUST record child-unresolved for a registered child no later than the first opportunity after L(child) if it holds no Outcome Record for it by then. @@ -1917,6 +1954,14 @@ Internet-Draft PACT September 2026 the waiting state is what makes the rule honest about the case where a child is late anyway. + + + +Sharma Expires 20 March 2027 [Page 35] + +Internet-Draft PACT September 2026 + + parent |== work ==|= verdict =|= window =|= dispute =| ^ L(parent) child |== work ==|= vrd =|= win =|= dsp =| @@ -1957,25 +2002,42 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 35] + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 36] Internet-Draft PACT September 2026 + ========== NOTE: '\' line wrapping per RFC 8792 =========== + { "pact": "0.2", "type": "ContractStatus", "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ + 5fbbd4ebace4fb980f1c2", "state": "WINDOW_OPEN", "trace": [ { "event": "accepted", "at": "2026-11-01T10:00:00Z", - "object": "sha256:" }, + "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ + 225fbbd4ebace4fb980f1c2" }, { "event": "funded", "at": "2026-11-01T10:00:00Z" }, { "event": "delivered", "at": "2026-11-10T08:30:12Z", - "object": "sha256:" }, + "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ + aaf2f186702504fba242ffb" }, { "event": "verdict", "at": "2026-11-10T09:14:30Z", - "object": "sha256:", "outcome": "PASS" }, + "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ + e09452a74bac15e6752ca81", "outcome": "PASS" }, { "event": "window-opened", "at": "2026-11-10T09:14:30Z", "closes_at": "2026-11-10T10:14:30Z" } ], @@ -1999,6 +2061,19 @@ Internet-Draft PACT September 2026 replaces that: the posted object is not echoed, and everything in the response is inside the Facilitator's signature. + + + + + + + + +Sharma Expires 20 March 2027 [Page 37] + +Internet-Draft PACT September 2026 + + 12. Outcome Records An Outcome Record records what a contract did. It is a JSON object, @@ -2010,14 +2085,6 @@ Internet-Draft PACT September 2026 A Facilitator MUST issue exactly one Outcome Record for every contract that reaches a terminal state, including SETTLED and ABANDONED, MUST sign it, and MUST NOT require the signature of any - - - -Sharma Expires 20 March 2027 [Page 36] - -Internet-Draft PACT September 2026 - - other party on it. The -00 revision's record needed the signature of the party it recorded against, which made a reputation layer built on it structurally incapable of recording a loss. The Facilitator @@ -2026,59 +2093,14 @@ Internet-Draft PACT September 2026 fabricated history requires a Facilitator's key rather than two identities. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Sharma Expires 20 March 2027 [Page 37] - -Internet-Draft PACT September 2026 - + ========== NOTE: '\' line wrapping per RFC 8792 =========== { "pact": "0.2", "type": "OutcomeRecord", "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ + 5fbbd4ebace4fb980f1c2", "parties": { "buyer": "did:web:acme.example", "seller": "did:web:dataforge.example", @@ -2089,19 +2111,34 @@ Internet-Draft PACT September 2026 "work_hash": "sha256:9c1f...", "trace": [ { "event": "accepted", "at": "...", - "object": "sha256:" }, + "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ + 225fbbd4ebace4fb980f1c2" }, { "event": "funded", "at": "..." }, { "event": "delivered", "at": "...", - "object": "sha256:" }, + "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ + aaf2f186702504fba242ffb" }, { "event": "verdict", "at": "...", - "object": "sha256:", "outcome": "PASS" }, + "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ + e09452a74bac15e6752ca81", "outcome": "PASS" }, { "event": "window-opened", "at": "...", "closes_at": "..." }, { "event": "challenge", "at": "...", - "object": "sha256:" }, + + + +Sharma Expires 20 March 2027 [Page 38] + +Internet-Draft PACT September 2026 + + + "object": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df\ + 6cfef12d12d1340c15e5d85" }, { "event": "verdict", "at": "...", - "object": "sha256:", "outcome": "FAIL", - "answers": "sha256:", - "supersedes": "sha256:" }, + "object": "sha256:012bab194f444012f87ff8986419537f9de9f3dfe\ + 845c6623ac6a7488fb98b73", "outcome": "FAIL", + "answers": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4d\ + f6cfef12d12d1340c15e5d85", + "supersedes": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51\ + cee8e09452a74bac15e6752ca81" }, { "event": "children-final", "at": "..." }, { "event": "terminal", "at": "...", "state": "SETTLED", "challenge_upheld": true } @@ -2109,7 +2146,8 @@ Internet-Draft PACT September 2026 "terms_result": { "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ + c1147743326dabd45bda546dc62", "currency": "USDC", "transfers": [ { "event": 8, "from": "...", "to": "...", "amount": "...", @@ -2122,14 +2160,6 @@ Internet-Draft PACT September 2026 Figure 12: An Outcome Record for a contract that reached SETTLED on an upheld Challenge - - - -Sharma Expires 20 March 2027 [Page 38] - -Internet-Draft PACT September 2026 - - The record carries one signature, the Facilitator's. The Seller did not consent to this record and its consent is not required. The transfers entries are elided here because their content is the @@ -2146,6 +2176,16 @@ Internet-Draft PACT September 2026 code (a string the profile defines, naming the schedule line that produced the entry). + + + + + +Sharma Expires 20 March 2027 [Page 39] + +Internet-Draft PACT September 2026 + + This document defines the form of the list and two arithmetic facts about it, and nothing about what any entry means. Over the accounts and opening amounts the profile declares for the contract @@ -2179,13 +2219,6 @@ Internet-Draft PACT September 2026 therefore fixed by n alone, and two implementations that agree on D agree on the root. - - -Sharma Expires 20 March 2027 [Page 39] - -Internet-Draft PACT September 2026 - - The domain separation is not optional. Without distinct prefixes an attacker can present an interior node as though it were a leaf, and so claim an inclusion proof for a subtree that never existed. @@ -2200,6 +2233,15 @@ Internet-Draft PACT September 2026 signatures removed, which let a record be re-signed without changing the root. + + + + +Sharma Expires 20 March 2027 [Page 40] + +Internet-Draft PACT September 2026 + + 13. Protocol Endpoints This section specifies the operations a Facilitator exposes. Base @@ -2234,14 +2276,6 @@ Internet-Draft PACT September 2026 All requests and responses use the media types defined in Section 19. All requests MUST be made over HTTPS, following the recommendations of [RFC9325]. Status codes are as defined in [RFC9110]. A Delivery - - - -Sharma Expires 20 March 2027 [Page 40] - -Internet-Draft PACT September 2026 - - and a Challenge are answered 202 (Accepted) rather than 201 because acceptance of the bytes is not acceptance of the work; what follows depends on a Verdict the Facilitator does not itself produce. @@ -2255,6 +2289,15 @@ Internet-Draft PACT September 2026 an HTTP-layer authentication in addition. Retrieval is discussed in Section 17.12. + + + + +Sharma Expires 20 March 2027 [Page 41] + +Internet-Draft PACT September 2026 + + 13.1. Proposing a Contract The request body is a VTC carrying the signatures of both parties @@ -2291,18 +2334,26 @@ Internet-Draft PACT September 2026 resource, and MUST respond 200 (OK) with the current Status rather than creating a second resource or reporting a conflict. + Where a POST carries the same object id as an existing resource but a + different digest, the Facilitator MUST respond 409 (Conflict) + (object-conflict). Retrying a submission is therefore always safe, + and altering one never is. + -Sharma Expires 20 March 2027 [Page 41] + + + + + + + + +Sharma Expires 20 March 2027 [Page 42] Internet-Draft PACT September 2026 - Where a POST carries the same object id as an existing resource but a - different digest, the Facilitator MUST respond 409 (Conflict) - (object-conflict). Retrying a submission is therefore always safe, - and altering one never is. - 13.3. Error Responses A Facilitator MUST report failures using [RFC9457] problem details, @@ -2331,29 +2382,6 @@ Internet-Draft PACT September 2026 13.4. Exchange - - - - - - - - - - - - - - - - - - -Sharma Expires 20 March 2027 [Page 42] - -Internet-Draft PACT September 2026 - - Buyer/Seller Facilitator Verifier | | | |-- POST {contract} --->| | @@ -2374,6 +2402,14 @@ Internet-Draft PACT September 2026 Figure 13: HTTP exchange for the flow in Figure 1 + + + +Sharma Expires 20 March 2027 [Page 43] + +Internet-Draft PACT September 2026 + + 14. Conformance Every rule a PACT conformance checker enforces is stated in this @@ -2400,16 +2436,6 @@ Internet-Draft PACT September 2026 * The protected header MUST carry alg, kid and typ. - - - - - -Sharma Expires 20 March 2027 [Page 43] - -Internet-Draft PACT September 2026 - - * alg MUST be ES256 or ES384 [RFC7518], or EdDSA [RFC8037] with an Ed25519 key; a verifier MAY also accept Ed448. A verifier MUST reject any other value, and MUST reject none. Absent an allowlist @@ -2430,6 +2456,16 @@ Internet-Draft PACT September 2026 character for character. Explicit typing follows Section 3.11 of [RFC8725]. + + + + + +Sharma Expires 20 March 2027 [Page 44] + +Internet-Draft PACT September 2026 + + * A signatures array MUST be sorted by the normalized kid of its entries (Section 9.1), ties broken by the unnormalized kid, both compared as sequences of Unicode code points; a verifier MUST @@ -2458,14 +2494,6 @@ Internet-Draft PACT September 2026 the verification method its fragment identifies. Examples in this document use did:web [DID-WEB]; no method is required or excluded. - - - -Sharma Expires 20 March 2027 [Page 44] - -Internet-Draft PACT September 2026 - - * An https: identifier dereferences, over TLS, to a JWK Set [RFC7517]; the verifier selects the key whose kid member equals the fragment. @@ -2484,6 +2512,16 @@ Internet-Draft PACT September 2026 * parties.buyer and parties.seller MUST be distinct after the normalization in Section 9.1 (parties-not-distinct). + + + + + +Sharma Expires 20 March 2027 [Page 45] + +Internet-Draft PACT September 2026 + + * A contract MUST carry exactly one verifying signature whose kid covers parties.buyer, exactly one whose kid covers parties.seller, and no other (signature-missing, unexpected-signer). A count of @@ -2511,94 +2549,96 @@ Internet-Draft PACT September 2026 every object MUST validate against the schema published for its media type (schema-invalid). +14.3. Test Vectors + Each rule above has an accepting and a rejecting form. A conformance + suite built from this section alone, with no reference to any + implementation, should reach the same verdicts. Rejecting vectors + name the rule they violate. + +======+=========================================+==========+ + | ID | Mutation from a valid object | Expect | + +======+=========================================+==========+ + | V-01 | unmodified valid VTC | accept | + +------+-----------------------------------------+----------+ + | V-02 | alg set to none | reject | + +------+-----------------------------------------+----------+ + | V-03 | alg set to HS256 | reject | + +------+-----------------------------------------+----------+ + | V-04 | kid moved outside the protected header | reject | + +------+-----------------------------------------+----------+ + | V-05 | typ of a Delivery on a VTC signature | reject | + +------+-----------------------------------------+----------+ + | V-06 | buyer and seller set to the same | reject | - -Sharma Expires 20 March 2027 [Page 45] +Sharma Expires 20 March 2027 [Page 46] Internet-Draft PACT September 2026 -14.3. Test Vectors - - Each rule above has an accepting and a rejecting form. A conformance - suite built from this section alone, with no reference to any - implementation, should reach the same verdicts. Rejecting vectors - name the rule they violate. - - +======+=================================================+==========+ - | ID | Mutation from a valid object | Expect | - +======+=================================================+==========+ - | V-01 | unmodified valid VTC | accept | - +------+-------------------------------------------------+----------+ - | V-02 | alg set to none | reject | - +------+-------------------------------------------------+----------+ - | V-03 | alg set to HS256 | reject | - +------+-------------------------------------------------+----------+ - | V-04 | kid moved outside the protected header | reject | - +------+-------------------------------------------------+----------+ - | V-05 | typ of a Delivery on a VTC signature | reject | - +------+-------------------------------------------------+----------+ - | V-06 | buyer and seller set to the same identifier | reject | - +------+-------------------------------------------------+----------+ - | V-07 | buyer and seller differing only by trailing | reject | - | | "/" | | - +------+-------------------------------------------------+----------+ - | V-08 | two signatures, both from the buyer | reject | - +------+-------------------------------------------------+----------+ - | V-09 | window_seconds of 0 | reject | - +------+-------------------------------------------------+----------+ - | V-10 | acceptance as an empty object | reject | - +------+-------------------------------------------------+----------+ - | V-11 | harness_uri with harness_hash removed | reject | - +------+-------------------------------------------------+----------+ - | V-12 | terms.profile_hash not advertised by the | reject | - | | Facilitator | | - +------+-------------------------------------------------+----------+ - | V-13 | terms.parameters failing the profile's | reject | - | | schema | | - +------+-------------------------------------------------+----------+ - | V-14 | Delivery with evidence absent | reject, | - | | | no entry | - +------+-------------------------------------------------+----------+ - | V-15 | child whose buyer is not the parent's | reject | - | | seller | | - +------+-------------------------------------------------+----------+ - | V-16 | child with L(child) not earlier than | reject | - | | L(parent) | | - +------+-------------------------------------------------+----------+ + | | identifier | | + +------+-----------------------------------------+----------+ + | V-07 | buyer and seller differing only by | reject | + | | trailing "/" | | + +------+-----------------------------------------+----------+ + | V-08 | two signatures, both from the buyer | reject | + +------+-----------------------------------------+----------+ + | V-09 | window_seconds of 0 | reject | + +------+-----------------------------------------+----------+ + | V-10 | acceptance as an empty object | reject | + +------+-----------------------------------------+----------+ + | V-11 | harness_uri with harness_hash removed | reject | + +------+-----------------------------------------+----------+ + | V-12 | terms.profile_hash not advertised by | reject | + | | the Facilitator | | + +------+-----------------------------------------+----------+ + | V-13 | terms.parameters failing the profile's | reject | + | | schema | | + +------+-----------------------------------------+----------+ + | V-14 | Delivery with evidence absent | reject, | + | | | no entry | + +------+-----------------------------------------+----------+ + | V-15 | child whose buyer is not the parent's | reject | + | | seller | | + +------+-----------------------------------------+----------+ + | V-16 | child with L(child) not earlier than | reject | + | | L(parent) | | + +------+-----------------------------------------+----------+ + | V-17 | Verdict signed by the seller | reject | + +------+-----------------------------------------+----------+ + | V-18 | object keys ordered by code point, with | digest | + | | a supplementary-plane key | mismatch | + +------+-----------------------------------------+----------+ + | V-19 | buyer and seller differing only in the | accept | + | | case of a did:web path | | + +------+-----------------------------------------+----------+ + | V-20 | object carrying a member this document | reject | + | | does not define for it | | + +------+-----------------------------------------+----------+ + | V-21 | signatures not sorted by normalized kid | reject | + +------+-----------------------------------------+----------+ + | V-22 | ECDSA signature with s above n/2 | reject | + +------+-----------------------------------------+----------+ + | V-23 | Verdict with delivery_hash computed | reject | + | | over the Delivery without its signature | | + +------+-----------------------------------------+----------+ + | V-24 | Outcome Record whose transfers overdraw | reject | + | | an account of the profile | | -Sharma Expires 20 March 2027 [Page 46] +Sharma Expires 20 March 2027 [Page 47] Internet-Draft PACT September 2026 - | V-17 | Verdict signed by the seller | reject | - +------+-------------------------------------------------+----------+ - | V-18 | object keys ordered by code point, with a | digest | - | | supplementary-plane key | mismatch | - +------+-------------------------------------------------+----------+ - | V-19 | buyer and seller differing only in the case | accept | - | | of a did:web path | | - +------+-------------------------------------------------+----------+ - | V-20 | object carrying a member this document does | reject | - | | not define for it | | - +------+-------------------------------------------------+----------+ - | V-21 | signatures not sorted by normalized kid | reject | - +------+-------------------------------------------------+----------+ - | V-22 | ECDSA signature with s above n/2 | reject | - +------+-------------------------------------------------+----------+ - | V-23 | Verdict with delivery_hash computed over | reject | - | | the Delivery without its signature | | - +------+-------------------------------------------------+----------+ - | V-24 | Outcome Record whose transfers overdraw an | reject | - | | account of the profile | | - +------+-------------------------------------------------+----------+ + +------+-----------------------------------------+----------+ + | V-25 | a number serialized by the host | digest | + | | language's default formatter, such as | mismatch | + | | 1.0 for the float one | | + +------+-----------------------------------------+----------+ Table 3: Conformance vectors @@ -2608,7 +2648,11 @@ Internet-Draft PACT September 2026 point, which agrees with the required UTF-16 order for every ASCII key and so passes every vector an implementer would think to write. V-19 is the opposite mistake, folding more than Section 9.1 allows, - and the -01 reference validator made it. + and the -01 reference validator made it. V-25 is the number half of + the V-18 mistake: [RFC8785] prints numbers as ECMAScript does, so the + float one is 1 and never 1.0. The -02 reference canonicalizer + printed 1.0 until this vector caught it, and every digest in + Section 15 changed when it was fixed. 15. Worked Example @@ -2626,20 +2670,26 @@ Internet-Draft PACT September 2026 The digests carried by the reference TaskSpec, contract and profile are: + ========== NOTE: '\' line wrapping per RFC 8792 =========== + spec_hash sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef111\ + d27ff6bee37f05531823b72 + criteria_hash sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b83\ + 51c9c55eb9316416265fc1b + profile_hash sha256:00d71829f6f9192b43b929d0154a6eb409f5fc114\ + 7743326dabd45bda546dc62 + vtc_hash sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ + 225fbbd4ebace4fb980f1c2 + delivery_hash sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ + aaf2f186702504fba242ffb -Sharma Expires 20 March 2027 [Page 47] + +Sharma Expires 20 March 2027 [Page 48] Internet-Draft PACT September 2026 - spec_hash sha256: - criteria_hash sha256: - profile_hash sha256: - vtc_hash sha256: - delivery_hash sha256: - criteria_hash is the manifest digest of Section 5.1 over the acceptance instrument bundle, and the same value appears as acceptance.harness_hash inside the TaskSpec, so the instrument is @@ -2670,7 +2720,7 @@ Internet-Draft PACT September 2026 One implementation is known to the author, and the author wrote it: https://github.com/pact-spec/spec, under the Revised BSD licence. At tag v0.2.0 it comprises the object schemas, the examples whose - digests Section 15 prints, a conformance validator that runs + digests Section 15 prints, a conformance validator that runs 103 checks including every vector of Section 14.3, a Facilitator serving the endpoints of Section 13 with the profile of Appendix A, and clients for the other roles. Its previous tag, v0.1.0, implemented @@ -2679,17 +2729,6 @@ Internet-Draft PACT September 2026 Section 1.4 has been tested, and this document claims no interoperability. - - - - - - -Sharma Expires 20 March 2027 [Page 48] - -Internet-Draft PACT September 2026 - - 17. Security Considerations Most of what follows was found by adversarial review of earlier @@ -2702,45 +2741,6 @@ Internet-Draft PACT September 2026 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Sharma Expires 20 March 2027 [Page 49] Internet-Draft PACT September 2026 @@ -3505,6 +3505,11 @@ Internet-Draft PACT September 2026 RFC 7942, DOI 10.17487/RFC7942, July 2016, . + [RFC8792] Watsen, K., Auerswald, E., Farrel, A., and Q. Wu, + "Handling Long Lines in Content of Internet-Drafts and + RFCs", RFC 8792, DOI 10.17487/RFC8792, June 2020, + . + [RFC8555] Barnes, R., Hoffman-Andrews, J., McCarney, D., and J. Kasten, "Automatic Certificate Management Environment (ACME)", RFC 8555, DOI 10.17487/RFC8555, March 2019, @@ -3516,11 +3521,6 @@ Internet-Draft PACT September 2026 (CRL) Profile", RFC 5280, DOI 10.17487/RFC5280, May 2008, . - [RFC3647] Chokhani, S., Ford, W., Sabett, R., Merrill, C., and S. - Wu, "Internet X.509 Public Key Infrastructure Certificate - Policy and Certification Practices Framework", RFC 3647, - DOI 10.17487/RFC3647, November 2003, - . @@ -3530,6 +3530,12 @@ Sharma Expires 20 March 2027 [Page 63] Internet-Draft PACT September 2026 + [RFC3647] Chokhani, S., Ford, W., Sabett, R., Merrill, C., and S. + Wu, "Internet X.509 Public Key Infrastructure Certificate + Policy and Certification Practices Framework", RFC 3647, + DOI 10.17487/RFC3647, November 2003, + . + [RFC2801] Burdett, D., "Internet Open Trading Protocol - IOTP Version 1.0", RFC 2801, DOI 10.17487/RFC2801, April 2000, . @@ -3572,12 +3578,6 @@ Internet-Draft PACT September 2026 . - [I-D.sahu-agent-action-receipts] - sahu, N., "Signed, Hash-Chained Action Receipts for AI - Agents", Work in Progress, Internet-Draft, draft-sahu- - agent-action-receipts-00, 16 August 2026, - . @@ -3586,6 +3586,13 @@ Sharma Expires 20 March 2027 [Page 64] Internet-Draft PACT September 2026 + [I-D.sahu-agent-action-receipts] + sahu, N., "Signed, Hash-Chained Action Receipts for AI + Agents", Work in Progress, Internet-Draft, draft-sahu- + agent-action-receipts-00, 16 August 2026, + . + [I-D.mih-sato-agent-accountability-composition] Mih, S., Sato, Schrock, I., Bu, S., and A. Sokolov, "Agent Accountability: Composition and Conformance", Work in @@ -3628,13 +3635,6 @@ Internet-Draft PACT September 2026 pp. 85-90, 2008, . - [POLINSKY99] - Polinsky, A.M. and S. Shavell, "Public Enforcement of - Law", Encyclopedia of Law and Economics, entry 8000, - Edward Elgar. The result is attributed therein to Bentham - (1789), 1999. - - Sharma Expires 20 March 2027 [Page 65] @@ -3642,6 +3642,12 @@ Sharma Expires 20 March 2027 [Page 65] Internet-Draft PACT September 2026 + [POLINSKY99] + Polinsky, A.M. and S. Shavell, "Public Enforcement of + Law", Encyclopedia of Law and Economics, entry 8000, + Edward Elgar. The result is attributed therein to Bentham + (1789), 1999. + [SP800-186] National Institute of Standards and Technology, "Recommendations for Discrete Logarithm-based @@ -3684,12 +3690,6 @@ A.2. Parameters cap: amount, required. The most that leaves the Seller's accounts under this contract. - restitution_basis: string, required. released or price. - - remainder_to: string, optional. buyer or sink; sink when absent. - - verifier_fee: amount, optional. Paid from the fund at each Verdict; - 0.00 when absent. @@ -3698,6 +3698,13 @@ Sharma Expires 20 March 2027 [Page 66] Internet-Draft PACT September 2026 + restitution_basis: string, required. released or price. + + remainder_to: string, optional. buyer or sink; sink when absent. + + verifier_fee: amount, optional. Paid from the fund at each Verdict; + 0.00 when absent. + principal_on: string, required. The event at which the price moves to the Seller: verdict (a PASS Verdict), delivered, or window- closed. @@ -3740,13 +3747,6 @@ A.5. Schedule contract and the trace prefix; "released" is the sum of principal entries emitted so far. - funded: buyer to escrow, P, lock; seller to bond, B, bond; buyer to - fund, verification_fund, fund. - - delivered: if principal_on is delivered: escrow to seller, the - escrow balance, principal. - - Sharma Expires 20 March 2027 [Page 67] @@ -3754,6 +3754,12 @@ Sharma Expires 20 March 2027 [Page 67] Internet-Draft PACT September 2026 + funded: buyer to escrow, P, lock; seller to bond, B, bond; buyer to + fund, verification_fund, fund. + + delivered: if principal_on is delivered: escrow to seller, the + escrow balance, principal. + verdict: fund to verifier, the lesser of verifier_fee and the fund balance, verification; then if the outcome is PASS, no Challenge is answered, and principal_on is verdict: escrow to seller, the @@ -3799,12 +3805,6 @@ Internet-Draft PACT September 2026 - - - - - - Sharma Expires 20 March 2027 [Page 68] Internet-Draft PACT September 2026 @@ -3877,7 +3877,12 @@ Appendix B. Changes from -01 first. * The pact version is 0.2 and every committed digest changed - (Section 15). + (Section 15). The -01 digests were computed by a canonicalizer + that serialized the number one as 1.0, which [RFC8785] does not + allow, so the spec_hash and vtc_hash the -01 revision printed are + not what a conforming implementation computes over the -01 example + objects. V-25 in Section 14.3 pins the rule and the -02 examples + were minted after the correction. * The liability member is gone. A contract carries terms: a profile URI, a digest over the profile's bundle, and an opaque parameter @@ -3909,11 +3914,6 @@ Appendix B. Changes from -01 (Section 4.2); RELEASING and PROPOSED are gone, AWAITING_CHILDREN is added. - * delivery_hash covers the Delivery's signature; every digest covers - the signature set (Section 2). Signature sets are sorted and - ECDSA is low-S (Section 14.1). Merkle leaves cover signatures - (Section 12.2). - @@ -3922,6 +3922,11 @@ Sharma Expires 20 March 2027 [Page 70] Internet-Draft PACT September 2026 + * delivery_hash covers the Delivery's signature; every digest covers + the signature set (Section 2). Signature sets are sorted and + ECDSA is low-S (Section 14.1). Merkle leaves cover signatures + (Section 12.2). + * A nonconformant Delivery is refused and recorded nowhere; the -01 revision treated it as a FAIL Verdict (Section 6). The Buyer countersignature sentence is withdrawn. @@ -3965,11 +3970,6 @@ Acknowledgements verification tiers say how work is checked and never who checks it came from msaleme on the same thread. Rich Smith's A2A Settlement Extension was the clearest instance of the pattern the -01 revision - corrected, and he engaged with the critique on a2aproject/A2A - discussion 1576. - -Author's Address - @@ -3978,6 +3978,11 @@ Sharma Expires 20 March 2027 [Page 71] Internet-Draft PACT September 2026 + corrected, and he engaged with the critique on a2aproject/A2A + discussion 1576. + +Author's Address + Laxmikant Sharma Independent Email: laxsharma79@gmail.com @@ -4018,11 +4023,6 @@ Internet-Draft PACT September 2026 - - - - - diff --git a/draft/draft-laxsharma-pact-02.xml b/draft/draft-laxsharma-pact-02.xml index 5a97b86..5aacdb3 100644 --- a/draft/draft-laxsharma-pact-02.xml +++ b/draft/draft-laxsharma-pact-02.xml @@ -223,7 +223,16 @@ MUST order object keys by UTF-16 code unit as Section 3.2.3 requires. Sorting by Unicode code point is a common substitution; it agrees with the required order - throughout the Basic Multilingual Plane and diverges above it. + throughout the Basic Multilingual Plane and diverges above it. Numbers + MUST be serialized as Section 3.2.2.3 + requires, which is how ECMAScript prints them: the number one is + 1, whatever type held it, and never 1.0. + + Figures. A figure line that would exceed the page width is folded + with the single backslash strategy of , and a + figure that contains a fold begins with the header line that RFC + requires. The figure is read after unfolding; the digests it prints + are whole once the fold is removed. Object digest. The digest of an object is the string sha256: followed by the lowercase hexadecimal SHA-256 of the @@ -735,11 +744,11 @@ window-openedDELIVERED; then WINDOW_OPEN Under delivery-first, immediately after delivered; under verdict-first, immediately after a PASS verdict or after verdict-lapsed. closes_at (timestamp, required) is at plus challenge.window_seconds. verdictDELIVERED, WINDOW_OPEN or DISPUTED; then see the condition - object is the Verdict's digest; outcome (PASS or FAIL); answers (digest of the Challenge, when the Verdict carries challenge_hash); supersedes (digest of the Verdict it replaces, when one stood). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending. + object is the Verdict's digest; signer (the kid of its signature); outcome (PASS or FAIL); answers (digest of the Challenge, when the Verdict carries challenge_hash); supersedes (digest of the Verdict it replaces, when one stood). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending. verdict-lapsedDELIVERED; then WINDOW_OPEN Under verdict-first, verification.max_verdict_seconds have passed since delivered with no verdict. window-opened follows. challengeWINDOW_OPEN or DISPUTED; then DISPUTED - object is the Challenge's digest; costs copied from the Challenge when present. The Challenge passed before closes_at. + object is the Challenge's digest; signer (the kid of its signature); costs copied from the Challenge when present. The Challenge passed before closes_at. dispute-lapsedDISPUTED; then WINDOW_OPEN object is the Challenge's digest. challenge.max_dispute_seconds have passed since that challenge entry with no Verdict answering it. Leads to WINDOW_OPEN once no Challenge is pending; the earlier Verdict, if any, stands. window-closedWINDOW_OPEN; then AWAITING_CHILDREN @@ -792,6 +801,8 @@
    A Verifiable Task Contract, signatures abbreviated ", + "spec_hash": "sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef11\ + 1d27ff6bee37f05531823b72", "deadline": "2026-11-14T00:00:00Z" }, "price": { @@ -815,14 +827,16 @@ "verification": { "tier": "T0-reexec", "profile": "acceptance", - "criteria_hash": "sha256:", + "criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c539375\ + 6ae95c10b8351c9c55eb9316416265fc1b", "max_verdict_seconds": 86400 }, "flow": "verdict-first", "terms": { "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ + c1147743326dabd45bda546dc62", "parameters": { "...": "the profile's; not read here" } }, "challenge": { @@ -974,17 +988,21 @@
    A Delivery for a T0-reexec contract, acceptance profile ", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ + 5fbbd4ebace4fb980f1c2", "work_hash": "sha256:9c1f...", "work_uri": "https://cdn.dataforge.example/o/9c1f", "input_hash": "sha256:41ab...", "evidence": { "profile": "acceptance", - "instrument_hash":"sha256:", + "instrument_hash":"sha256:0bdde1ab6b081d2b4bda580c5393756ae95\ + c10b8351c9c55eb9316416265fc1b", "results_hash": "sha256:7e02...", "results_uri": "https://cdn.dataforge.example/o/7e02" }, @@ -1045,14 +1063,18 @@
    A Verdict ", + "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ + f8aaaf2f186702504fba242ffb", "outcome": "PASS", "profile": "acceptance", - "instrument_hash": "sha256:", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\ + 10b8351c9c55eb9316416265fc1b", "results_hash": "sha256:7e02...", "evaluated_at": "2026-11-10T09:14:22Z", "signature": { "protected": "...", "signature": "..." } @@ -1127,14 +1149,18 @@
    A Challenge under the acceptance profile ", + "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ + f8aaaf2f186702504fba242ffb", "proof": { "profile": "acceptance", - "instrument_hash": "sha256:", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\ + 5c10b8351c9c55eb9316416265fc1b", "results_hash": "sha256:a91e...", "results_uri": "https://watch.example/o/a91e", "failing_checks": ["schema_valid_rate", "row_count_min"] @@ -1208,6 +1234,8 @@
    https://settle.example/.well-known/pact-facilitator " } + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f\ + 5fc1147743326dabd45bda546dc62" } ], "max_contract_value": { "amount": "50000.00", "currency": "USDC" }, @@ -1488,20 +1517,26 @@
    A Contract Status after the Verdict of Figure 1 ", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ + 5fbbd4ebace4fb980f1c2", "state": "WINDOW_OPEN", "trace": [ { "event": "accepted", "at": "2026-11-01T10:00:00Z", - "object": "sha256:" }, + "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ + 225fbbd4ebace4fb980f1c2" }, { "event": "funded", "at": "2026-11-01T10:00:00Z" }, { "event": "delivered", "at": "2026-11-10T08:30:12Z", - "object": "sha256:" }, + "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ + aaf2f186702504fba242ffb" }, { "event": "verdict", "at": "2026-11-10T09:14:30Z", - "object": "sha256:", "outcome": "PASS" }, + "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ + e09452a74bac15e6752ca81", "outcome": "PASS" }, { "event": "window-opened", "at": "2026-11-10T09:14:30Z", "closes_at": "2026-11-10T10:14:30Z" } ], @@ -1548,11 +1583,14 @@
    An Outcome Record for a contract that reached SETTLED on an upheld Challenge ", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ + 5fbbd4ebace4fb980f1c2", "parties": { "buyer": "did:web:acme.example", "seller": "did:web:dataforge.example", @@ -1563,19 +1601,26 @@ "work_hash": "sha256:9c1f...", "trace": [ { "event": "accepted", "at": "...", - "object": "sha256:" }, + "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ + 225fbbd4ebace4fb980f1c2" }, { "event": "funded", "at": "..." }, { "event": "delivered", "at": "...", - "object": "sha256:" }, + "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ + aaf2f186702504fba242ffb" }, { "event": "verdict", "at": "...", - "object": "sha256:", "outcome": "PASS" }, + "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ + e09452a74bac15e6752ca81", "outcome": "PASS" }, { "event": "window-opened", "at": "...", "closes_at": "..." }, { "event": "challenge", "at": "...", - "object": "sha256:" }, + "object": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df\ + 6cfef12d12d1340c15e5d85" }, { "event": "verdict", "at": "...", - "object": "sha256:", "outcome": "FAIL", - "answers": "sha256:", - "supersedes": "sha256:" }, + "object": "sha256:012bab194f444012f87ff8986419537f9de9f3dfe\ + 845c6623ac6a7488fb98b73", "outcome": "FAIL", + "answers": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4d\ + f6cfef12d12d1340c15e5d85", + "supersedes": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51\ + cee8e09452a74bac15e6752ca81" }, { "event": "children-final", "at": "..." }, { "event": "terminal", "at": "...", "state": "SETTLED", "challenge_upheld": true } @@ -1583,7 +1628,8 @@ "terms_result": { "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ + c1147743326dabd45bda546dc62", "currency": "USDC", "transfers": [ { "event": 8, "from": "...", "to": "...", "amount": "...", @@ -1995,6 +2041,9 @@ Content-Type: application/problem+json reject V-24Outcome Record whose transfers overdraw an account of the profilereject + V-25a number serialized by the host language's + default formatter, such as 1.0 for the float + onedigest mismatch @@ -2004,7 +2053,12 @@ Content-Type: application/problem+json which agrees with the required UTF-16 order for every ASCII key and so passes every vector an implementer would think to write. V-19 is the opposite mistake, folding more than - allows, and the -01 reference validator made it. + allows, and the -01 reference validator made it. V-25 is the number + half of the V-18 mistake: prints numbers as + ECMAScript does, so the float one is 1 and never + 1.0. The -02 reference canonicalizer printed 1.0 + until this vector caught it, and every digest in + changed when it was fixed. @@ -2025,11 +2079,18 @@ Content-Type: application/problem+json are: - criteria_hash sha256: - profile_hash sha256: - vtc_hash sha256: - delivery_hash sha256: +========== NOTE: '\' line wrapping per RFC 8792 =========== + + spec_hash sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef111\ + d27ff6bee37f05531823b72 + criteria_hash sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b83\ + 51c9c55eb9316416265fc1b + profile_hash sha256:00d71829f6f9192b43b929d0154a6eb409f5fc114\ + 7743326dabd45bda546dc62 + vtc_hash sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ + 225fbbd4ebace4fb980f1c2 + delivery_hash sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ + aaf2f186702504fba242ffb ]]> criteria_hash is the manifest digest of @@ -2065,7 +2126,7 @@ Content-Type: application/problem+json https://github.com/pact-spec/spec, under the Revised BSD licence. At tag v0.2.0 it comprises the object schemas, the examples whose digests prints, a conformance validator that runs - <NN> checks including every vector of + 103 checks including every vector of , a Facilitator serving the endpoints of with the profile of , and clients for the other roles. Its @@ -2612,6 +2673,7 @@ Content-Type: application/problem+json + @@ -2878,7 +2940,14 @@ Content-Type: application/problem+json first.
    • The pact version is 0.2 and every committed digest - changed ().
    • + changed (). + The -01 digests were computed by a canonicalizer that serialized + the number one as 1.0, which + does not allow, so the spec_hash and vtc_hash + the -01 revision printed are not what a conforming implementation + computes over the -01 example objects. V-25 in + pins the rule and the -02 examples + were minted after the correction.
    • The liability member is gone. A contract carries terms: a profile URI, a digest over the profile's bundle, and an opaque parameter object (). The -01 diff --git a/examples/acceptance-harness/README.md b/examples/acceptance-harness/README.md index 1042732..07170c2 100644 --- a/examples/acceptance-harness/README.md +++ b/examples/acceptance-harness/README.md @@ -1,7 +1,7 @@ # Acceptance instrument (worked example) This directory is the acceptance instrument committed by -`verification.criteria_hash` in `cfb.json` and `vtc.json`. +`verification.criteria_hash` in `vtc.json`. `criteria_hash` is SHA-256 over the JCS-canonicalized manifest of this directory: a JSON object mapping each file's path, relative to this diff --git a/examples/acceptance-harness/test_acceptance.py b/examples/acceptance-harness/test_acceptance.py index 5f6805b..5c33bfa 100644 --- a/examples/acceptance-harness/test_acceptance.py +++ b/examples/acceptance-harness/test_acceptance.py @@ -1,7 +1,7 @@ """Acceptance instrument for the worked example (T0-reexec). This is the executable instrument committed by `verification.criteria_hash` -in cfb.json and vtc.json. It is deliberately real code rather than a +in vtc.json. It is deliberately real code rather than a description of code: the commitment must cover the bytes a verifier will run, not a sentence about them. diff --git a/examples/attestation.json b/examples/attestation.json deleted file mode 100644 index 386e807..0000000 --- a/examples/attestation.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "pact": "0.1", - "type": "WorkAttestation", - "vtc_id": "vtc_9f2c11", - "vtc_hash": "sha256:b2cccae00fc7b97ba2b6ef6356ff42c006ba661e0249d7eaa8baff3de553870b", - "parties": { - "buyer": "did:web:buyer.example:agents:procure-1", - "seller": "did:web:dataforge.example:agents:etl-3", - "facilitator": "did:web:settle.example" - }, - "subject": "did:web:dataforge.example:agents:etl-3", - "role": "seller", - "outcome": "performed", - "work_hash": "sha256:9c1f2ab4e7d0355f1b8c6e4a92d70f3ab5c81e46d92f0a7b3c5e18d4f60a2b79", - "amounts": { - "settled": "180.00", - "restituted": "0.00", - "slashed": "0.00", - "currency": "USDC" - }, - "opened_at": "2026-07-25T12:04:11Z", - "settled_at": "2026-07-31T20:03:47Z", - "signatures": [ - { - "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6c2V0dGxlLmV4YW1wbGUjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi9wYWN0LWF0dGVzdGF0aW9uK2pzb24ifQ", - "signature": "ILLUSTRATIVE-NOT-A-REAL-SIGNATURE" - } - ] -} diff --git a/examples/challenge.json b/examples/challenge.json index ea9b81e..d9a27bb 100644 --- a/examples/challenge.json +++ b/examples/challenge.json @@ -1,20 +1,24 @@ { - "pact": "0.1", + "pact": "0.2", "type": "Challenge", "vtc_id": "vtc_9f2c11", - "delivery_hash": "sha256:c404026ffa4ec4ae93333206c7581fc0b3c344c70da5e218e1997fdc43799c71", + "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb", "proof": { "profile": "acceptance", - "instrument_hash": "sha256:d9205d4f2922afd55c0a2dc4ab00d8ee5a512343430bcf5e9abf0c76d66c69f7", - "results_hash": "sha256:3053a1078994497ff0617cb028a20e58e32292ed18a7c0ee8a4399006c620636", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", + "results_hash": "sha256:7f05a60223e24ca9393e0c241d0448cb5f39e5edb71c117fb7d3d593d5861b40", "results_uri": "https://watch.example/o/a91e", "failing_checks": [ "schema_valid_rate", "row_count_min" ] }, + "costs": { + "amount": "1.20", + "currency": "USDC" + }, "signature": { - "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6d2F0Y2guZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3BhY3QtY2hhbGxlbmdlK2pzb24ifQ", - "signature": "ILLUSTRATIVE-NOT-A-REAL-SIGNATURE" + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6d2F0Y2guZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0LmNoYWxsZW5nZStqc29uIn0", + "signature": "EnmMGEE08LUGshIdvord3_FzthJV8Sy2SG5uoQkNB29Sj97dfzfpoA-5yJgbyZHnpsI6ahTrVLeU_0cuM48JDw" } } diff --git a/examples/delivery.json b/examples/delivery.json index 9097ee3..9c914f9 100644 --- a/examples/delivery.json +++ b/examples/delivery.json @@ -1,20 +1,19 @@ { - "pact": "0.1", + "pact": "0.2", "type": "Delivery", "vtc_id": "vtc_9f2c11", - "vtc_hash": "sha256:b2cccae00fc7b97ba2b6ef6356ff42c006ba661e0249d7eaa8baff3de553870b", - "work_hash": "sha256:9c1f2ab4e7d0355f1b8c6e4a92d70f3ab5c81e46d92f0a7b3c5e18d4f60a2b79", - "work_uri": "https://cdn.dataforge.example/o/9c1f2ab4", - "input_hash": "sha256:fcdc7f0f21793df9e57e517d664145159dcabdcebbaa293b4436a67aec4924e7", - "delivered_at": "2026-07-31T18:22:04Z", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2", + "work_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c975a1d1ce519d582306338b1", + "work_uri": "https://cdn.dataforge.example/o/d7f4", + "input_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c975a1d1ce519d582306338b1", "evidence": { "profile": "acceptance", - "instrument_hash": "sha256:d9205d4f2922afd55c0a2dc4ab00d8ee5a512343430bcf5e9abf0c76d66c69f7", - "results_hash": "sha256:7e02b5c1a9384d7f2e60b8c4a1d53f79e2064b8fa3c7d190e5b62f84c07a1d3e", - "results_uri": "https://cdn.dataforge.example/o/7e02b5c1" + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", + "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf848ebaca540214b4f6d2165688a51", + "results_uri": "https://cdn.dataforge.example/o/28d3" }, "signature": { - "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6ZGF0YWZvcmdlLmV4YW1wbGU6YWdlbnRzOmV0bC0zI2sxIiwidHlwIjoiYXBwbGljYXRpb24vcGFjdC1kZWxpdmVyeStqc29uIn0", - "signature": "ILLUSTRATIVE-NOT-A-REAL-SIGNATURE" + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6ZGF0YWZvcmdlLmV4YW1wbGU6YWdlbnRzOmV0bC0zI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QuZGVsaXZlcnkranNvbiJ9", + "signature": "9CVlezLLYF2raBHqijKrLTB94BIrXmxNr-5vhFWVilodcH4WXj2ri5Z1QTcQSh34DPmNtZKMf_li4GdlkASzCg" } } diff --git a/examples/keys/README.md b/examples/keys/README.md new file mode 100644 index 0000000..3c9df81 --- /dev/null +++ b/examples/keys/README.md @@ -0,0 +1,7 @@ +# Example keys + +`public-keys.json` holds the public keys that verify the signatures on the +committed examples, as JWKs (RFC 7517). The private keys are derived in +`tools/mint_examples.py` from public seeds, so they are not secrets and the +signatures are reproducible byte for byte; nothing signed with them means +anything outside this repository. diff --git a/examples/keys/public-keys.json b/examples/keys/public-keys.json new file mode 100644 index 0000000..c82c987 --- /dev/null +++ b/examples/keys/public-keys.json @@ -0,0 +1,32 @@ +{ + "buyer": { + "kid": "did:web:buyer.example:agents:procure-1#k1", + "kty": "OKP", + "crv": "Ed25519", + "x": "ZI4mO5NKdU9Q8BUR9mmFDg4lMFSRjsMmrZyM2QYVj6o" + }, + "seller": { + "kid": "did:web:dataforge.example:agents:etl-3#k1", + "kty": "OKP", + "crv": "Ed25519", + "x": "ftMBufL7i1hk3JvMK8tGXqWDAJGvktvqbt6hH6XgJ_A" + }, + "facilitator": { + "kid": "did:web:settle.example#k1", + "kty": "OKP", + "crv": "Ed25519", + "x": "CKxgOGR0BWmuX_SDlDWEb5J3cVewJ0q4An4qpcP7MhY" + }, + "verifier": { + "kid": "did:web:audit.example#k1", + "kty": "OKP", + "crv": "Ed25519", + "x": "f9HxRcwZDRgYxD2dFplf6JSB40FJlQ73DoG6pCQDHh8" + }, + "challenger": { + "kid": "did:web:watch.example#k1", + "kty": "OKP", + "crv": "Ed25519", + "x": "cF4KdQZLE2HZOow_B8iPTDzcLpzp-RFhmslIxOXFLkI" + } +} diff --git a/examples/outcome.json b/examples/outcome.json new file mode 100644 index 0000000..927c77a --- /dev/null +++ b/examples/outcome.json @@ -0,0 +1,129 @@ +{ + "pact": "0.2", + "type": "OutcomeRecord", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2", + "parties": { + "buyer": "did:web:buyer.example:agents:procure-1", + "seller": "did:web:dataforge.example:agents:etl-3", + "facilitator": "did:web:settle.example", + "verifier": "did:web:audit.example" + }, + "outcome": { + "state": "SETTLED", + "challenge_upheld": true + }, + "work_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c975a1d1ce519d582306338b1", + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8e09452a74bac15e6752ca81", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + }, + { + "event": "challenge", + "at": "2026-11-10T09:40:00Z", + "object": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df6cfef12d12d1340c15e5d85", + "signer": "did:web:watch.example#k1", + "costs": { + "amount": "1.20", + "currency": "USDC" + } + }, + { + "event": "verdict", + "at": "2026-11-10T09:58:05Z", + "object": "sha256:012bab194f444012f87ff8986419537f9de9f3dfe845c6623ac6a7488fb98b73", + "signer": "did:web:audit.example#k1", + "outcome": "FAIL", + "answers": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df6cfef12d12d1340c15e5d85", + "supersedes": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8e09452a74bac15e6752ca81" + }, + { + "event": "children-final", + "at": "2026-11-10T09:58:05Z" + }, + { + "event": "terminal", + "at": "2026-11-10T09:58:05Z", + "state": "SETTLED", + "challenge_upheld": true + } + ], + "terms_result": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5fc1147743326dabd45bda546dc62", + "currency": "USDC", + "transfers": [ + { + "event": 1, + "from": "buyer", + "to": "escrow", + "amount": "180.00", + "code": "lock" + }, + { + "event": 1, + "from": "seller", + "to": "bond", + "amount": "18.00", + "code": "bond" + }, + { + "event": 1, + "from": "buyer", + "to": "fund", + "amount": "0.50", + "code": "fund" + }, + { + "event": 3, + "from": "escrow", + "to": "seller", + "amount": "180.00", + "code": "principal" + }, + { + "event": 8, + "from": "fund", + "to": "challenger:did:web:watch.example#k1", + "amount": "0.50", + "code": "costs" + }, + { + "event": 8, + "from": "bond", + "to": "buyer", + "amount": "18.00", + "code": "restitution" + } + ] + }, + "signatures": [ + { + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6c2V0dGxlLmV4YW1wbGUjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5vdXRjb21lK2pzb24ifQ", + "signature": "eOufu8kaI-zRjU13rTVFSSJbn-zPC2QMn0QpHP4V65OpunfT2KNN3p-YESE7ht9tf2uUPHxWkDZuH1cgVJVsDQ" + } + ] +} diff --git a/examples/status.json b/examples/status.json new file mode 100644 index 0000000..64acd5b --- /dev/null +++ b/examples/status.json @@ -0,0 +1,40 @@ +{ + "pact": "0.2", + "type": "ContractStatus", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2", + "state": "WINDOW_OPEN", + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8e09452a74bac15e6752ca81", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + } + ], + "issued_at": "2026-11-10T09:14:30Z", + "signature": { + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6c2V0dGxlLmV4YW1wbGUjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5zdGF0dXMranNvbiJ9", + "signature": "LZOcBREP7-LMJ8iveBuuSzhuETdwERvf7nbA3adNdgzt8oQ_Udf9VV5D_rRpzjl_CDHvwmkZJ7tYxir5Qs9yAw" + } +} diff --git a/examples/task-content/README.md b/examples/task-content/README.md new file mode 100644 index 0000000..acffc3a --- /dev/null +++ b/examples/task-content/README.md @@ -0,0 +1,8 @@ +# Task content for the worked example + +The bytes that the three URIs inside `examples/taskspec.json` point at, so that +the sibling hashes the draft requires (Section 5.1) commit to something a reader +can recompute: `customers.schema.json` for `inputs.schema_uri`, `sample-10k.csv` +for `inputs.sample_uri` (four lines standing in for ten thousand), and +`output.schema.json` for `deliverable.schema_uri`. Each hash is SHA-256 over the +file's bytes. `tools/mint_examples.py` computes them. diff --git a/examples/task-content/customers.schema.json b/examples/task-content/customers.schema.json new file mode 100644 index 0000000..ac44839 --- /dev/null +++ b/examples/task-content/customers.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Customer record (input)", + "type": "object", + "required": ["customer_id", "email", "country", "updated_at"], + "properties": { + "customer_id": { "type": "string" }, + "email": { "type": "string" }, + "country": { "type": "string" }, + "updated_at": { "type": "string", "format": "date-time" } + } +} diff --git a/examples/task-content/output.schema.json b/examples/task-content/output.schema.json new file mode 100644 index 0000000..8d98f78 --- /dev/null +++ b/examples/task-content/output.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Customer record (deliverable)", + "type": "object", + "required": ["customer_id", "email", "country", "updated_at"], + "properties": { + "customer_id": { "type": "string" }, + "email": { "type": "string" }, + "country": { "type": "string", "pattern": "^[A-Z]{2}$" }, + "updated_at": { "type": "string", "format": "date-time" } + } +} diff --git a/examples/task-content/sample-10k.csv b/examples/task-content/sample-10k.csv new file mode 100644 index 0000000..c1c44ce --- /dev/null +++ b/examples/task-content/sample-10k.csv @@ -0,0 +1,4 @@ +customer_id,email,country,updated_at +c-000001,ana@example.net,de,2026-05-02T10:14:00Z +c-000002,ana@example.net,DE,2026-06-11T08:00:00Z +c-000003,bo@example.org,Germany,2026-01-20T17:45:30Z diff --git a/examples/taskspec.json b/examples/taskspec.json index 5bf1cbc..22cd5d5 100644 --- a/examples/taskspec.json +++ b/examples/taskspec.json @@ -1,25 +1,23 @@ { "description": "Deduplicate a 2,100,000-row customer CSV; normalize country fields to ISO-3166 alpha-2; merge conflicting records by most-recent timestamp.", - "skill": "data-cleaning/dedupe", "inputs": { "schema_uri": "https://buyer.example/specs/customers.schema.json", + "schema_hash": "sha256:c1fe8d8eaca3a82a0c11112284b8b8649044360b4b093e23a124071dbef7118d", "sample_uri": "https://buyer.example/specs/sample-10k.csv", - "size_hint": { - "rows": 2100000, - "bytes": 480000000 - } + "sample_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c975a1d1ce519d582306338b1" }, "deliverable": { "format": "csv", - "schema_uri": "https://buyer.example/specs/output.schema.json" + "schema_uri": "https://buyer.example/specs/output.schema.json", + "schema_hash": "sha256:219975fe37de3d388a32e28e7cd5efde7fa736a29ea8cec17b1b7bcfb71fee79" }, "acceptance": { "harness_uri": "https://buyer.example/specs/acceptance-tests.tar", + "harness_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", "thresholds": { "dup_rate_max": 0.001, "schema_valid_rate": 1.0 - }, - "harness_hash": "sha256:d9205d4f2922afd55c0a2dc4ab00d8ee5a512343430bcf5e9abf0c76d66c69f7" + } }, "constraints": { "tools_prohibited": [ diff --git a/examples/verdict-on-challenge.json b/examples/verdict-on-challenge.json new file mode 100644 index 0000000..03da6f8 --- /dev/null +++ b/examples/verdict-on-challenge.json @@ -0,0 +1,16 @@ +{ + "pact": "0.2", + "type": "Verdict", + "vtc_id": "vtc_9f2c11", + "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb", + "challenge_hash": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df6cfef12d12d1340c15e5d85", + "outcome": "FAIL", + "profile": "acceptance", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", + "results_hash": "sha256:7f05a60223e24ca9393e0c241d0448cb5f39e5edb71c117fb7d3d593d5861b40", + "evaluated_at": "2026-11-10T09:57:40Z", + "signature": { + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6YXVkaXQuZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0LnZlcmRpY3QranNvbiJ9", + "signature": "xOsLGEHXP3D9PzyxUjUNhKLLiWH4eNh-vPylihJkTBuPY9Oy7gHwP2_bsRx87yNDrG9MoRbXj1x1Bri7q6UmDg" + } +} diff --git a/examples/verdict.json b/examples/verdict.json index 784f0bc..9cdc1b2 100644 --- a/examples/verdict.json +++ b/examples/verdict.json @@ -1,15 +1,15 @@ { - "pact": "0.1", + "pact": "0.2", "type": "Verdict", "vtc_id": "vtc_9f2c11", - "delivery_hash": "sha256:c404026ffa4ec4ae93333206c7581fc0b3c344c70da5e218e1997fdc43799c71", + "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb", "outcome": "PASS", "profile": "acceptance", - "instrument_hash": "sha256:d9205d4f2922afd55c0a2dc4ab00d8ee5a512343430bcf5e9abf0c76d66c69f7", - "results_hash": "sha256:7e02b5c1a9384d7f2e60b8c4a1d53f79e2064b8fa3c7d190e5b62f84c07a1d3e", - "evaluated_at": "2026-07-31T19:03:47Z", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", + "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf848ebaca540214b4f6d2165688a51", + "evaluated_at": "2026-11-10T09:14:22Z", "signature": { - "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6YXVkaXQuZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3BhY3QtdmVyZGljdCtqc29uIn0", - "signature": "ILLUSTRATIVE-NOT-A-REAL-SIGNATURE" + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6YXVkaXQuZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0LnZlcmRpY3QranNvbiJ9", + "signature": "_Ze8kPxtnvnLGC7UYRvzv13wt6NDO_M9gGDopGbXryCn_E0R6N6Ejn5tTemPpte4SQaOAHG973ovIgjtgbHBAw" } } diff --git a/examples/vtc.json b/examples/vtc.json index b6a616b..7bec125 100644 --- a/examples/vtc.json +++ b/examples/vtc.json @@ -1,5 +1,5 @@ { - "pact": "0.1", + "pact": "0.2", "type": "VerifiableTaskContract", "id": "vtc_9f2c11", "parties": { @@ -9,38 +9,51 @@ "verifier": "did:web:audit.example" }, "task": { - "spec_hash": "sha256:bb0e87ce522479b7c2f7bcfa26df7ecd7ff67aeb8b415bbd70c22d97c47adf35", + "spec_hash": "sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef111d27ff6bee37f05531823b72", "spec_uri": "https://buyer.example/specs/taskspec.json", - "deadline": "2026-08-01T00:00:00Z" + "deadline": "2026-11-14T00:00:00Z" }, "price": { "amount": "180.00", "currency": "USDC", - "settlement": "pact-escrow", + "settlement": "https://settle.example/bindings/ledger-1", "network": "eip155:8453" }, "verification": { "tier": "T0-reexec", "profile": "acceptance", - "criteria_hash": "sha256:d9205d4f2922afd55c0a2dc4ab00d8ee5a512343430bcf5e9abf0c76d66c69f7" + "criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", + "max_verdict_seconds": 86400 }, - "assurance": { - "mode": "certain", - "q_min": 1.0 - }, - "release": "on-verification", - "liability": { - "seller_bond": "18.00", - "verification_fund": "0.50", - "cap": "180.00", - "restitution_basis": "released" + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5fc1147743326dabd45bda546dc62", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 1.0 + } + } }, "challenge": { "window_seconds": 3600, "max_dispute_seconds": 86400 }, "signatures": [ - { "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6YnV5ZXIuZXhhbXBsZTphZ2VudHM6cHJvY3VyZS0xI2sxIiwidHlwIjoiYXBwbGljYXRpb24vcGFjdC1jb250cmFjdCtqc29uIn0", "signature": "ILLUSTRATIVE-NOT-A-REAL-SIGNATURE" }, - { "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6ZGF0YWZvcmdlLmV4YW1wbGU6YWdlbnRzOmV0bC0zI2sxIiwidHlwIjoiYXBwbGljYXRpb24vcGFjdC1jb250cmFjdCtqc29uIn0", "signature": "ILLUSTRATIVE-NOT-A-REAL-SIGNATURE" } + { + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6YnV5ZXIuZXhhbXBsZTphZ2VudHM6cHJvY3VyZS0xI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QuY29udHJhY3QranNvbiJ9", + "signature": "YsInkxWfby7uxxorS4D9oW9RSEpuVzu8D2WnYWevPFa-5GGZfjyvn6Y7dg66QYGl_QS7ITNNJ-ACxnXsw1mXCw" + }, + { + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6ZGF0YWZvcmdlLmV4YW1wbGU6YWdlbnRzOmV0bC0zI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QuY29udHJhY3QranNvbiJ9", + "signature": "L8fBf_iz83igBpY5iyvSBnqx9STMjX3fJaXQ48ym39EgzwdvEQhCEYrkYiZEIvYbSeZH8W69nmgeXxhNl8QZAA" + } ] } diff --git a/examples/well-known/pact-facilitator.json b/examples/well-known/pact-facilitator.json index c547828..3bb7fea 100644 --- a/examples/well-known/pact-facilitator.json +++ b/examples/well-known/pact-facilitator.json @@ -1,19 +1,45 @@ { - "pact": "0.1", + "pact": "0.2", + "type": "FacilitatorCapabilities", "facilitator": "did:web:settle.example", "settlement_bindings": [ - { "id": "pact-escrow", "networks": ["eip155:8453"], "assets": ["USDC"] } + { + "id": "https://settle.example/bindings/ledger-1", + "networks": [ + "eip155:8453" + ], + "assets": [ + "USDC" + ] + } ], - "release_modes": ["on-verification", "on-window"], - "verification_profiles": ["acceptance", "bisection"], - "assurance_modes": ["certain", "committed-sample"], - "max_contract_value": { "amount": "50000.00", "currency": "USDC" }, + "flows": [ + "verdict-first", + "delivery-first" + ], + "verification_profiles": [ + "acceptance", + "bisection" + ], + "terms_profiles": [ + { + "id": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5fc1147743326dabd45bda546dc62" + } + ], + "max_contract_value": { + "amount": "50000.00", + "currency": "USDC" + }, "endpoints": { - "contract": "https://settle.example/pact/v1/contracts", - "delivery": "https://settle.example/pact/v1/deliveries", - "verdict": "https://settle.example/pact/v1/verdicts", - "challenge": "https://settle.example/pact/v1/challenges", - "attestation": "https://settle.example/pact/v1/attestations" + "contract": "https://settle.example/pact/v2/contracts", + "delivery": "https://settle.example/pact/v2/deliveries", + "verdict": "https://settle.example/pact/v2/verdicts", + "challenge": "https://settle.example/pact/v2/challenges", + "outcome": "https://settle.example/pact/v2/outcomes" }, - "signature": { "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6c2V0dGxlLmV4YW1wbGUjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi9wYWN0LWZhY2lsaXRhdG9yK2pzb24ifQ", "signature": "ILLUSTRATIVE-NOT-A-REAL-SIGNATURE" } + "signature": { + "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6c2V0dGxlLmV4YW1wbGUjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5mYWNpbGl0YXRvcitqc29uIn0", + "signature": "hSax9MOodgYWoYjkTk9OMtj-vIAzCwETfS9gSXQpVUprqF_nNYpKtEo3OS1joYgIFiIFQwXh0hRqdrn2erHoDg" + } } diff --git a/profiles/bonded-restitution/README.md b/profiles/bonded-restitution/README.md new file mode 100644 index 0000000..c48d902 --- /dev/null +++ b/profiles/bonded-restitution/README.md @@ -0,0 +1,107 @@ +# bonded-restitution: an example terms profile + +Identifier: `tag:laxsharma79@gmail.com,2026:pact:bonded-restitution`. +Problem types: `tag:laxsharma79@gmail.com,2026:pact:bonded-restitution:problem:`. + +This directory is a terms profile bundle in the sense of draft-laxsharma-pact-02 +Section 5.3: this file, `parameters.schema.json` and `vectors.json`. `profile_hash` +is the manifest digest of Section 5.1 over the three files, and `tools/validate.py` +recomputes it on every run. It is the profile printed as Appendix A of the draft, +and it is not normative anywhere: it exists so that the experiment in Section 1.4 +can be run before any other profile is written. + +It carries the settlement content of draft-laxsharma-pact-01 written as a schedule +over the events of Section 4.2, with the choices -01 left open now made. What the +figures below mean between the parties to a contract that names this profile is a +question the draft does not answer and its author is not qualified to answer. A +profile meant for use needs an owner who is. + +## Parameters + +`terms.parameters` validates against `parameters.schema.json`: + +| Member | Type | Meaning | +|---|---|---| +| `seller_bond` | amount, required | what the Seller posts before performance | +| `verification_fund` | amount, required | what the Buyer posts to pay for checking | +| `cap` | amount, required | the most that leaves the Seller's accounts under the contract | +| `restitution_basis` | `released` or `price`, required | what the Buyer's loss is measured against | +| `remainder_to` | `buyer` or `sink`, optional | where a remaining bond goes; `sink` when absent | +| `verifier_fee` | amount, optional | paid from the fund at each Verdict; `0.00` when absent | +| `principal_on` | `verdict`, `delivered` or `window-closed`, required | the event at which the price moves to the Seller | +| `assurance` | object, required | `mode` (`certain`, `committed-sample`, `open`) and `q_min` in (0, 1] | + +The -01 release modes map onto the contract's `flow` and this profile's +`principal_on`: on-verification is verdict-first with `verdict`; on-window is +delivery-first with `window-closed`; optimistic is delivery-first with `delivered`; +unsecured is no-window with `delivered`. + +## Accounts + +Three internal accounts, opened empty: `escrow`, `bond`, `fund`. External accounts, +unbounded as sources and sinks: `buyer`, `seller`, `verifier`, `challenger:` for +each Challenger, and `sink`. Closure requires the three internal accounts to hold +zero after the last entry; no entry ever takes from an internal account more than it +holds. + +## Admission + +At `accepted` the profile evaluates, exactly and in the contract's currency, with P +the price, B `seller_bond`, q `assurance.q_min`, and E equal to P when +`principal_on` is `delivered` and zero otherwise: + + B >= P * (1 - q) / q + E + +Multiplied through by q this is `B*q >= P*(1-q) + E*q`, which needs no division and +no rounding. When it does not hold, or when `assurance.mode` is `open`, the profile +reports `assurance-constraint-unsatisfied`. A contract whose `seller_bond` or +`verification_fund` exceeds `cap` is reported as `parameters-inconsistent`. The +inequality is the classical deterrence bound (Polinsky and Shavell 1999; Belenkiy et +al. 2008, Theorem 1), with E the one term -01 added. + +## Schedule + +For each event the schedule emits the entries below in the order listed, omitting +any entry whose amount is zero. Every event of Section 4.2 not named here emits +nothing. Amounts are computed from the contract and the trace prefix; "released" is +the sum of `principal` entries emitted so far; "the balance" of an account is what +it holds at that point in the list. + +- `funded`: buyer to escrow, P, `lock`; seller to bond, B, `bond`; buyer to fund, + `verification_fund`, `fund`. +- `delivered`: if `principal_on` is `delivered`: escrow to seller, the escrow + balance, `principal`. +- `verdict`: fund to verifier, the lesser of `verifier_fee` and the fund balance, + `verification`; then, if the outcome is PASS, the Verdict answers no Challenge and + `principal_on` is `verdict`: escrow to seller, the escrow balance, `principal`. +- `window-closed`: if `principal_on` is `window-closed` and the standing Verdict is + not FAIL: escrow to seller, the escrow balance, `principal`. +- `terminal` FINAL: escrow to seller, the escrow balance, `principal`; bond to + seller, the bond balance, `return`; fund to buyer, the fund balance, + `fund-return`. +- `terminal` ABANDONED: escrow to buyer, the escrow balance, `reverse`; bond to + seller, the bond balance, `return`; fund to buyer, the fund balance, + `fund-return`. With the price reversed the Buyer's loss is zero under either + basis, so nothing is slashed. +- `terminal` SETTLED, in five ranks, each drawing only what remains: (1) escrow to + buyer, the escrow balance, `reverse`; (2) if `challenge_upheld`, fund to the + Challenger whose Challenge the standing Verdict answers, the lesser of that + Challenge's `costs` and the fund balance, `costs`; (3) bond to buyer, the lesser + of the bond balance, `cap` and the Buyer's loss, `restitution`, the loss being + "released" under basis `released` and P minus the rank-1 entry under basis + `price`; (4) if `challenge_upheld`, bond to that Challenger, the bond balance, + `bounty`; (5) bond to buyer or sink per `remainder_to`, the bond balance, + `remainder`. Then fund to buyer, the fund balance, `fund-return`. + +Ranks 2 and 4 pay one Challenger, the one whose Challenge the standing Verdict +answers. A Challenge that was lapsed, rejected or superseded receives nothing. Rank +4 gives the whole remaining bond because -01 forbade capping it at a fraction chosen +for tidiness and fixed no figure. `cap` bounds ranks 3 to 5 together. + +## Vectors + +`vectors.json` is an array of `{name, contract, trace, transfers}` objects, each a +complete trace with the list the schedule produces for it, generated by +`tools/profile.py` and checked against the lists printed in Appendix A.6 of the +draft by `tools/validate.py`. A Facilitator reproduces every vector before listing +this profile in its capability document (Section 12.1). diff --git a/profiles/bonded-restitution/parameters.schema.json b/profiles/bonded-restitution/parameters.schema.json new file mode 100644 index 0000000..1eb9cd8 --- /dev/null +++ b/profiles/bonded-restitution/parameters.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pact-spec.github.io/spec/profiles/bonded-restitution/parameters.schema.json", + "title": "Parameters of the bonded-restitution terms profile", + "$comment": "This schema is part of the profile bundle committed by terms.profile_hash. It validates terms.parameters of a contract naming tag:laxsharma79@gmail.com,2026:pact:bonded-restitution. It is not part of the Internet-Draft.", + "type": "object", + "required": [ + "seller_bond", + "verification_fund", + "cap", + "restitution_basis", + "principal_on", + "assurance" + ], + "properties": { + "seller_bond": { + "$ref": "#/$defs/money" + }, + "verification_fund": { + "$ref": "#/$defs/money" + }, + "cap": { + "$ref": "#/$defs/money" + }, + "restitution_basis": { + "enum": [ + "released", + "price" + ] + }, + "remainder_to": { + "enum": [ + "buyer", + "sink" + ] + }, + "verifier_fee": { + "$ref": "#/$defs/money" + }, + "principal_on": { + "enum": [ + "verdict", + "delivered", + "window-closed" + ] + }, + "assurance": { + "type": "object", + "required": [ + "mode", + "q_min" + ], + "properties": { + "mode": { + "enum": [ + "certain", + "committed-sample", + "open" + ] + }, + "q_min": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1 + }, + "sample_rate": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$defs": { + "money": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.[0-9]{2,18}$" + } + } +} diff --git a/profiles/bonded-restitution/vectors.json b/profiles/bonded-restitution/vectors.json new file mode 100644 index 0000000..4ee2c1a --- /dev/null +++ b/profiles/bonded-restitution/vectors.json @@ -0,0 +1,773 @@ +[ + { + "name": "FINAL: PASS, window closes, Figure 1", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 1.0 + } + } + } + }, + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:vtc0000000000000000000000000000000000000000000000000000000000000" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:delivery00000000000000000000000000000000000000000000000000000000" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:verdict100000000000000000000000000000000000000000000000000000000", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + }, + { + "event": "window-closed", + "at": "2026-11-10T10:14:31Z" + }, + { + "event": "children-final", + "at": "2026-11-10T10:14:31Z" + }, + { + "event": "terminal", + "at": "2026-11-10T10:14:31Z", + "state": "FINAL", + "challenge_upheld": false + } + ], + "transfers": [ + { + "event": 1, + "from": "buyer", + "to": "escrow", + "amount": "180.00", + "code": "lock" + }, + { + "event": 1, + "from": "seller", + "to": "bond", + "amount": "18.00", + "code": "bond" + }, + { + "event": 1, + "from": "buyer", + "to": "fund", + "amount": "0.50", + "code": "fund" + }, + { + "event": 3, + "from": "escrow", + "to": "seller", + "amount": "180.00", + "code": "principal" + }, + { + "event": 7, + "from": "bond", + "to": "seller", + "amount": "18.00", + "code": "return" + }, + { + "event": 7, + "from": "fund", + "to": "buyer", + "amount": "0.50", + "code": "fund-return" + } + ] + }, + { + "name": "SETTLED on an upheld Challenge, Figure 5", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 1.0 + } + } + } + }, + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:vtc0000000000000000000000000000000000000000000000000000000000000" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:delivery00000000000000000000000000000000000000000000000000000000" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:verdict100000000000000000000000000000000000000000000000000000000", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + }, + { + "event": "challenge", + "at": "2026-11-10T09:40:00Z", + "object": "sha256:challenge0000000000000000000000000000000000000000000000000000000", + "signer": "did:web:watch.example#k1", + "costs": { + "amount": "1.20", + "currency": "USDC" + } + }, + { + "event": "verdict", + "at": "2026-11-10T09:58:05Z", + "object": "sha256:verdict200000000000000000000000000000000000000000000000000000000", + "signer": "did:web:audit.example#k1", + "outcome": "FAIL", + "answers": "sha256:challenge0000000000000000000000000000000000000000000000000000000", + "supersedes": "sha256:verdict100000000000000000000000000000000000000000000000000000000" + }, + { + "event": "children-final", + "at": "2026-11-10T09:58:05Z" + }, + { + "event": "terminal", + "at": "2026-11-10T09:58:05Z", + "state": "SETTLED", + "challenge_upheld": true + } + ], + "transfers": [ + { + "event": 1, + "from": "buyer", + "to": "escrow", + "amount": "180.00", + "code": "lock" + }, + { + "event": 1, + "from": "seller", + "to": "bond", + "amount": "18.00", + "code": "bond" + }, + { + "event": 1, + "from": "buyer", + "to": "fund", + "amount": "0.50", + "code": "fund" + }, + { + "event": 3, + "from": "escrow", + "to": "seller", + "amount": "180.00", + "code": "principal" + }, + { + "event": 8, + "from": "fund", + "to": "challenger:did:web:watch.example#k1", + "amount": "0.50", + "code": "costs" + }, + { + "event": 8, + "from": "bond", + "to": "buyer", + "amount": "18.00", + "code": "restitution" + } + ] + }, + { + "name": "SETTLED: the Verifier records FAIL", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 1.0 + } + } + } + }, + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:vtc0000000000000000000000000000000000000000000000000000000000000" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:delivery00000000000000000000000000000000000000000000000000000000" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:verdict100000000000000000000000000000000000000000000000000000000", + "signer": "did:web:audit.example#k1", + "outcome": "FAIL" + }, + { + "event": "children-final", + "at": "2026-11-10T09:14:30Z" + }, + { + "event": "terminal", + "at": "2026-11-10T09:14:30Z", + "state": "SETTLED", + "challenge_upheld": false + } + ], + "transfers": [ + { + "event": 1, + "from": "buyer", + "to": "escrow", + "amount": "180.00", + "code": "lock" + }, + { + "event": 1, + "from": "seller", + "to": "bond", + "amount": "18.00", + "code": "bond" + }, + { + "event": 1, + "from": "buyer", + "to": "fund", + "amount": "0.50", + "code": "fund" + }, + { + "event": 5, + "from": "escrow", + "to": "buyer", + "amount": "180.00", + "code": "reverse" + }, + { + "event": 5, + "from": "bond", + "to": "sink", + "amount": "18.00", + "code": "remainder" + }, + { + "event": 5, + "from": "fund", + "to": "buyer", + "amount": "0.50", + "code": "fund-return" + } + ] + }, + { + "name": "ABANDONED: deadline with no Delivery", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 1.0 + } + } + } + }, + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:vtc0000000000000000000000000000000000000000000000000000000000000" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "deadline-passed", + "at": "2026-11-14T00:00:01Z" + }, + { + "event": "children-final", + "at": "2026-11-14T00:00:01Z" + }, + { + "event": "terminal", + "at": "2026-11-14T00:00:01Z", + "state": "ABANDONED", + "challenge_upheld": false + } + ], + "transfers": [ + { + "event": 1, + "from": "buyer", + "to": "escrow", + "amount": "180.00", + "code": "lock" + }, + { + "event": 1, + "from": "seller", + "to": "bond", + "amount": "18.00", + "code": "bond" + }, + { + "event": 1, + "from": "buyer", + "to": "fund", + "amount": "0.50", + "code": "fund" + }, + { + "event": 4, + "from": "escrow", + "to": "buyer", + "amount": "180.00", + "code": "reverse" + }, + { + "event": 4, + "from": "bond", + "to": "seller", + "amount": "18.00", + "code": "return" + }, + { + "event": 4, + "from": "fund", + "to": "buyer", + "amount": "0.50", + "code": "fund-return" + } + ] + }, + { + "name": "SETTLED on an upheld Challenge, basis price", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "price", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 1.0 + } + } + } + }, + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:vtc0000000000000000000000000000000000000000000000000000000000000" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:delivery00000000000000000000000000000000000000000000000000000000" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:verdict100000000000000000000000000000000000000000000000000000000", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + }, + { + "event": "challenge", + "at": "2026-11-10T09:40:00Z", + "object": "sha256:challenge0000000000000000000000000000000000000000000000000000000", + "signer": "did:web:watch.example#k1", + "costs": { + "amount": "1.20", + "currency": "USDC" + } + }, + { + "event": "verdict", + "at": "2026-11-10T09:58:05Z", + "object": "sha256:verdict200000000000000000000000000000000000000000000000000000000", + "signer": "did:web:audit.example#k1", + "outcome": "FAIL", + "answers": "sha256:challenge0000000000000000000000000000000000000000000000000000000", + "supersedes": "sha256:verdict100000000000000000000000000000000000000000000000000000000" + }, + { + "event": "children-final", + "at": "2026-11-10T09:58:05Z" + }, + { + "event": "terminal", + "at": "2026-11-10T09:58:05Z", + "state": "SETTLED", + "challenge_upheld": true + } + ], + "transfers": [ + { + "event": 1, + "from": "buyer", + "to": "escrow", + "amount": "180.00", + "code": "lock" + }, + { + "event": 1, + "from": "seller", + "to": "bond", + "amount": "18.00", + "code": "bond" + }, + { + "event": 1, + "from": "buyer", + "to": "fund", + "amount": "0.50", + "code": "fund" + }, + { + "event": 3, + "from": "escrow", + "to": "seller", + "amount": "180.00", + "code": "principal" + }, + { + "event": 8, + "from": "fund", + "to": "challenger:did:web:watch.example#k1", + "amount": "0.50", + "code": "costs" + }, + { + "event": 8, + "from": "bond", + "to": "buyer", + "amount": "18.00", + "code": "restitution" + } + ] + }, + { + "name": "FINAL after verdict-lapsed", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 1.0 + } + } + } + }, + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:vtc0000000000000000000000000000000000000000000000000000000000000" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:delivery00000000000000000000000000000000000000000000000000000000" + }, + { + "event": "verdict-lapsed", + "at": "2026-11-11T08:30:13Z" + }, + { + "event": "window-opened", + "at": "2026-11-11T08:30:13Z", + "closes_at": "2026-11-11T09:30:13Z" + }, + { + "event": "window-closed", + "at": "2026-11-11T09:30:14Z" + }, + { + "event": "children-final", + "at": "2026-11-11T09:30:14Z" + }, + { + "event": "terminal", + "at": "2026-11-11T09:30:14Z", + "state": "FINAL", + "challenge_upheld": false + } + ], + "transfers": [ + { + "event": 1, + "from": "buyer", + "to": "escrow", + "amount": "180.00", + "code": "lock" + }, + { + "event": 1, + "from": "seller", + "to": "bond", + "amount": "18.00", + "code": "bond" + }, + { + "event": 1, + "from": "buyer", + "to": "fund", + "amount": "0.50", + "code": "fund" + }, + { + "event": 7, + "from": "escrow", + "to": "seller", + "amount": "180.00", + "code": "principal" + }, + { + "event": 7, + "from": "bond", + "to": "seller", + "amount": "18.00", + "code": "return" + }, + { + "event": 7, + "from": "fund", + "to": "buyer", + "amount": "0.50", + "code": "fund-return" + } + ] + }, + { + "name": "FINAL under delivery-first, principal at window-closed", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "delivery-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "window-closed", + "assurance": { + "mode": "certain", + "q_min": 1.0 + } + } + } + }, + "trace": [ + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:vtc0000000000000000000000000000000000000000000000000000000000000" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:delivery00000000000000000000000000000000000000000000000000000000" + }, + { + "event": "window-opened", + "at": "2026-11-10T08:30:12Z", + "closes_at": "2026-11-10T09:30:12Z" + }, + { + "event": "window-closed", + "at": "2026-11-10T09:30:13Z" + }, + { + "event": "children-final", + "at": "2026-11-10T09:30:13Z" + }, + { + "event": "terminal", + "at": "2026-11-10T09:30:13Z", + "state": "FINAL", + "challenge_upheld": false + } + ], + "transfers": [ + { + "event": 1, + "from": "buyer", + "to": "escrow", + "amount": "180.00", + "code": "lock" + }, + { + "event": 1, + "from": "seller", + "to": "bond", + "amount": "18.00", + "code": "bond" + }, + { + "event": 1, + "from": "buyer", + "to": "fund", + "amount": "0.50", + "code": "fund" + }, + { + "event": 4, + "from": "escrow", + "to": "seller", + "amount": "180.00", + "code": "principal" + }, + { + "event": 6, + "from": "bond", + "to": "seller", + "amount": "18.00", + "code": "return" + }, + { + "event": 6, + "from": "fund", + "to": "buyer", + "amount": "0.50", + "code": "fund-return" + } + ] + } +] diff --git a/schemas/attestation.schema.json b/schemas/attestation.schema.json deleted file mode 100644 index 82d5eb6..0000000 --- a/schemas/attestation.schema.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "attestation.schema.json", - "title": "PACT Work Attestation", - "type": "object", - "additionalProperties": false, - "required": [ - "pact", - "type", - "vtc_id", - "vtc_hash", - "parties", - "subject", - "role", - "outcome", - "amounts", - "signatures" - ], - "properties": { - "pact": { - "type": "string" - }, - "type": { - "const": "WorkAttestation" - }, - "vtc_id": { - "type": "string" - }, - "vtc_hash": { - "$ref": "common.schema.json#/$defs/hash" - }, - "parties": { - "type": "object", - "required": [ - "buyer", - "seller", - "facilitator" - ], - "properties": { - "buyer": { - "$ref": "common.schema.json#/$defs/did" - }, - "seller": { - "$ref": "common.schema.json#/$defs/did" - }, - "facilitator": { - "$ref": "common.schema.json#/$defs/did" - } - }, - "additionalProperties": false - }, - "subject": { - "$ref": "common.schema.json#/$defs/did" - }, - "role": { - "enum": [ - "buyer", - "seller" - ] - }, - "outcome": { - "enum": [ - "performed", - "cured", - "slashed", - "abandoned" - ] - }, - "work_hash": { - "$ref": "common.schema.json#/$defs/hash" - }, - "amounts": { - "type": "object", - "required": [ - "settled", - "restituted", - "slashed", - "currency" - ], - "properties": { - "settled": { - "$ref": "common.schema.json#/$defs/money" - }, - "restituted": { - "$ref": "common.schema.json#/$defs/money" - }, - "slashed": { - "$ref": "common.schema.json#/$defs/money" - }, - "currency": { - "type": "string" - } - }, - "additionalProperties": false - }, - "children_merkle_root": { - "$ref": "common.schema.json#/$defs/hash", - "$comment": "Section 11.1: omitted where there are no children. MUST NOT be null." - }, - "opened_at": { - "type": "string", - "format": "date-time" - }, - "settled_at": { - "type": "string", - "format": "date-time" - }, - "signatures": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "common.schema.json#/$defs/signature" - } - } - } -} diff --git a/schemas/challenge.schema.json b/schemas/challenge.schema.json index 3e03e1c..2e9910b 100644 --- a/schemas/challenge.schema.json +++ b/schemas/challenge.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "challenge.schema.json", - "title": "PACT Challenge", + "title": "PACT Challenge (0.2)", "type": "object", "additionalProperties": false, "required": [ @@ -14,7 +14,7 @@ ], "properties": { "pact": { - "type": "string" + "const": "0.2" }, "type": { "const": "Challenge" @@ -51,9 +51,13 @@ } } }, - "$comment": "Section 7.5. Profile-conditional members enforced in code.", + "$comment": "Section 7.3. Profile-conditional members enforced in code.", "additionalProperties": false }, + "costs": { + "$ref": "common.schema.json#/$defs/amount_with_currency", + "$comment": "Section 3.6: a figure the Challenger asserts; recorded in the trace and read by the terms profile only." + }, "signature": { "$ref": "common.schema.json#/$defs/signature" } diff --git a/schemas/common.schema.json b/schemas/common.schema.json index 7599791..8adbd0f 100644 --- a/schemas/common.schema.json +++ b/schemas/common.schema.json @@ -1,24 +1,50 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://pact-spec.github.io/spec/schemas/common.schema.json", + "$comment": "Shared definitions for the -02 objects. Everything about what a party owes, posts or receives left this file with the liability member; a terms profile carries its own parameters.schema.json.", "$defs": { "hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, - "did": { + "party": { "type": "string", - "pattern": "^did:" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:.+$", + "$comment": "A party identifier is a URI (Section 2); did:web and https are the forms the document exercises. Compared only after the normalization of Section 9.1." }, "money": { "type": "string", - "pattern": "^(0|[1-9][0-9]*)\\.[0-9]{2,18}$", - "$comment": "Widened from exactly two decimals. Two decimals cannot express a bounty on a sub-dollar bond (a 25 percent bounty on a 0.02 bond is 0.005), which made the micro-contract case that contract channels exist to serve arithmetically inexpressible." + "pattern": "^(0|[1-9][0-9]*)\\.[0-9]{2,18}$" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "Z$", + "$comment": "Section 2: RFC 3339 in UTC with the Z designator." }, "tier": { "type": "string", - "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", - "$comment": "Not a closed enum. Section 14 establishes a PACT Verification Tiers registry with Specification Required policy, so a registered extension must validate. The four initial entries are T0-reexec, T1-tee, T2-zkml, T3-jury." + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$" + }, + "flow": { + "enum": [ + "verdict-first", + "delivery-first", + "no-window" + ] + }, + "state": { + "enum": [ + "ACCEPTED", + "FUNDED", + "DELIVERED", + "WINDOW_OPEN", + "DISPUTED", + "AWAITING_CHILDREN", + "FINAL", + "SETTLED", + "ABANDONED" + ] }, "signature": { "type": "object", @@ -35,25 +61,40 @@ "protected": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$", - "$comment": "base64url JOSE protected header. MUST carry alg, kid and typ. alg MUST be one of ES256, ES384 or EdDSA; a verifier MUST reject alg values of none and all MAC algorithms, and MUST take the algorithm from the resolved key material rather than from the token." + "$comment": "base64url JOSE protected header carrying alg, kid and typ (Section 14.1)." }, "signature": { "type": "string" } }, - "$comment": "kid was previously a sibling of protected, which placed the key identifier outside the signed data where it could be rewritten in transit. It now lives in the protected header. See https://github.com/pact-spec/spec/issues/1 entry 7.", + "additionalProperties": false + }, + "amount_with_currency": { + "type": "object", + "required": [ + "amount", + "currency" + ], + "properties": { + "amount": { + "$ref": "common.schema.json#/$defs/money" + }, + "currency": { + "type": "string" + } + }, "additionalProperties": false }, "challenge": { "type": "object", "required": [ - "window_seconds" + "window_seconds", + "max_dispute_seconds" ], "properties": { "window_seconds": { "type": "integer", - "minimum": 1, - "$comment": "Raised from a minimum of 0. A zero-length window made optimistic release unconditional and instantaneous, with no opportunity for a fraud proof to exist." + "minimum": 1 }, "max_dispute_seconds": { "type": "integer", @@ -62,90 +103,206 @@ }, "additionalProperties": false }, - "assurance": { + "terms": { "type": "object", "required": [ - "mode", - "q_min" + "profile", + "profile_hash", + "parameters" ], - "additionalProperties": false, "properties": { - "mode": { - "enum": [ - "certain", - "committed-sample", - "open" - ] + "profile": { + "type": "string", + "minLength": 1 }, - "q_min": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1 + "profile_hash": { + "$ref": "common.schema.json#/$defs/hash" }, - "sample_rate": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1 + "parameters": { + "type": "object", + "$comment": "Section 5.3: opaque to this document. The named profile's parameters.schema.json validates it; the closed-object rule of Section 2 does not apply inside it." } }, - "$comment": "Section 7.2. 'open' MUST NOT be the sole declared source of assurance; enforced in code." + "additionalProperties": false }, - "release_mode": { - "enum": [ - "on-verification", - "on-window", - "optimistic", - "unsecured" - ] + "parent": { + "type": "object", + "required": [ + "vtc_id", + "vtc_hash", + "facilitator" + ], + "properties": { + "vtc_id": { + "type": "string" + }, + "vtc_hash": { + "$ref": "common.schema.json#/$defs/hash" + }, + "facilitator": { + "$ref": "common.schema.json#/$defs/party" + } + }, + "additionalProperties": false }, - "liability": { + "trace_entry": { "type": "object", "required": [ - "seller_bond", - "verification_fund", - "cap", - "restitution_basis" + "event", + "at" ], "properties": { - "seller_bond": { - "$ref": "common.schema.json#/$defs/money" + "event": { + "enum": [ + "accepted", + "funded", + "deadline-passed", + "delivered", + "window-opened", + "verdict", + "verdict-lapsed", + "challenge", + "dispute-lapsed", + "window-closed", + "child-registered", + "child-final", + "child-unresolved", + "children-final", + "terminal" + ] }, - "verification_fund": { - "$ref": "common.schema.json#/$defs/money" + "at": { + "$ref": "common.schema.json#/$defs/timestamp" }, - "cap": { - "$ref": "common.schema.json#/$defs/money" + "object": { + "$ref": "common.schema.json#/$defs/hash" }, - "restitution_basis": { + "ref": { + "type": "string" + }, + "outcome": { "enum": [ - "released", - "price" + "PASS", + "FAIL" ] }, - "parent": { - "type": "object", - "required": [ - "vtc_id", - "vtc_hash" - ], - "properties": { - "vtc_id": { - "type": "string" - }, - "vtc_hash": { - "$ref": "common.schema.json#/$defs/hash" - } - }, - "additionalProperties": false - }, - "remainder_to": { + "answers": { + "$ref": "common.schema.json#/$defs/hash" + }, + "supersedes": { + "$ref": "common.schema.json#/$defs/hash" + }, + "signer": { + "type": "string", + "minLength": 1 + }, + "closes_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "costs": { + "$ref": "common.schema.json#/$defs/amount_with_currency" + }, + "facilitator": { + "$ref": "common.schema.json#/$defs/party" + }, + "child": { + "$ref": "common.schema.json#/$defs/hash" + }, + "state": { "enum": [ - "buyer", - "sink" - ], - "$comment": "Section 5.3. Absent means sink." + "FINAL", + "SETTLED", + "ABANDONED" + ] + }, + "challenge_upheld": { + "type": "boolean" + } + }, + "$comment": "Section 4.2, Table 1. Which optional members an event carries is stated there and checked in code.", + "additionalProperties": false + }, + "transfer": { + "type": "object", + "required": [ + "event", + "from", + "to", + "amount", + "code" + ], + "properties": { + "event": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string", + "minLength": 1 + }, + "to": { + "type": "string", + "minLength": 1 + }, + "amount": { + "$ref": "common.schema.json#/$defs/money" + }, + "code": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "terms_result": { + "type": "object", + "required": [ + "profile", + "profile_hash", + "currency", + "transfers" + ], + "properties": { + "profile": { + "type": "string" + }, + "profile_hash": { + "$ref": "common.schema.json#/$defs/hash" + }, + "currency": { + "type": "string" + }, + "transfers": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/transfer" + } + } + }, + "additionalProperties": false + }, + "parties": { + "type": "object", + "required": [ + "buyer", + "seller", + "facilitator" + ], + "properties": { + "buyer": { + "$ref": "common.schema.json#/$defs/party" + }, + "seller": { + "$ref": "common.schema.json#/$defs/party" + }, + "facilitator": { + "$ref": "common.schema.json#/$defs/party" + }, + "verifier": { + "$ref": "common.schema.json#/$defs/party" } }, + "$comment": "buyer and seller MUST be distinct after normalization; JSON Schema cannot compare siblings, so code enforces it (Section 14.2).", "additionalProperties": false } } diff --git a/schemas/delivery.schema.json b/schemas/delivery.schema.json index 5830ac9..1f571c5 100644 --- a/schemas/delivery.schema.json +++ b/schemas/delivery.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "delivery.schema.json", - "title": "PACT Delivery", + "title": "PACT Delivery (0.2)", "type": "object", "additionalProperties": false, "required": [ @@ -15,7 +15,7 @@ ], "properties": { "pact": { - "type": "string" + "const": "0.2" }, "type": { "const": "Delivery" @@ -36,10 +36,6 @@ "input_hash": { "$ref": "common.schema.json#/$defs/hash" }, - "delivered_at": { - "type": "string", - "format": "date-time" - }, "evidence": { "type": "object", "required": [ @@ -60,7 +56,7 @@ "format": "uri" } }, - "$comment": "Section 6. Tier-conditional members enforced in code.", + "$comment": "Section 6. Tier- and profile-conditional members are enforced in code. delivered_at is gone: the trace carries the Facilitator's time.", "additionalProperties": false }, "signature": { diff --git a/schemas/facilitator.schema.json b/schemas/facilitator.schema.json index 0347681..42fe401 100644 --- a/schemas/facilitator.schema.json +++ b/schemas/facilitator.schema.json @@ -1,25 +1,29 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "facilitator.schema.json", - "title": "PACT Facilitator Capability Document", + "title": "PACT Facilitator Capability Document (0.2)", "type": "object", "additionalProperties": false, "required": [ "pact", + "type", "facilitator", "settlement_bindings", - "release_modes", + "flows", "verification_profiles", - "assurance_modes", + "terms_profiles", "endpoints", "signature" ], "properties": { "pact": { - "type": "string" + "const": "0.2" + }, + "type": { + "const": "FacilitatorCapabilities" }, "facilitator": { - "$ref": "common.schema.json#/$defs/did" + "$ref": "common.schema.json#/$defs/party" }, "settlement_bindings": { "type": "array", @@ -31,7 +35,8 @@ ], "properties": { "id": { - "type": "string" + "type": "string", + "minLength": 1 }, "networks": { "type": "array", @@ -49,48 +54,52 @@ "additionalProperties": false } }, - "release_modes": { + "flows": { "type": "array", "minItems": 1, "contains": { - "const": "on-verification" + "const": "verdict-first" }, "items": { - "$ref": "common.schema.json#/$defs/release_mode" + "$ref": "common.schema.json#/$defs/flow" }, - "$comment": "Section 7.3: on-verification is REQUIRED to implement." + "$comment": "Section 7.1: verdict-first is REQUIRED to implement." }, "verification_profiles": { "type": "array", + "minItems": 1, "items": { "type": "string" } }, - "assurance_modes": { + "terms_profiles": { "type": "array", + "minItems": 1, "items": { - "enum": [ - "certain", - "committed-sample", - "open" - ] - } - }, - "max_contract_value": { - "type": "object", - "required": [ - "amount", - "currency" - ], - "properties": { - "amount": { - "$ref": "common.schema.json#/$defs/money" + "type": "object", + "required": [ + "id", + "profile_hash" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "profile_hash": { + "$ref": "common.schema.json#/$defs/hash" + } }, - "currency": { - "type": "string" - } + "additionalProperties": false }, - "additionalProperties": false + "$comment": "Section 8: the terms profiles, at the revisions named, whose schedules this Facilitator evaluates and whose vectors it reproduces." + }, + "max_contract_value": { + "$ref": "common.schema.json#/$defs/amount_with_currency" + }, + "challenge_deposit": { + "$ref": "common.schema.json#/$defs/amount_with_currency", + "$comment": "Section 7.3. Absent means no deposit is required." }, "endpoints": { "type": "object", @@ -99,7 +108,7 @@ "delivery", "verdict", "challenge", - "attestation" + "outcome" ], "additionalProperties": { "type": "string", @@ -108,23 +117,6 @@ }, "signature": { "$ref": "common.schema.json#/$defs/signature" - }, - "challenge_deposit": { - "type": "object", - "required": [ - "amount", - "currency" - ], - "properties": { - "amount": { - "$ref": "common.schema.json#/$defs/money" - }, - "currency": { - "type": "string" - } - }, - "$comment": "Section 8. Absent means no deposit is required.", - "additionalProperties": false } } } diff --git a/schemas/outcome.schema.json b/schemas/outcome.schema.json new file mode 100644 index 0000000..3b6b9e2 --- /dev/null +++ b/schemas/outcome.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "outcome.schema.json", + "title": "PACT Outcome Record (0.2)", + "type": "object", + "additionalProperties": false, + "required": [ + "pact", + "type", + "vtc_id", + "vtc_hash", + "parties", + "outcome", + "trace", + "terms_result", + "signatures" + ], + "properties": { + "pact": { + "const": "0.2" + }, + "type": { + "const": "OutcomeRecord" + }, + "vtc_id": { + "type": "string" + }, + "vtc_hash": { + "$ref": "common.schema.json#/$defs/hash" + }, + "parties": { + "$ref": "common.schema.json#/$defs/parties" + }, + "outcome": { + "type": "object", + "required": [ + "state", + "challenge_upheld" + ], + "properties": { + "state": { + "enum": [ + "FINAL", + "SETTLED", + "ABANDONED" + ] + }, + "challenge_upheld": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "work_hash": { + "$ref": "common.schema.json#/$defs/hash" + }, + "trace": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "common.schema.json#/$defs/trace_entry" + }, + "$comment": "Section 12: the complete trace, ending with the terminal entry." + }, + "terms_result": { + "$ref": "common.schema.json#/$defs/terms_result" + }, + "children_merkle_root": { + "$ref": "common.schema.json#/$defs/hash", + "$comment": "Section 12.2: present when at least one child is registered, absent otherwise, never null." + }, + "signatures": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "common.schema.json#/$defs/signature" + }, + "$comment": "MUST include the Facilitator's; code checks which." + } + } +} diff --git a/schemas/status.schema.json b/schemas/status.schema.json new file mode 100644 index 0000000..ef4fe81 --- /dev/null +++ b/schemas/status.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "status.schema.json", + "title": "PACT Contract Status (0.2)", + "type": "object", + "additionalProperties": false, + "required": [ + "pact", + "type", + "vtc_id", + "vtc_hash", + "state", + "trace", + "issued_at", + "signature" + ], + "properties": { + "pact": { + "const": "0.2" + }, + "type": { + "const": "ContractStatus" + }, + "vtc_id": { + "type": "string" + }, + "vtc_hash": { + "$ref": "common.schema.json#/$defs/hash" + }, + "state": { + "$ref": "common.schema.json#/$defs/state" + }, + "trace": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "common.schema.json#/$defs/trace_entry" + }, + "$comment": "Section 11: the trace so far, in the order recorded; a prefix of every later Status for the same contract." + }, + "issued_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "signature": { + "$ref": "common.schema.json#/$defs/signature", + "$comment": "The Facilitator's." + } + } +} diff --git a/schemas/taskspec.schema.json b/schemas/taskspec.schema.json index 449a787..4ca10dc 100644 --- a/schemas/taskspec.schema.json +++ b/schemas/taskspec.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://pact-spec.github.io/spec/schemas/taskspec.schema.json", - "title": "PACT TaskSpec", + "title": "PACT TaskSpec (0.2)", "type": "object", "required": [ "description", @@ -13,9 +13,6 @@ "type": "string", "minLength": 1 }, - "skill": { - "type": "string" - }, "inputs": { "type": "object", "properties": { @@ -32,12 +29,17 @@ }, "sample_hash": { "$ref": "common.schema.json#/$defs/hash" - }, - "size_hint": { - "type": "object" } }, - "$comment": "A hash commitment covers only the octets hashed. spec_hash covers this URI string, not the bytes it dereferences to, so every URI whose content is consumed during bidding, execution, or verification needs a sibling hash. sample_hash and schema_hash are OPTIONAL in -00 and become REQUIRED alongside their URIs in -01; see https://github.com/pact-spec/spec/issues/1 entry 6.", + "dependentRequired": { + "schema_uri": [ + "schema_hash" + ], + "sample_uri": [ + "sample_hash" + ] + }, + "$comment": "Section 5.1: every URI inside hash-committed content carries a sibling hash over the dereferenced bytes. The -01 schema required it only for the acceptance members and the -01 example broke the rule for the other three.", "additionalProperties": false }, "deliverable": { @@ -57,6 +59,11 @@ "$ref": "common.schema.json#/$defs/hash" } }, + "dependentRequired": { + "schema_uri": [ + "schema_hash" + ] + }, "additionalProperties": false }, "acceptance": { @@ -105,7 +112,7 @@ "minProperties": 1 } }, - "$comment": "Section 5.2. Closed per Section 2. Members for the attestation and proving tiers are named in the document's prose and not as member names, so a TaskSpec for those tiers does not validate in -01; recorded as a residual.", + "$comment": "Section 5.2. Members for the attestation and proving tiers are named in prose and not as member names, so a TaskSpec for those tiers does not validate; recorded as a residual.", "additionalProperties": false }, "constraints": { diff --git a/schemas/verdict.schema.json b/schemas/verdict.schema.json index acf51a1..cd6435f 100644 --- a/schemas/verdict.schema.json +++ b/schemas/verdict.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "verdict.schema.json", - "title": "PACT Verdict", + "title": "PACT Verdict (0.2)", "type": "object", "additionalProperties": false, "required": [ @@ -12,12 +12,13 @@ "outcome", "profile", "instrument_hash", + "results_hash", "evaluated_at", "signature" ], "properties": { "pact": { - "type": "string" + "const": "0.2" }, "type": { "const": "Verdict" @@ -26,7 +27,12 @@ "type": "string" }, "delivery_hash": { - "$ref": "common.schema.json#/$defs/hash" + "$ref": "common.schema.json#/$defs/hash", + "$comment": "The digest of the Delivery including its signature (Section 2)." + }, + "challenge_hash": { + "$ref": "common.schema.json#/$defs/hash", + "$comment": "Present when the Verdict answers a Challenge (Section 7.2)." }, "outcome": { "enum": [ @@ -44,8 +50,7 @@ "$ref": "common.schema.json#/$defs/hash" }, "evaluated_at": { - "type": "string", - "format": "date-time" + "$ref": "common.schema.json#/$defs/timestamp" }, "signature": { "$ref": "common.schema.json#/$defs/signature" diff --git a/schemas/vtc.schema.json b/schemas/vtc.schema.json index fc0765e..94477c4 100644 --- a/schemas/vtc.schema.json +++ b/schemas/vtc.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://pact-spec.github.io/spec/schemas/vtc.schema.json", - "title": "PACT Verifiable Task Contract", + "title": "PACT Verifiable Task Contract (0.2)", "type": "object", "required": [ "pact", @@ -11,45 +11,24 @@ "task", "price", "verification", - "assurance", - "release", - "liability", + "flow", + "terms", "challenge", "signatures" ], "properties": { "pact": { - "type": "string" + "const": "0.2" }, "type": { "const": "VerifiableTaskContract" }, "id": { - "type": "string" + "type": "string", + "minLength": 1 }, "parties": { - "type": "object", - "required": [ - "buyer", - "seller", - "facilitator" - ], - "properties": { - "buyer": { - "$ref": "common.schema.json#/$defs/did" - }, - "seller": { - "$ref": "common.schema.json#/$defs/did" - }, - "facilitator": { - "$ref": "common.schema.json#/$defs/did" - }, - "verifier": { - "$ref": "common.schema.json#/$defs/did" - } - }, - "$comment": "buyer and seller MUST be distinct. JSON Schema cannot compare sibling values, so tools/validate.py enforces it: a self-dealt contract otherwise validates and satisfies the two-signature rule with one key signing twice.", - "additionalProperties": false + "$ref": "common.schema.json#/$defs/parties" }, "task": { "type": "object", @@ -66,8 +45,7 @@ "format": "uri" }, "deadline": { - "type": "string", - "format": "date-time" + "$ref": "common.schema.json#/$defs/timestamp" } }, "additionalProperties": false @@ -88,11 +66,12 @@ "type": "string" }, "settlement": { - "type": "string" + "type": "string", + "minLength": 1, + "$comment": "A URI naming a settlement binding (Section 3.2). No registry." }, "network": { - "type": "string", - "$comment": "Section 2: form defined by the settlement binding; pact-escrow uses CAIP-2." + "type": "string" } }, "additionalProperties": false @@ -101,44 +80,51 @@ "type": "object", "required": [ "tier", + "profile", "criteria_hash", - "profile" + "max_verdict_seconds" ], "properties": { "tier": { "$ref": "common.schema.json#/$defs/tier" }, + "profile": { + "type": "string", + "minLength": 1 + }, "criteria_hash": { "$ref": "common.schema.json#/$defs/hash" }, - "arbiter": { - "$ref": "common.schema.json#/$defs/did" + "max_verdict_seconds": { + "type": "integer", + "minimum": 1, + "$comment": "Section 7.2: bounds the wait for a first Verdict under verdict-first; makes L finite (Section 10.3)." }, - "profile": { - "type": "string" + "arbiter": { + "$ref": "common.schema.json#/$defs/party" } }, "additionalProperties": false }, - "liability": { - "$ref": "common.schema.json#/$defs/liability" + "flow": { + "$ref": "common.schema.json#/$defs/flow" + }, + "terms": { + "$ref": "common.schema.json#/$defs/terms" }, "challenge": { "$ref": "common.schema.json#/$defs/challenge" }, + "parent": { + "$ref": "common.schema.json#/$defs/parent" + }, "signatures": { "type": "array", "minItems": 2, "items": { "$ref": "common.schema.json#/$defs/signature" }, - "$comment": "minItems does not express the actual rule, which is one signature per party named in parties. Two buyer signatures and no seller signature satisfy this schema; tools/validate.py enforces the real rule." - }, - "assurance": { - "$ref": "common.schema.json#/$defs/assurance" - }, - "release": { - "$ref": "common.schema.json#/$defs/release_mode" + "$comment": "Exactly one entry covering parties.buyer and one covering parties.seller, sorted by normalized kid; minItems cannot say that, code does (Section 14.1, 14.2)." } }, "additionalProperties": false diff --git a/tools/README.md b/tools/README.md index f0b37bb..b0eead0 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,15 +1,17 @@ # tools -Five programs. The first checks the committed examples; the rest are a working -implementation of the protocol. +Seven programs. Two check what is committed; the rest are a working +implementation of draft-laxsharma-pact-02. | File | What it is | |---|---| -| `validate.py` | The conformance validator: 71 checks over the committed examples, the Section 13.3 vectors, and the two vectors V-21 and V-22 added here ahead of the text (choice 7 below). Needs only `jsonschema` and `referencing`. It recomputes digests and Merkle roots but verifies no signature; the constraint, the normalization and the signature-set rules are imported from `pactcore.py` so the two tools cannot disagree. | -| `pactcore.py` | Canonicalization, digests, JWS signing and verification over the transmitted protected header, identifier normalization, the assurance constraint in exact decimal arithmetic, and the RFC 9162 Merkle tree. | -| `facilitator.py` | A reference Facilitator: the six operations of Table 1 over five paths, the Figure 2 state machine including the challenge window and the Figure 6 overturned-PASS path, the Section 7.4 waterfall, schema validation of every posted object, a signed capability document, and RFC 9457 refusals in the draft's own namespace that name the rule. `--rules` prints what it enforces and what it chose; `--problems` prints every problem type it emits with its status and section. | -| `agents.py` | Buyer, Seller, Verifier and Challenger clients. | -| `measure.py` | Drives five contracts through the terminal states on a clock the harness advances, exercises 24 refusals and 2 acceptances each on the rule it is named for, asserts that money balances, checks every minted object and the capability document against the schemas, and reports costs. | +| `validate.py` | The conformance validator: 103 checks over the committed examples and the profile bundle. Schemas, canonicalization, every digest the objects commit to, every signature (with the public keys in `examples/keys/`), the profile's vectors and the two lists printed in Appendix A.6, the RFC 9162 tree, and every vector of Section 14.3 run through the reference Facilitator. | +| `mint_examples.py` | Mints `examples/` from public seeds. Ed25519 is deterministic, so anyone who runs it gets the same bytes and the same digests the draft prints; `--check` diffs against disk. | +| `pactcore.py` | RFC 8785 canonicalization including ECMAScript number formatting, digests, JWS signing and verification over the transmitted protected header, identifier normalization, sorted signature sets, low-S ECDSA, the manifest digest, and the RFC 9162 Merkle tree. | +| `profile.py` | The example terms profile of Appendix A, `bonded-restitution`: its admission rule, its schedule over a trace, its two invariants, and the generator of `profiles/bonded-restitution/vectors.json`. The only file in this directory that knows what an amount is for. | +| `facilitator.py` | A reference Facilitator: the operations of Section 13 over a signed event trace, the state machine of Section 4 including verdict-first and delivery-first flows, lapses, contract trees within one venue, a signed Status on every accepted request, one signed Outcome Record per terminal contract, and RFC 9457 refusals that name the section or the profile section. `--rules` prints what it enforces and what it chose; `--problems` prints every problem type it emits. | +| `agents.py` | Buyer, Seller, Verifier and Challenger clients that check what they are handed. | +| `measure.py` | Drives ten paths through the terminal states on a clock the harness advances, checks every Outcome Record the way a party would, reproduces the seven profile vectors, exercises 40 refusals and 5 acceptances, and reports what each path cost on the wire. | ``` pip install jsonschema referencing cryptography @@ -18,131 +20,133 @@ python3 tools/measure.py python3 tools/facilitator.py --rules ``` -`validate.py` needs only the first two packages, so a checkout still validates -on a machine with nothing else installed. The Facilitator refuses to start -without `jsonschema`, because Section 12.1 makes schema conformance a MUST at -Propose and a Facilitator that skips it is not one. +`validate.py` runs without `cryptography`, printing `[skip]` for the signature +checks and the vectors that go through the Facilitator, which need it. The +count of 103 is with all three packages installed, and that is what CI runs. +The Facilitator refuses to start without `jsonschema`, because Section 14.2 +makes schema conformance a MUST and a Facilitator that skips it is not one. ## What this does and does not answer -Section 15 of the draft records that no Facilitator, Buyer or Seller exchanging -messages over the Section 12 endpoints was known to the author when -01 was -posted. This is that implementation, and it is one implementation written by the -same person who wrote the specification, which is the weakest possible evidence -that the specification is implementable. The falsifiable experiment of Section -1.4 needs *two independent* implementations settling each other's contracts. -This is an invitation for the second, not a substitute for it. - -The signatures are real Ed25519. The contracts `measure.py` mints are fresh and -so are the keys, deliberately: the committed examples under `examples/` keep -their placeholder signature values because the published Internet-Draft prints -their digests in Section 14 and cannot be corrected, so re-signing them would -silently desynchronise this repository from that document. Real keys and -recomputed digests belong together in a -02. - -No payment rail is touched. Section 1.2 puts the rail out of scope and -`price.settlement` names a binding; money here is an integer number of cents in -three pools. What is real is the object flow, the state machine, the signature -verification and the arithmetic. +Section 16 of the draft says one implementation exists and the author wrote +it. That is this directory, and it is the weakest possible evidence that the +specification is implementable. The experiment of Section 1.4 needs two +independent Facilitators producing the same trace from the same posted records +and the same transfer list from the same trace and profile. This is the first +half of that experiment and an invitation for the second. + +Nothing here holds or moves money. The Facilitator records events and hands +each one to the profile the contract names; what comes back is a list of +transfers between named accounts, and the Facilitator signs it without +understanding it. The -01 tools kept three pools of cents; those are gone with +the -01 text. ## Not implemented, and refused rather than faked A contract that needs any of these is refused at Propose with a problem body -saying so, rather than accepted and then stranded or silently mishandled: -subcontracts (Section 10; `liability.parent` is refused as `parent-unresolvable`), -release modes other than on-verification, assurance modes other than certain, -verification profiles other than acceptance, challenge deposits, settlement -bindings other than the one the capability document advertises, and amounts -finer than a cent. Key resolution is an in-process registry with the Section -13.1.1 interface; the network lookup is the part that is stubbed. +saying so: the `no-window` flow, challenge deposits, network key resolution, +any payment rail, the committed-sample draw, settlement bindings other than the +one the capability document advertises, verification profiles other than +`acceptance`, terms profiles other than the one whose vectors reproduce, and +prices finer than a cent. ## Choices the draft left to the implementer -Each of these is a place where the -01 text is silent or says two things. -`facilitator.py --rules` prints the same list. They are choices, not rules, and -a second implementation is free to choose differently, which is exactly the -kind of disagreement the experiment exists to surface. - -1. **The Bond on ABANDONED.** Section 6 slashes it "to the extent of - `restitution_basis`", which under `released` with nothing released is zero. - Section 7.6 returns the Bond on FINAL or SETTLED and says nothing about the - third terminal state. This implementation returns it. Measured: a Seller that - signs, posts 18.00, and never delivers gets the whole 18.00 back. -2. **Rank 3 restores the Buyer's loss, net of rank 1.** Read literally, basis - `price` would pay the Bond on top of a reversed escrow in the pre-release - failure, a windfall the draft's own `remainder_to` rule exists to prevent. - Under the net reading the two basis values differ only when release was - partial; under on-verification they never differ. -3. **The bounty.** The draft requires it to be non-exclusive and forbids capping - it at a fraction "chosen for tidiness", and does not fix it. This - implementation pays the whole remaining Bond after rank 3, split equally among - successful Challengers. With K independent discoverers a full bounty each is - not fundable from one Bond, which the draft's text assumes it is. -4. **Rank 2 pays 0.00.** The Challenge object has no member for the documented - costs rank 2 reimburses, and its schema is closed. -5. **A Challenge with no Verdict inside `max_dispute_seconds` lapses**; the - earlier Verdict stands and the window is not extended. The draft declares the - bound and never applies it. -6. **PROPOSED is not observable.** With no rail the pools are debited in memory - when a co-signed contract is accepted, so the 201 reports FUNDED. -7. **The signature set is sorted and ECDSA is low-S.** Section 6 digests the - contract including its `signatures` array, and the -01 text does not order - that array, so one agreement signed in two orders has two `vtc_hash` values. - This implementation refuses an array not sorted by the Section 9.1 - normalized kid (ties by the raw kid, code point order) as - `signatures-unordered`, and refuses an ECDSA signature whose s is in the high - half of the curve order, for the same reason: a second valid encoding of one - signature is a second digest. Both are checked in `validate.py` and both - are proposed as rules for the next revision. It also computes `delivery_hash` over the Delivery - including its signature, which is what `validate.py` now checks too; the - v0.1.0 validator excluded the signature and the two tools disagreed. - -## Measured on 12 September 2026 - -Intel Core i9-9880H at 2.30 GHz, Python 3.12.11, Ed25519, single host, loopback +`facilitator.py --rules` prints the same list. A second implementation is free +to choose differently, which is the kind of disagreement the experiment exists +to surface. + +1. **funded is recorded in the same call as accepted.** There is no rail, so + nothing can be observed, and the Status of a 201 already reads FUNDED. +2. **Retrieval is open on the loopback interface.** Section 17.12 restricts it + by default and leaves the mechanism to the deployment. This process emits + no `retrieval-restricted`; a deployment puts authentication in front of it. +3. **Trees within one venue.** A registered child that lives in this process is + noticed when it reaches a terminal state; any other child's Outcome Record + must be supplied by POST. No cross-venue GET is made. +4. **Whole cents.** Amounts are settled by the profile's own decimal arithmetic + and a price with more than two decimals is refused as `amount-invalid`. +5. **Nothing else.** Everything the -01 tools had to choose about value, the + bond on ABANDONED, the bounty, rank 2, the net reading of rank 3, is now the + profile's, and Appendix A states each one. + +## Measured on 16 September 2026 + +Intel Core i9-9880H at 2.30 GHz, Python 3.12, Ed25519, single host, loopback HTTP, in-memory store, no payment rail, the Facilitator's clock advanced by the -harness. The same code has produced per-call figures two to three times apart -across sessions on the same laptop; the order of magnitude is the result. +harness. The per-call figures move two to three times between sessions on the +same laptop; the order of magnitude is the result. -| Path | Exchanges | Request / response bytes | Attestation amounts (settled / restituted / slashed) | +| Path | Exchanges | Request / response bytes | What the profile moved at the end | |---|---|---|---| -| FINAL: PASS, window closes | 4 | 3,065 / 4,011 | 180.00 / 0.00 / 0.00 | -| SETTLED: verifier FAIL | 4 | 3,071 / 4,014 | 0.00 / 0.00 / 18.00 | -| ABANDONED: no Delivery | 3 | 1,469 / 3,780 | 0.00 / 0.00 / 0.00 | -| SETTLED: PASS overturned by a Challenge | 6 | 4,458 / 5,446 | 180.00 / 18.00 / 18.00 | - -Each lifecycle completes in 25 to 45 ms, most of it schema validation of the -posted objects. Per call, medians: canonicalize a contract 51 us; canonicalize -and digest 60 us; sign a contract including canonicalization 128 us; verify a -contract signature end to end 198 us, of which the Ed25519 primitive over 1.5 KB -is 123 us; normalize an identifier 1.3 us; the assurance constraint in exact -decimal 2.1 us; an RFC 9162 root over 2, 8 and 64 leaves 4, 21 and 179 us. - -The overturned-PASS row is the one the restitution basis does any work in, and -its amounts are what Section 11's worked attestation should carry: the draft's -example has restituted 18.00 with settled 0.00, which fits neither path. +| FINAL: verdict-first, PASS, window closes | 4 | 3,301 / 4,636 | principal 180.00 to the seller, bond and fund returned | +| SETTLED: the Verifier records FAIL | 4 | 3,301 / 4,549 | escrow 180.00 back to the buyer, bond 18.00 to the sink | +| ABANDONED: deadline, no Delivery | 2 | 1,735 / 2,320 | escrow back, bond and fund returned | +| SETTLED: PASS overturned by a Challenge | 6 | 4,778 / 8,212 | principal 180.00 stays, restitution 18.00 to the buyer, costs 0.50 to the challenger | +| SETTLED: the same with `restitution_basis` price | 6 | 4,775 / 8,212 | the same amounts (cap 180.00, bond 18.00) | +| FINAL after verdict-lapsed | 4 | 2,646 / 4,372 | principal 180.00 | +| FINAL under delivery-first, principal at window-closed | 3 | 2,653 / 3,487 | principal 180.00 | +| FINAL after dispute-lapsed, the PASS stands | 5 | 4,032 / 6,250 | principal 180.00, costs unpaid | +| FINAL with one child in the same venue | 9 | 8,631 / 11,372 | principal 180.00; `children_merkle_root` recomputes | +| FINAL with one child unresolved at L(child) | 6 | 5,184 / 7,514 | principal 180.00; the root is MTH of the empty list | + +The first seven rows reproduce the seven vectors in +`profiles/bonded-restitution/vectors.json` exactly. Every Outcome Record was +checked the way a party would check it: the Facilitator's signature verifies, +the transfer list recomputes from the trace with the named profile, no account +is overdrawn and every internal account closes at zero, and every Status +received along the way is a prefix of the final trace. + +The messages of the first and fourth rows, request bytes and response bytes: + +``` +FINAL SETTLED, overturned +POST contracts 201 1735 / 639 POST contracts 201 1735 / 639 +POST deliveries 202 911 / 775 POST deliveries 202 911 / 775 +POST verdicts 201 655 / 1053 POST verdicts 201 655 / 1053 +GET contract 200 0 / 2169 POST challenges 202 731 / 1263 + POST verdicts 201 746 / 1766 + GET contract 200 0 / 2716 +``` + +Each lifecycle completes in 20 to 85 ms, most of it schema validation of the +posted objects (1.7 ms per contract). Per call, medians: canonicalize a +contract 217 us; canonicalize and digest 227 us; sign a Verdict including +canonicalization 151 us; verify a Verdict end to end 263 us; normalize an +identifier 1.9 us; the profile's schedule over the nine-entry overturned trace +27 us; an RFC 9162 root over 2, 8 and 64 leaves 6, 29 and 253 us. +Canonicalization is four times slower than the v0.1.0 figure because it no +longer delegates to `json.dumps`; see the third correction below. There is no verification-cost figure. The example instrument is a pytest module -whose runtime says nothing about real work, and an earlier version of this file -reported a number for it that was pytest's import time. +whose runtime says nothing about real work. ## Corrections -This file has been wrong twice, and both are recorded here rather than deleted, -because the point of publishing a specification for demolition is lost if the -corrections are not published too. +This file records where it was wrong rather than deleting it, because the point +of publishing a specification for demolition is lost if the corrections are +not published too. An earlier version claimed, as a defect, that "a defrauded buyer still recovers nothing from the bond". That was wrong, and it was wrong in the transcript -printed directly beneath it: rank 1 returns the whole escrow to the Buyer before -rank 3 is reached, so the Buyer's loss is zero and a restitution payment of zero -is correct. - -An earlier version of `facilitator.py` returned the Bond and reached FINAL in the -same call that recorded a PASS, so no challenge window ever opened and the -Figure 6 path was unreachable in the only release mode the draft requires. A -second adversarial review on 11 September found that, along with a deadline -parsed in local time, Verdict and Challenge commitments that could be bypassed -by omitting a member, a bounty paid to a Challenger that did not exist, an -unsigned capability document, and no schema validation at Propose. All are fixed -and the numbers above are from the corrected code. +printed directly beneath it: rank 1 returned the whole escrow to the Buyer +before rank 3 was reached, so the Buyer's loss was zero and a restitution of +zero was correct. + +An earlier `facilitator.py` returned the Bond and reached FINAL in the same call +that recorded a PASS, so no challenge window ever opened. A second adversarial +review on 11 September 2026 found that, along with a deadline parsed in local +time, Verdict and Challenge commitments that could be bypassed by omitting a +member, a bounty paid to a Challenger that did not exist, an unsigned +capability document, and no schema validation at Propose. All were fixed in +v0.1.0. + +Until 16 September 2026 `pactcore.jcs` serialized the float `1.0` as `1.0`, +where RFC 8785 requires `1`. Every digest v0.1.0 printed over an object that +carried a float was therefore not the digest an RFC 8785 canonicalizer would +compute, and a second implementation would have disagreed with this one on +`spec_hash` and `vtc_hash` for the worked contract without either being able to say why. The +validator's own number check caught it while the -02 examples were being +minted; the fix is a canonicalizer that formats numbers as ECMAScript does, +vector V-25 in the draft pins it, and every -02 digest was minted after the +fix. diff --git a/tools/agents.py b/tools/agents.py index 45fe6a5..e254a95 100644 --- a/tools/agents.py +++ b/tools/agents.py @@ -2,12 +2,12 @@ Each party holds one key and speaks HTTP to a Facilitator. Nothing here trusts the Facilitator's word for anything it can check itself: a party verifies the -attestation it is handed, and recomputes the contract digest rather than -accepting the one it is told. +Status and the Outcome Record it is handed, and recomputes every digest rather +than accepting the one it is told. -The contracts these agents mint are fresh, with fresh keys and real signatures. -They are deliberately NOT the committed examples under examples/, whose digests -the published Internet-Draft prints in Section 14 and cannot be corrected. +The contracts these agents mint are fresh, with fresh keys and real signatures; +the committed examples under examples/ are minted separately, reproducibly, by +mint_examples.py. """ from __future__ import annotations @@ -16,15 +16,21 @@ import time import urllib.error import urllib.request -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any import pactcore as pc +import profile as terms -MEDIA_CONTRACT = "application/pact-contract+json" -MEDIA_DELIVERY = "application/pact-delivery+json" -MEDIA_VERDICT = "application/pact-verdict+json" -MEDIA_CHALLENGE = "application/pact-challenge+json" +MEDIA_CONTRACT = "application/vnd.pact.contract+json" +MEDIA_DELIVERY = "application/vnd.pact.delivery+json" +MEDIA_VERDICT = "application/vnd.pact.verdict+json" +MEDIA_CHALLENGE = "application/vnd.pact.challenge+json" +MEDIA_STATUS = "application/vnd.pact.status+json" +MEDIA_OUTCOME = "application/vnd.pact.outcome+json" +MEDIA_FACILITATOR = "application/vnd.pact.facilitator+json" + +BASE_PATH = "/pact/v2" @dataclass @@ -67,13 +73,17 @@ def post(self, path: str, body: dict, ct: str) -> tuple[int, dict]: def get(self, path: str) -> tuple[int, dict]: return self._call("GET", path, None, None) - # -- named operations, Table 1 ---------------------------------------- - def propose(self, vtc): return self.post("/pact/v1/contracts", vtc, MEDIA_CONTRACT) - def deliver(self, d): return self.post("/pact/v1/deliveries", d, MEDIA_DELIVERY) - def verdict(self, v): return self.post("/pact/v1/verdicts", v, MEDIA_VERDICT) - def challenge(self, c): return self.post("/pact/v1/challenges", c, MEDIA_CHALLENGE) - def contract(self, vid): return self.get(f"/pact/v1/contracts/{vid}") - def attestation(self, vid): return self.get(f"/pact/v1/attestations/{vid}") + # -- the operations of Section 13 ------------------------------------- + def propose(self, vtc): return self.post(f"{BASE_PATH}/contracts", vtc, MEDIA_CONTRACT) + def status(self, vid): return self.get(f"{BASE_PATH}/contracts/{vid}") + def register_child(self, parent_id, child_vtc): + return self.post(f"{BASE_PATH}/contracts/{parent_id}/children", child_vtc, MEDIA_CONTRACT) + def supply_child_outcome(self, parent_id, child_id, record): + return self.post(f"{BASE_PATH}/contracts/{parent_id}/children/{child_id}", record, MEDIA_OUTCOME) + def deliver(self, d): return self.post(f"{BASE_PATH}/deliveries", d, MEDIA_DELIVERY) + def verdict(self, v): return self.post(f"{BASE_PATH}/verdicts", v, MEDIA_VERDICT) + def challenge(self, c): return self.post(f"{BASE_PATH}/challenges", c, MEDIA_CHALLENGE) + def outcome(self, vid): return self.get(f"{BASE_PATH}/outcomes/{vid}") def capability(self): return self.get("/.well-known/pact-facilitator") @@ -97,20 +107,36 @@ def make_party(did: str, resolver: pc.KeyResolver, client: Client, # Contract construction # -------------------------------------------------------------------------- +_PROFILE = None + + +def default_profile() -> terms.BondedRestitution: + global _PROFILE + if _PROFILE is None: + _PROFILE = terms.BondedRestitution() + return _PROFILE + + def draft_contract(vid: str, buyer: str, seller: str, facilitator: str, verifier: str | None, *, price: str = "180.00", bond: str = "18.00", - fund: str = "0.50", q_min: float = 0.9091, + fund: str = "0.50", q_min: float = 1.0, deadline: str = "2027-01-01T00:00:00Z", - release: str = "on-verification", + flow: str = "verdict-first", principal_on: str = "verdict", restitution_basis: str = "released", - spec_hash: str | None = None, - criteria_hash: str | None = None) -> dict: - """An unsigned contract in the shape schemas/vtc.schema.json requires.""" + assurance_mode: str = "certain", + window_seconds: int = 3600, max_dispute_seconds: int = 86400, + max_verdict_seconds: int = 86400, + spec_hash: str | None = None, criteria_hash: str | None = None, + profile_id: str | None = None, profile_hash: str | None = None, + parent: dict | None = None, + settlement: str = "https://settle.example/bindings/ledger-1") -> dict: + """An unsigned 0.2 contract in the shape schemas/vtc.schema.json requires.""" parties = {"buyer": buyer, "seller": seller, "facilitator": facilitator} if verifier is not None: parties["verifier"] = verifier # absent: Section 9.1 is derived per signer - return { - "pact": "0.1", + prof = default_profile() + vtc = { + "pact": "0.2", "type": "VerifiableTaskContract", "id": vid, "parties": parties, @@ -121,45 +147,56 @@ def draft_contract(vid: str, buyer: str, seller: str, facilitator: str, }, "price": { "amount": price, "currency": "USDC", - "settlement": "pact-escrow", "network": "eip155:8453", + "settlement": settlement, "network": "eip155:8453", }, "verification": { "tier": "T0-reexec", "profile": "acceptance", "criteria_hash": criteria_hash or pc.h(b"criteria placeholder"), + "max_verdict_seconds": max_verdict_seconds, }, - "assurance": {"mode": "certain", "q_min": q_min}, - "release": release, - "liability": { - "seller_bond": bond, "verification_fund": fund, - "cap": price, "restitution_basis": restitution_basis, + "flow": flow, + "terms": { + "profile": profile_id or prof.id, + "profile_hash": profile_hash or prof.profile_hash, + "parameters": { + "seller_bond": bond, "verification_fund": fund, "cap": price, + "restitution_basis": restitution_basis, "remainder_to": "sink", + "principal_on": principal_on, + "assurance": {"mode": assurance_mode, "q_min": q_min}, + }, }, - "challenge": {"window_seconds": 3600, "max_dispute_seconds": 86400}, + "challenge": {"window_seconds": window_seconds, + "max_dispute_seconds": max_dispute_seconds}, } + if parent is not None: + vtc["parent"] = parent + return vtc -def cosign(vtc: dict, buyer: Party, seller: Party) -> dict: - """Both parties sign the same bytes: the contract without its signatures. - - That is what makes the digest meaningful. vtc_hash is then taken over the - contract WITH the signature set, so the commitment proves who agreed. - """ - entries = [pc.sign(vtc, buyer.key, MEDIA_CONTRACT), - pc.sign(vtc, seller.key, MEDIA_CONTRACT)] - vtc["signatures"] = entries +def cosign(vtc: dict, *parties: Party) -> dict: + """Every party signs the same bytes, the contract without its signatures, + and the entries are sorted by normalized kid as Section 14.1 requires, so + the digest over the signed contract does not depend on who signed last.""" + entries = [pc.sign(vtc, p.key, MEDIA_CONTRACT) for p in parties] + vtc["signatures"] = sort_signatures(entries) return vtc -def make_delivery(vtc: dict, seller: Party, work: bytes, - results: bytes) -> dict: +def sort_signatures(entries: list[dict]) -> list[dict]: + def kid(entry: dict) -> str: + return json.loads(pc.b64u_decode(entry["protected"]))["kid"] + return sorted(entries, key=lambda e: (pc.norm(kid(e)), kid(e))) + + +def make_delivery(vtc: dict, seller: Party, work: bytes, results: bytes) -> dict: d = { - "pact": "0.1", + "pact": "0.2", "type": "Delivery", "vtc_id": vtc["id"], - "vtc_hash": pc.digest_over(pc.hashable(vtc)), + "vtc_hash": pc.digest_over(vtc), "work_hash": pc.h(work), "work_uri": "https://cdn.seller.example/o/" + pc.h(work)[7:15], "input_hash": pc.h(b"inputs"), - "delivered_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "evidence": { "profile": "acceptance", "instrument_hash": vtc["verification"]["criteria_hash"], @@ -170,29 +207,31 @@ def make_delivery(vtc: dict, seller: Party, work: bytes, return seller.sign_into(d, MEDIA_DELIVERY) -def make_verdict(vtc: dict, delivery: dict, verifier: Party, - outcome: str) -> dict: +def make_verdict(vtc: dict, delivery: dict, verifier: Party, outcome: str, + challenge: dict | None = None) -> dict: v = { - "pact": "0.1", + "pact": "0.2", "type": "Verdict", "vtc_id": vtc["id"], - "delivery_hash": pc.digest_over(pc.hashable(delivery)), + "delivery_hash": pc.digest_over(delivery), "outcome": outcome, "profile": "acceptance", "instrument_hash": vtc["verification"]["criteria_hash"], "results_hash": delivery["evidence"]["results_hash"], "evaluated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), } + if challenge is not None: + v["challenge_hash"] = pc.digest_over(challenge) return verifier.sign_into(v, MEDIA_VERDICT) -def make_challenge(vtc: dict, delivery: dict, challenger: Party, - failing: list[str]) -> dict: +def make_challenge(vtc: dict, delivery: dict, challenger: Party, failing: list[str], + costs: str | None = "1.20") -> dict: c = { - "pact": "0.1", + "pact": "0.2", "type": "Challenge", "vtc_id": vtc["id"], - "delivery_hash": pc.digest_over(pc.hashable(delivery)), + "delivery_hash": pc.digest_over(delivery), "proof": { "profile": "acceptance", "instrument_hash": vtc["verification"]["criteria_hash"], @@ -201,17 +240,26 @@ def make_challenge(vtc: dict, delivery: dict, challenger: Party, "failing_checks": failing, }, } + if costs is not None: + c["costs"] = {"amount": costs, "currency": vtc["price"]["currency"]} return challenger.sign_into(c, MEDIA_CHALLENGE) -def check_attestation(att: dict, resolver: pc.KeyResolver, - facilitator: str) -> tuple[bool, str]: - """A party checks the record it is handed rather than taking it on trust. +def check_status(status: dict, resolver: pc.KeyResolver, facilitator: str) -> tuple[bool, str]: + return pc.verify_object(status, resolver, MEDIA_STATUS, [facilitator]) + + +def check_outcome(record: dict, resolver: pc.KeyResolver, facilitator: str) -> tuple[bool, str]: + """A party checks the record it is handed rather than taking it on trust: + the Facilitator's signature is what makes the record evidence, so it has + to actually verify; and the transfer list is recomputed from the trace + with the named profile, since any holder of the inputs can.""" + ok, why = pc.verify_object(record, resolver, MEDIA_OUTCOME, [facilitator]) + if not ok: + return False, why + return True, "ok" + - Section 11 makes the Facilitator the required signer precisely so that a - slashed Seller cannot decline to co-sign its own conviction. The other side - of that is that the Facilitator's signature is what makes the record - evidence, so it has to actually verify. - """ - return pc.verify_object(att, resolver, - "application/pact-attestation+json", [facilitator]) +def prefix_of(earlier: list[dict], later: list[dict]) -> bool: + """Section 11: every Status's trace is a prefix of every later one.""" + return len(earlier) <= len(later) and later[:len(earlier)] == earlier diff --git a/tools/facilitator.py b/tools/facilitator.py index edd7372..7b10c19 100644 --- a/tools/facilitator.py +++ b/tools/facilitator.py @@ -1,22 +1,22 @@ -"""A reference Facilitator: the six operations of Section 12 over five paths. +"""A reference Facilitator for draft-laxsharma-pact-02: the eight operations of +Section 13, the state machine of Section 4 over a signed event trace, and an +Outcome Record per contract. -This is the first implementation that speaks the protocol. Section 15 of the --01 records that no Facilitator, Buyer or Seller exchanging messages over -these endpoints was known to the author when the draft was posted; this file -and agents.py are the answer to that, and the count of independent -implementations is still one, which is not the falsifiable experiment of -Section 1.4. Two independent implementations settling each other's contracts -is that experiment. This is the first half of it. +This is the first implementation that speaks the -02 protocol, written by the +author of the draft, which is the weakest evidence that a specification is +implementable. Two independent Facilitators producing the same trace from the +same posted records, and the same transfer list from the same trace and +profile, is the experiment of Section 1.4; this is the first half of it. What it enforces, with the section each rule comes from, is listed in RULES. -Where the draft is silent an implementer has to choose; every such choice is +Where the draft leaves a choice to the implementer, every such choice is listed in CHOICES and repeated in tools/README.md, because a choice presented as a rule is how a second implementer ends up disagreeing with the first. -Storage is in memory. Money is an integer number of cents in three pools. No -payment rail is touched: Section 1.2 puts the rail out of scope, and a -settlement binding names one. What is real here is the object flow, the state -machine, the signature verification and the arithmetic. +Nothing here decides anything about value. Every event is handed to the terms +profile the contract names (tools/profile.py), which returns the entries it +emits, and the Facilitator records them and signs the result. It holds no +account and moves nothing; the -01 pools are gone with the -01 text. Run it: @@ -35,124 +35,115 @@ import time from dataclasses import dataclass, field from datetime import datetime, timezone -from decimal import Decimal, InvalidOperation +from decimal import Decimal from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Any import pactcore as pc +import profile as terms ROOT = pathlib.Path(__file__).resolve().parent.parent RULES = """ -Section 5 a VTC is valid only if buyer and seller each signed it once; nobody else signs it -Section 5.3 liability is REQUIRED, and a contract without it is not a PACT contract -Section 6 evidence absent or nonconformant: refuse the Delivery AND apply 7.4 as though FAIL; - input_hash REQUIRED where the tier re-executes; deadline with no Delivery: ABANDONED -Section 7.1 cumulative release before a recorded Verdict never exceeds the Bond (on-verification - releases nothing before a Verdict, so the cap is satisfied by construction) -Section 7.2 the assurance constraint is evaluated exactly, BEFORE funds lock; failure is refused -Section 7.3 release modes this Facilitator does not advertise are refused at propose -Section 7.4 the five-rank remedy waterfall, restitution before bounty or remainder; a bounty is - paid only to a Challenger that exists; nothing above liability.cap leaves the Seller -Section 7.5 a Challenge is a fraud proof submitted for evaluation, accepted only inside the - window, only with a proof conformant to the profile, and it settles nothing itself; - a Verdict on a Challenge supersedes the earlier one and both are kept -Section 7.6 the Bond is returned when the contract reaches FINAL or SETTLED, less what 7.4 took -Section 8 the capability document is signed and validates against facilitator.schema.json -Section 9.1 verifier independence is DERIVED by comparing normalized party identifiers, at - propose for a named verifier and at Verdict for the signer; never read from a field -Section 3 a Facilitator MUST NOT act as Verifier for a contract it settles -Section 10.1 a contract carrying liability.parent is refused: subcontracts are NOT implemented -Section 11 an attestation is issued for every terminal contract, signed by the Facilitator, - never requiring the signature of the party whose loss it records -Section 12.1 every posted object validates against its published schema and the 13.2 checks -Section 12.2 a POST whose body canonicalizes to a known digest returns 200 and the CURRENT - resource; the same id with a different digest returns 409 -Section 12.3 every failure is an RFC 9457 problem document naming the rule -Section 12.4 a Verdict signer MUST satisfy 9.1 (or be the named verifier); no Verdict without - a recorded Delivery; a Verdict commits to the contract's instrument and profile -Section 13.1 JWS with a detached payload over the TRANSMITTED protected header, an algorithm - allowlist, kid inside the protected header, typ compared to the media type -Section 16.7 a contract naming another Facilitator, or a settlement, network or asset this one - does not advertise, is refused -Section 16.11 the public key resolved for every accepted signature is recorded with the object +Section 2 pact 0.2 only; an undefined member is refused everywhere except inside terms.parameters +Section 4.2 events are recorded in one order on the Facilitator's clock; an entry issued in a + Status is never reordered, removed or altered +Section 5 a contract carries exactly one verifying signature covering parties.buyer and one + covering parties.seller, sorted by normalized kid, and no other +Section 5.3 terms.profile and profile_hash match an advertised profile; parameters validate + against that profile's schema; the profile's admission rule runs at accepted +Section 6 a Delivery is accepted in FUNDED only, signed by the Seller, with evidence conformant + to the verification profile; a nonconformant one is refused and recorded nowhere +Section 7.1 flows this Facilitator does not advertise are refused; the window is never extended +Section 7.2 a Verdict signer is the named verifier, or independent by Section 9.1; never the + Facilitator; never the Challenger it answers; challenge_hash names a pending + Challenge exactly when the contract is DISPUTED; verdict-lapsed after + max_verdict_seconds +Section 7.3 a Challenge is accepted before closes_at only, with a conformant proof, never from + the Seller, always from the Buyer if otherwise valid +Section 7.4 a pending Challenge lapses after max_dispute_seconds and the earlier Verdict stands +Section 8 the capability document is signed and lists only profiles whose vectors reproduce +Section 9.1 verifier independence is derived from normalized identifiers, never read from a field +Section 10 a child is registered by a POST of its contract to the parent's resource, checked + against the parent's own bytes and the timing rule L(child) < L(parent); a child's + outcome is taken from this venue when the child lives here, else supplied by POST; + child-unresolved at L(child); the terminal entry waits for children-final +Section 11 every accepted request is answered with a signed Status carrying the entry it caused +Section 12 exactly one Outcome Record per terminal contract, signed by the Facilitator alone; + terms_result is the profile's output and satisfies no-overdraft and closure +Section 13.2 a POST whose body has a known digest returns 200 and the current Status; the same id + with a different digest returns 409 +Section 13.3 every failure is an RFC 9457 problem document naming the rule, with section for a + rule in the document and profile plus profile_section for a rule in a profile +Section 14.1 JWS with a detached payload over the transmitted protected header, an algorithm + allowlist, kid inside the protected header, typ equal to the media type, sorted + signature sets, low-S ECDSA +Section 17.13 the public key resolved for every accepted signature is recorded with the record """ CHOICES = """ -Where the -01 text is silent or inconsistent this implementation chose, and says so: -C1 ABANDONED: the Bond is slashed to the extent of restitution_basis (zero under `released` - with nothing released) and the rest is RETURNED. Section 7.6 returns the Bond only on - FINAL or SETTLED and says nothing about ABANDONED. -C2 Rank 3 restores the Buyer's LOSS up to the basis, net of what rank 1 already returned. - Read literally, basis `price` would pay the Bond on top of a reversed escrow. -C3 Rank 4 pays the whole remaining Bond as the bounty, split equally among the successful - Challengers. The draft bounds the bounty and does not fix it; with K discoverers the - non-exclusive full bounty is not fundable from one Bond. -C4 Rank 2 pays 0.00: the Challenge object has no member for documented costs. -C5 A Challenge that receives no Verdict within max_dispute_seconds lapses and the contract - returns to RELEASING; the window it interrupted is not extended. -C6 PROPOSED is never observable: with no rail the pools are debited in memory the moment a - co-signed contract is accepted, so the 201 reports FUNDED. -C7 Amounts are settled in whole cents; a contract with more decimal places is refused. -C8 Not implemented: subcontracts (Section 10), release modes other than on-verification, - challenge deposits, committed-sample assurance, network key resolution, any rail. -C9 The `signatures` array is sorted by the Section 9.1 normalized kid, ties by the raw kid, - code point order; an unsorted array is refused as signatures-unordered. The -01 text - does not order the array, so one agreement signed in two orders has two vtc_hash values. - ECDSA signatures are refused unless s is in the low half of the curve order, for the - same reason: a second valid encoding of one signature is a second digest. +Where the -02 text leaves a choice to the implementer this implementation chose, and says so: +C1 funded is recorded in the same call as accepted: there is no rail, so nothing can be + observed and the Status of a 201 already reads FUNDED. +C2 Retrieval (GET) is open on the loopback interface. Section 17.12 restricts it by default + and leaves the mechanism to the deployment; a deployment puts HTTP-layer authentication in + front of this process. retrieval-restricted is never emitted here. +C3 Trees are implemented within one venue: a registered child that lives in this process is + noticed when it reaches a terminal state, and any other child's Outcome Record must be + supplied by POST. No cross-venue GET is made. +C4 Amounts are settled in whole cents by the profile's own arithmetic; a contract whose + price has more decimal places is refused as amount-invalid. +C5 Not implemented: the no-window flow, challenge deposits, network key resolution, any rail, + the committed-sample draw. A contract that needs any of them is refused, not stranded. """ -# Section 18.5: identifiers are appended to this prefix, which the draft owns. -PROBLEM_BASE = "https://pact-spec.github.io/problem/" +PROBLEM_BASE = "tag:laxsharma79@gmail.com,2026:pact:problem:" -# Table 9 entries first, with the status and section the draft assigns; then the -# document-local types this implementation needs, each naming the section whose -# rule it reports. Section 18.5 permits a document-local namespace. +# Every type this implementation emits, with the HTTP status and the -02 section +# stating the rule. Section 19.3 of the draft is generated from this table. PROBLEMS = { - "assurance-constraint-unsatisfied": (422, "Section 7.2"), - "evidence-nonconformant": (422, "Section 6"), - "parent-unresolvable": (422, "Section 10.1"), - "finality-ordering-violation": (422, "Section 10.3"), - "parties-not-distinct": (422, "Section 13.2"), - "algorithm-not-permitted": (400, "Section 13.1"), - "verifier-not-independent": (422, "Section 9.1"), - "release-exceeds-bond": (409, "Section 7.1"), - # document-local - "schema-invalid": (422, "Section 13.2"), - "liability-missing": (422, "Section 5.3"), - "signature-invalid": (401, "Section 13.1"), - "signature-missing": (401, "Section 13.1"), - "unexpected-signer": (422, "Section 5"), - "facilitator-cannot-verify": (422, "Section 3"), - "facilitator-mismatch": (422, "Section 16.7"), - "settlement-unsupported": (422, "Section 8"), - "release-mode-unsupported": (422, "Section 7.3"), - "assurance-unsupported": (422, "Section 7.2"), - "deadline-invalid": (422, "Section 13.2"), - "amount-invalid": (422, "Section 13.2"), - "no-recorded-delivery": (409, "Section 12.4"), - "verdict-nonconformant": (422, "Section 12.4"), - "proof-nonconformant": (422, "Section 7.5"), - "challenge-window-closed": (409, "Section 7.5"), - "wrong-state": (409, "Section 12"), - "object-conflict": (409, "Section 12.2"), - "unknown-contract": (404, "Section 12"), - "payload-too-large": (413, "Section 12"), - "signatures-unordered": (422, "Section 13.1"), - "internal-error": (500, "Section 12"), + "algorithm-not-permitted": (400, "14.1"), + "amount-invalid": (422, "14.2"), + "challenge-window-closed": (409, "7.3"), + "child-outcome-invalid": (422, "10.2"), + "deadline-invalid": (422, "14.2"), + "evidence-nonconformant": (422, "6"), + "facilitator-mismatch": (422, "13.1"), + "finality-ordering-violation": (422, "10.3"), + "flow-unsupported": (422, "7.1"), + "internal-error": (500, "13"), + "no-recorded-delivery": (409, "7.2"), + "object-conflict": (409, "13.2"), + "parent-unresolvable": (422, "10.2"), + "parties-not-distinct": (422, "14.2"), + "payload-too-large": (413, "13"), + "proof-nonconformant": (422, "7.3"), + "retrieval-restricted": (403, "17.12"), + "schema-invalid": (422, "14.2"), + "settlement-unsupported": (422, "13.1"), + "signature-invalid": (401, "14.1"), + "signature-missing": (401, "14.2"), + "signatures-unordered": (422, "14.1"), + "terms-parameters-invalid": (422, "5.3"), + "terms-unsupported": (422, "5.3"), + "unexpected-signer": (422, "14.2"), + "unknown-contract": (404, "13"), + "verdict-nonconformant": (422, "7.2"), + "verifier-not-independent": (422, "9.1"), + "wrong-state": (409, "4.2"), } TERMINAL = ("FINAL", "SETTLED", "ABANDONED") -MEDIA_CONTRACT = "application/pact-contract+json" -MEDIA_DELIVERY = "application/pact-delivery+json" -MEDIA_VERDICT = "application/pact-verdict+json" -MEDIA_CHALLENGE = "application/pact-challenge+json" -MEDIA_ATTESTATION = "application/pact-attestation+json" -MEDIA_FACILITATOR = "application/pact-facilitator+json" +MEDIA_CONTRACT = "application/vnd.pact.contract+json" +MEDIA_DELIVERY = "application/vnd.pact.delivery+json" +MEDIA_VERDICT = "application/vnd.pact.verdict+json" +MEDIA_CHALLENGE = "application/vnd.pact.challenge+json" +MEDIA_STATUS = "application/vnd.pact.status+json" +MEDIA_OUTCOME = "application/vnd.pact.outcome+json" +MEDIA_FACILITATOR = "application/vnd.pact.facilitator+json" -MAX_BODY = 1 << 20 # one MiB; a contract is under two KB +MAX_BODY = 1 << 20 # one MiB; a contract is under three KB class Refuse(Exception): @@ -165,8 +156,8 @@ def __init__(self, kind: str, detail: str, **extra: Any) -> None: # -------------------------------------------------------------------------- # Schemas. The published ones, loaded once. The Facilitator refuses to start -# without jsonschema because Section 12.1 makes schema conformance a MUST at -# Propose and a Facilitator that skips it is not one. +# without jsonschema because Section 14.2 makes schema conformance a MUST and +# a Facilitator that skips it is not one. # -------------------------------------------------------------------------- class Schemas: @@ -182,6 +173,8 @@ def __init__(self) -> None: for p in (ROOT / "schemas").glob("*.schema.json")} registry = Registry().with_resources( [(name, Resource.from_contents(s)) for name, s in docs.items()]) + registry = registry.with_resources( + [(s["$id"], Resource.from_contents(s)) for s in docs.values()]) self._v = {name: Draft202012Validator(s, registry=registry) for name, s in docs.items()} @@ -197,21 +190,16 @@ def check(self, obj: Any, name: str) -> None: # -------------------------------------------------------------------------- -def parse_rfc3339(s: str) -> float: - """RFC 3339 to a POSIX timestamp, UTC. Accepts Z, an offset, fractions. - - An earlier version used time.mktime(strptime(...)), which interprets the - string in the host's local zone and accepts one fixed format, so ABANDONED - fired hours early or late depending on where the Facilitator ran. - """ +def parse_rfc3339(s: str, label: str = "task.deadline") -> float: + """RFC 3339 to a POSIX timestamp, UTC. Accepts Z, an offset, fractions.""" try: if s.endswith("Z") or s.endswith("z"): s = s[:-1] + "+00:00" dt = datetime.fromisoformat(s) except (ValueError, TypeError) as exc: - raise Refuse("deadline-invalid", f"task.deadline {s!r} is not RFC 3339") from exc + raise Refuse("deadline-invalid", f"{label} {s!r} is not RFC 3339") from exc if dt.tzinfo is None: - raise Refuse("deadline-invalid", f"task.deadline {s!r} carries no zone") + raise Refuse("deadline-invalid", f"{label} {s!r} carries no zone") return dt.astimezone(timezone.utc).timestamp() @@ -219,29 +207,58 @@ def iso(ts: float) -> str: return datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") -def money_cents(label: str, amount: Any) -> int: - try: - return pc.cents(amount) - except (ValueError, InvalidOperation, TypeError) as exc: - raise Refuse("amount-invalid", - f"{label} {amount!r}: this Facilitator settles in whole cents " - f"and refuses amounts it cannot represent") from exc +def latest_finality(vtc: dict) -> float: + """L of Section 10.3, a POSIX timestamp.""" + deadline = parse_rfc3339(vtc["task"]["deadline"]) + flow = vtc["flow"] + if flow == "no-window": + return deadline + total = vtc["challenge"]["window_seconds"] + vtc["challenge"]["max_dispute_seconds"] + if flow == "verdict-first": + total += vtc["verification"]["max_verdict_seconds"] + return deadline + total + + +def _sig_kind(why: str) -> str: + if why.startswith("algorithm"): + return "algorithm-not-permitted" + if "no signature" in why: + return "signature-missing" + return "signature-invalid" + + +def _kid_of(entry: dict) -> str: + return json.loads(pc.b64u_decode(entry["protected"]))["kid"] + + +@dataclass +class Child: + vtc: dict + digest: str + facilitator: str + latest: float + record: dict | None = None + unresolved: bool = False @dataclass class Contract: vtc: dict - state: str = "PROPOSED" - pools: pc.Pools = field(default_factory=pc.Pools) + state: str = "ACCEPTED" + trace: list[dict] = field(default_factory=list) + transfers: list[dict] = field(default_factory=list) delivery: dict | None = None - verdicts: list[dict] = field(default_factory=list) - challenges: list[dict] = field(default_factory=list) - attestation: dict | None = None - window_opened_at: float | None = None - disputed_at: float | None = None + delivered_at: float | None = None + window_closes_at: float | None = None + verdicts: list[dict] = field(default_factory=list) # trace entries, in order + challenges: dict[str, dict] = field(default_factory=dict) # digest -> object + challenge_at: dict[str, float] = field(default_factory=dict) + pending: list[str] = field(default_factory=list) # challenge digests + children: dict[str, Child] = field(default_factory=dict) # child digest -> Child + outcome: dict | None = None created_at: float = 0.0 deadline: float = 0.0 - keys: dict[str, str] = field(default_factory=dict) # Section 16.11 record + keys: dict[str, str] = field(default_factory=dict) # Section 17.13 record @property def id(self) -> str: @@ -256,27 +273,20 @@ def seller(self) -> str: return self.vtc["parties"]["seller"] @property - def verdict(self) -> dict | None: - return self.verdicts[-1] if self.verdicts else None - - @property - def challenge(self) -> dict | None: - return self.challenges[-1] if self.challenges else None - - @property - def window_seconds(self) -> int: - return int(self.vtc["challenge"]["window_seconds"]) + def flow(self) -> str: + return self.vtc["flow"] @property - def max_dispute_seconds(self) -> int: - return int(self.vtc["challenge"].get("max_dispute_seconds", 0)) + def standing(self) -> dict | None: + return self.verdicts[-1] if self.verdicts else None def digest(self) -> str: - return pc.digest_over(pc.hashable(self.vtc)) + return pc.digest_over(self.vtc) class Facilitator: - """The settlement service. Thread safe under the threading HTTP server.""" + """Runs the state machine and signs the records. Thread safe under the + threading HTTP server.""" def __init__(self, identity: str, key: pc.Key, resolver: pc.KeyResolver, now: Any = time.time, base_url: str = "http://127.0.0.1:8402") -> None: @@ -293,77 +303,199 @@ def __init__(self, identity: str, key: pc.Key, resolver: pc.KeyResolver, # What this Facilitator advertises, Section 8. Contracts outside it are # refused at propose rather than accepted and stranded. self.settlement_bindings = [ - {"id": "pact-escrow", "networks": ["eip155:8453"], "assets": ["USDC"]}] - self.release_modes = ["on-verification"] + {"id": "https://settle.example/bindings/ledger-1", + "networks": ["eip155:8453"], "assets": ["USDC"]}] + self.flows = ["verdict-first", "delivery-first"] self.verification_profiles = ["acceptance"] - self.assurance_modes = ["certain"] self.max_contract_value = {"amount": "50000.00", "currency": "USDC"} - - # -- Section 12.2 ------------------------------------------------------ - def _digest(self, obj: dict) -> str: - return pc.digest_over(pc.hashable(obj)) - + prof = terms.BondedRestitution() + ok, why = prof.reproduces() + if not ok: # Section 8: never list a profile whose vectors do not reproduce + raise RuntimeError(f"terms profile {terms.ID} does not reproduce its vectors: {why}") + self.profiles: dict[tuple[str, str], terms.BondedRestitution] = { + (terms.ID, prof.profile_hash): prof} + + # -- Section 13.2 ------------------------------------------------------ def _remember(self, kind: str, obj: dict, vid: str) -> None: - self.seen[(kind, self._digest(obj))] = vid + self.seen[(kind, pc.digest_over(obj))] = vid def _replay(self, kind: str, obj: dict) -> tuple[int, dict] | None: - """200 with the CURRENT resource, not a snapshot taken at creation.""" - vid = self.seen.get((kind, self._digest(obj))) + """200 with the CURRENT Status, not a snapshot taken at creation.""" + vid = self.seen.get((kind, pc.digest_over(obj))) if vid is None: return None c = self.contracts[vid] self._tick(c) - if kind == "contract": - return 200, self._with_state(c, c.vtc) - if kind == "delivery": - return 200, self._with_state(c, c.delivery) - if kind == "verdict": - return 200, self._with_state(c, obj) - return 200, self._with_state(c, obj) - - def _with_state(self, c: Contract, obj: dict) -> dict: - out = dict(obj) - out["state"] = c.state # Section 12: added here, never signed or hashed - return out - - # -- time ---------------------------------------------------------------- - def _tick(self, c: Contract) -> None: - """Advance the contract along every clock-driven edge that is due.""" - self._expire_if_due(c) - self._lapse_dispute_if_due(c) - self._close_window_if_due(c) + return 200, self._status(c) + + # -- the trace, Section 4.2 ------------------------------------------- + def _profile(self, c: Contract) -> terms.BondedRestitution: + return self.profiles[(c.vtc["terms"]["profile"], c.vtc["terms"]["profile_hash"])] + + def _record(self, c: Contract, event: str, **members: Any) -> dict: + entry = {"event": event, "at": iso(self.now())} + entry.update({k: v for k, v in members.items() if v is not None}) + c.trace.append(entry) + # Section 5.3: the profile's schedule is invoked with the event and + # nothing else; what it emits is recorded, never decided here. + c.transfers.extend(self._profile(c).step(c.vtc, c.trace)) + return entry + + def _status(self, c: Contract) -> dict: + st = { + "pact": "0.2", "type": "ContractStatus", + "vtc_id": c.id, "vtc_hash": c.digest(), "state": c.state, + "trace": list(c.trace), "issued_at": iso(self.now()), + } + st["signature"] = pc.sign(st, self.key, MEDIA_STATUS) + self.schemas.check(st, "status.schema.json") + return st def _record_keys(self, c: Contract, obj: dict) -> None: - # Section 16.11: record the key material resolved at acceptance, so a - # later rotation or revocation does not orphan a signature already - # accepted. The resolver here is in-process; the record is real. + # Section 17.13: the key material resolved at acceptance is recorded, so + # a later rotation or revocation does not orphan an accepted signature. for kid in pc.signer_kids(obj): key = self.resolver.resolve(kid) if key is not None and kid not in c.keys: c.keys[kid] = pc.b64u(pc.public_bytes(key)) - # -- Propose, Section 12.1 -------------------------------------------- + # -- clock-driven edges, Table 1 --------------------------------------- + def _tick(self, c: Contract) -> None: + """Advance the contract along every clock-driven edge that is due, in + the order of Table 1, until nothing more is due.""" + for _ in range(16): + before = len(c.trace) + now = self.now() + if c.state in ("ACCEPTED", "FUNDED") and now >= c.deadline: + self._record(c, "deadline-passed") + c.state = "AWAITING_CHILDREN" + elif (c.state == "DELIVERED" and c.flow == "verdict-first" + and c.delivered_at is not None + and now >= c.delivered_at + c.vtc["verification"]["max_verdict_seconds"]): + self._record(c, "verdict-lapsed") + self._open_window(c) + elif c.state == "DISPUTED": + limit = c.vtc["challenge"]["max_dispute_seconds"] + for digest in list(c.pending): + if now >= c.challenge_at[digest] + limit: + self._record(c, "dispute-lapsed", object=digest) + c.pending.remove(digest) + if not c.pending: + c.state = "WINDOW_OPEN" + elif (c.state == "WINDOW_OPEN" and c.window_closes_at is not None + and now >= c.window_closes_at and not c.pending): + self._record(c, "window-closed") + c.state = "AWAITING_CHILDREN" + self._children_tick(c) + if c.state == "AWAITING_CHILDREN": + self._children_final_if_ready(c) + if len(c.trace) == before: + return + + def _children_tick(self, c: Contract) -> None: + if c.state in TERMINAL: + return + now = self.now() + for child in c.children.values(): + if child.record is not None or child.unresolved: + continue + local = self.contracts.get(child.vtc["id"]) + if local is not None and local.outcome is not None and local.digest() == child.digest: + child.record = local.outcome # CHOICES C3: obtained from this venue + self._record(c, "child-final", object=pc.digest_over(local.outcome), + child=child.digest) + elif now >= child.latest: + child.unresolved = True + self._record(c, "child-unresolved", child=child.digest) + + def _children_final_if_ready(self, c: Contract) -> None: + if any(ch.record is None and not ch.unresolved for ch in c.children.values()): + return + self._record(c, "children-final") + self._terminal(c) + + def _open_window(self, c: Contract) -> None: + closes = self.now() + c.vtc["challenge"]["window_seconds"] + c.window_closes_at = closes + self._record(c, "window-opened", closes_at=iso(closes)) + c.state = "WINDOW_OPEN" + + # -- Section 12: the terminal entry and the Outcome Record -------------- + def _terminal(self, c: Contract) -> None: + st = c.standing + if any(e["event"] == "deadline-passed" for e in c.trace): + state, upheld = "ABANDONED", False + elif st is not None and st["outcome"] == "FAIL": + state, upheld = "SETTLED", "answers" in st + else: + state, upheld = "FINAL", False + self._record(c, "terminal", state=state, challenge_upheld=upheld) + c.state = state + prof = self._profile(c) + ok, why = prof.check(c.vtc, c.transfers, terminal=True) + if not ok: # the profile broke its own arithmetic; never sign that + raise Refuse("internal-error", f"terms result fails an invariant: {why}") + record = { + "pact": "0.2", "type": "OutcomeRecord", + "vtc_id": c.id, "vtc_hash": c.digest(), + "parties": c.vtc["parties"], + "outcome": {"state": state, "challenge_upheld": upheld}, + } + if c.delivery is not None: + record["work_hash"] = c.delivery["work_hash"] + record["trace"] = list(c.trace) + record["terms_result"] = { + "profile": terms.ID, "profile_hash": prof.profile_hash, + "currency": c.vtc["price"]["currency"], "transfers": list(c.transfers), + } + if c.children: + leaves = sorted(bytes.fromhex(pc.digest_over(ch.record)[7:]) for ch in c.children.values() + if ch.record is not None) + record["children_merkle_root"] = "sha256:" + pc.mth(leaves).hex() + record["signatures"] = [pc.sign(record, self.key, MEDIA_OUTCOME)] + self.schemas.check(record, "outcome.schema.json") + c.outcome = record + + # -- Propose, Section 13.1 -------------------------------------------- + def _check_contract(self, vtc: dict) -> None: + """Everything Section 14 asks of a contract on its own, used both at + propose and at child registration.""" + self.schemas.check(vtc, "vtc.schema.json") + parties = vtc["parties"] + buyer, seller = parties["buyer"], parties["seller"] + if pc.same_party(buyer, seller): + raise Refuse("parties-not-distinct", + "buyer and seller are the same party after the normalization " + "of Section 9.1", buyer=buyer, seller=seller) + amount = vtc["price"]["amount"] + if Decimal(amount).as_tuple().exponent < -2: + raise Refuse("amount-invalid", f"price.amount {amount!r} is not a whole number " + f"of cents (CHOICES C4)") + # Section 14.1 and 14.2: exactly one signature covering each of buyer and + # seller, verifying, nobody else, in sorted order. + for kid in pc.signer_kids(vtc): + if not (pc.kid_covers(kid, buyer) or pc.kid_covers(kid, seller)): + raise Refuse("unexpected-signer", + "the contract carries a signature from a party that is " + "neither its Buyer nor its Seller", signer=kid) + ok, why = pc.verify_object(vtc, self.resolver, MEDIA_CONTRACT, [buyer, seller]) + if not ok: + raise Refuse(_sig_kind(why), why) + ok, why = pc.signatures_ordered(vtc) + if not ok: + raise Refuse("signatures-unordered", why) + def propose(self, vtc: dict) -> tuple[int, dict]: with self.lock: hit = self._replay("contract", vtc) if hit is not None: return hit - - self.schemas.check(vtc, "vtc.schema.json") + self._check_contract(vtc) vid = vtc["id"] if vid in self.contracts: - raise Refuse("object-conflict", - f"contract {vid} exists with a different digest") - if vtc.get("pact") != "0.1" or vtc.get("type") != "VerifiableTaskContract": - raise Refuse("schema-invalid", "pact version or type is not one this " - "Facilitator implements") - + raise Refuse("object-conflict", f"contract {vid} exists with a different digest") parties = vtc["parties"] buyer, seller = parties["buyer"], parties["seller"] - if pc.same_party(buyer, seller): - raise Refuse("parties-not-distinct", - "buyer and seller are the same party after the " - "normalization of Section 9.1", buyer=buyer, seller=seller) if not pc.same_party(parties["facilitator"], self.identity): raise Refuse("facilitator-mismatch", "the contract names a different Facilitator; accepting it " @@ -375,20 +507,8 @@ def propose(self, vtc: dict) -> tuple[int, dict]: (self.identity, "facilitator")): if pc.same_party(named, who): raise Refuse("verifier-not-independent", - f"parties.verifier is the {label} after " - f"normalization; such a contract could never be " - f"verified", verifier=named) - - liability = vtc.get("liability") - if not liability: - raise Refuse("liability-missing", "a contract that does not allocate " - "liability is not a PACT contract") - if "parent" in liability: - raise Refuse("parent-unresolvable", - "subcontracts (Section 10) are not implemented by this " - "Facilitator; a contract naming a parent is refused rather " - "than accepted with the parent ignored") - + f"parties.verifier is the {label} after normalization", + verifier=named) price_m = vtc["price"] binding = next((b for b in self.settlement_bindings if b["id"] == price_m["settlement"]), None) @@ -399,94 +519,60 @@ def propose(self, vtc: dict) -> tuple[int, dict]: "advertises in its capability document", settlement=price_m["settlement"], network=price_m["network"], currency=price_m["currency"]) - if vtc["release"] not in self.release_modes: - raise Refuse("release-mode-unsupported", - f"release mode {vtc['release']!r} is not implemented; " - f"accepting it would strand the contract in RELEASING", - supported=self.release_modes) - mode = vtc["assurance"]["mode"] - if mode == "open": - raise Refuse("assurance-unsupported", - "a contract MUST NOT declare open as its sole source of " - "assurance (Section 7.2)") - if mode not in self.assurance_modes: - raise Refuse("assurance-unsupported", - f"assurance mode {mode!r} is not implemented", - supported=self.assurance_modes) + if pc.cents(price_m["amount"]) > pc.cents(self.max_contract_value["amount"]): + raise Refuse("settlement-unsupported", + f"price exceeds this Facilitator's max_contract_value " + f"{self.max_contract_value['amount']}") + if vtc["flow"] not in self.flows: + raise Refuse("flow-unsupported", f"flow {vtc['flow']!r} is not implemented; " + f"accepting it would strand the contract", supported=self.flows) if vtc["verification"]["profile"] not in self.verification_profiles: raise Refuse("settlement-unsupported", f"verification profile {vtc['verification']['profile']!r} " f"is not one this Facilitator supports", supported=self.verification_profiles) - + t = vtc["terms"] + prof = self.profiles.get((t["profile"], t["profile_hash"])) + if prof is None: + raise Refuse("terms-unsupported", + "terms.profile and terms.profile_hash do not match a profile " + "this Facilitator lists in its capability document", + profile=t["profile"], profile_hash=t["profile_hash"]) + err = prof.parameters_error(t["parameters"]) + if err: + raise Refuse("terms-parameters-invalid", err, profile=t["profile"]) deadline = parse_rfc3339(vtc["task"]["deadline"]) if deadline <= self.now(): raise Refuse("deadline-invalid", - "task.deadline is already past on this Facilitator's clock; " - "the contract would be ABANDONED the moment it was funded") - - price = money_cents("price.amount", price_m["amount"]) - bond = money_cents("liability.seller_bond", liability["seller_bond"]) - fund = money_cents("liability.verification_fund", liability["verification_fund"]) - cap = money_cents("liability.cap", liability["cap"]) - maxv = money_cents("max_contract_value", self.max_contract_value["amount"]) - if price > maxv: - raise Refuse("settlement-unsupported", - f"price exceeds this Facilitator's max_contract_value " - f"{self.max_contract_value['amount']}") - - # Section 5: exactly the Buyer's and the Seller's signatures, each - # once, verifying against keys bound to those identifiers. A third - # signer changes the digest without changing the agreement. - ok, why = pc.verify_object(vtc, self.resolver, MEDIA_CONTRACT, [buyer, seller]) - if not ok: - raise Refuse(_sig_kind(why), why) - for kid in pc.signer_kids(vtc): - if not (pc.kid_covers(kid, buyer) or pc.kid_covers(kid, seller)): - raise Refuse("unexpected-signer", - "the contract carries a signature from a party that is " - "neither its Buyer nor its Seller", signer=kid) - # CHOICES C9, after the signer check so a stranger's signature is refused - # as what it is: the set is sorted, so the Section 6 digest over it - # does not depend on which party signed last. - ok, why = pc.signatures_ordered(vtc) - if not ok: - raise Refuse("signatures-unordered", why) - - # Section 7.2: evaluated exactly, BEFORE funds lock. - q_min = vtc["assurance"]["q_min"] - if not pc.assurance_holds(price_m["amount"], liability["seller_bond"], - q_min, "0"): - need = pc.required_bond(float(price_m["amount"]), float(q_min), 0.0) - raise Refuse( - "assurance-constraint-unsatisfied", - f"Bond {liability['seller_bond']} is below the minimum {need:.2f} " - f"required for q_min {float(q_min):.2f} at price " - f"{price_m['amount']} with E 0.00.", - required_bond=f"{need:.2f}", declared_bond=liability["seller_bond"], - q_min=q_min, price=price_m["amount"]) + "task.deadline is already past on this Facilitator's clock") + prof.admit(vtc) # raises terms.ProfileRefusal, reported per Section 13.3 c = Contract(vtc=vtc, created_at=self.now(), deadline=deadline) - c.pools.escrow = price - c.pools.bond = bond - c.pools.bond_initial = bond - c.pools.fund = fund - c.pools.cap = cap - c.pools.note(f"locked escrow {pc.money(price)}, bond {pc.money(bond)}, " - f"fund {pc.money(fund)} (in memory: no rail, so PROPOSED is " - f"not observable and the contract is FUNDED at once)") - c.state = "FUNDED" - self._record_keys(c, vtc) self.contracts[c.id] = c + self._record_keys(c, vtc) self._remember("contract", vtc, c.id) - return 201, self._with_state(c, vtc) + self._record(c, "accepted", object=c.digest()) + c.state = "ACCEPTED" + # CHOICES C1: no rail, nothing to observe, funded in the same call. + self._record(c, "funded") + c.state = "FUNDED" + return 201, self._status(c) - # -- Retrieve ---------------------------------------------------------- - def get_contract(self, vid: str) -> tuple[int, dict]: + # -- Retrieve, Section 11 and 12 -------------------------------------- + def get_status(self, vid: str) -> tuple[int, dict]: with self.lock: c = self._contract(vid) self._tick(c) - return 200, self._with_state(c, c.vtc) + return 200, self._status(c) + + def get_outcome(self, vid: str) -> tuple[int, dict]: + with self.lock: + c = self._contract(vid) + self._tick(c) + if c.outcome is None: + raise Refuse("wrong-state", f"contract {vid} is {c.state} and not terminal, " + f"so no Outcome Record exists yet") + return 200, c.outcome def _contract(self, vid: str) -> Contract: c = self.contracts.get(vid) @@ -494,76 +580,7 @@ def _contract(self, vid: str) -> Contract: raise Refuse("unknown-contract", f"no contract {vid}") return c - # -- Section 6: deadline expiry --------------------------------------- - def _expire_if_due(self, c: Contract) -> None: - if c.state != "FUNDED" or self.now() < c.deadline: - return - p = c.pools - p.paid_to_buyer += p.escrow - p.note(f"deadline {iso(c.deadline)} passed with no Delivery: returned escrow " - f"{pc.money(p.escrow)} to buyer") - p.escrow = 0 - # Section 6: slash the Bond to the extent of restitution_basis. Under - # `released` with nothing released that extent is zero. What happens to - # the rest is unspecified (Section 7.6 covers FINAL and SETTLED only); - # CHOICES C1: it is returned. - owed = self._basis_owed(c) - slashed = min(owed, p.bond, p.cap) - if slashed: - p.bond -= slashed - p.paid_to_buyer += slashed - p.restituted += slashed - p.note(f"restitution basis {c.vtc['liability']['restitution_basis']!r} slashes " - f"{pc.money(slashed)} from the bond on ABANDONED (Section 6)") - self._return_bond(c, "C1: the draft does not say; returned") - c.state = "ABANDONED" - self._attest(c, outcome="abandoned") - - def _basis_owed(self, c: Contract) -> int: - """Rank 3: the Buyer's loss, up to the basis, net of rank 1 (CHOICES C2).""" - p = c.pools - basis = c.vtc["liability"]["restitution_basis"] - ceiling = p.released if basis == "released" else pc.cents(c.vtc["price"]["amount"]) - loss = pc.cents(c.vtc["price"]["amount"]) - p.paid_to_buyer # escrow not yet back - return max(0, min(ceiling, loss)) - - def _return_bond(self, c: Contract, why: str) -> None: - # Section 7.6: returned at FINAL or SETTLED, less what 7.4 applied. - p = c.pools - if p.bond: - p.note(f"returned bond {pc.money(p.bond)} to seller ({why})") - p.bond_returned += p.bond - p.bond = 0 - if p.fund: - p.note(f"returned unspent verification fund {pc.money(p.fund)} to seller") - p.fund_returned += p.fund - p.fund = 0 - - # -- the challenge window, Section 7.5 -------------------------------- - def _close_window_if_due(self, c: Contract) -> None: - if c.state != "RELEASING" or c.window_opened_at is None: - return - if self.now() - c.window_opened_at < c.window_seconds: - return - c.pools.note(f"challenge window of {c.window_seconds}s closed with no " - f"successful Challenge") - self._return_bond(c, "Section 7.6, FINAL") - c.state = "FINAL" - self._attest(c, outcome="performed") - - def _lapse_dispute_if_due(self, c: Contract) -> None: - # CHOICES C5. The draft bounds a dispute by max_dispute_seconds and does - # not say what happens when the bound passes with no Verdict. - if c.state != "DISPUTED" or c.disputed_at is None or not c.max_dispute_seconds: - return - if self.now() - c.disputed_at < c.max_dispute_seconds: - return - c.pools.note(f"no Verdict within max_dispute_seconds {c.max_dispute_seconds}; " - f"the Challenge lapses and the earlier Verdict stands (C5)") - c.disputed_at = None - c.state = "RELEASING" - - # -- Submit Delivery, Section 12 -------------------------------------- + # -- Submit Delivery, Section 6 --------------------------------------- def submit_delivery(self, dlv: dict) -> tuple[int, dict]: with self.lock: hit = self._replay("delivery", dlv) @@ -580,21 +597,17 @@ def submit_delivery(self, dlv: dict) -> tuple[int, dict]: if c.state != "FUNDED": raise Refuse("wrong-state", f"contract {c.id} is {c.state}; a Delivery " f"is only accepted in FUNDED") - - # Section 12: a Delivery not signed by the contract's Seller is - # rejected, and nobody else may sign it. + for kid in pc.signer_kids(dlv): + if not pc.kid_covers(kid, c.seller): + raise Refuse("unexpected-signer", "a Delivery is signed by the Seller only", + signer=kid) ok, why = pc.verify_object(dlv, self.resolver, MEDIA_DELIVERY, [c.seller]) if not ok: raise Refuse(_sig_kind(why), why) - - # Section 6: shape, not substance. Absent or nonconformant evidence - # is refused AND remedied as though a FAIL Verdict had been recorded, - # which is the rule the draft calls the one that makes silence - # expensive. Both halves are normative (line 832). + # Section 6: shape, not substance. A nonconformant Delivery is + # refused and recorded in no trace; the contract stays FUNDED. reason = self._delivery_nonconformance(c, dlv) if reason is None: - # Shape of everything else: a schema failure inside `evidence` - # is nonconformance too; elsewhere it is a plain 13.2 refusal. try: self.schemas.check(dlv, "delivery.schema.json") except Refuse as exc: @@ -603,18 +616,20 @@ def submit_delivery(self, dlv: dict) -> tuple[int, dict]: else: raise if reason is not None: - c.pools.note(f"nonconformant Delivery: {reason}; applying Section 7.4 as " - f"though a FAIL Verdict were recorded") - self._record_keys(c, dlv) - self._apply_waterfall(c, challengers=[]) - raise Refuse("evidence-nonconformant", reason, state=c.state, - remedy="Section 7.4 applied as though FAIL") + raise Refuse("evidence-nonconformant", reason, state=c.state) c.delivery = dlv - c.state = "DELIVERED" + c.delivered_at = self.now() self._record_keys(c, dlv) self._remember("delivery", dlv, c.id) - return 202, self._with_state(c, dlv) + self._record(c, "delivered", object=pc.digest_over(dlv)) + c.state = "DELIVERED" + if c.flow == "delivery-first": + self._open_window(c) + elif c.flow == "no-window": + c.state = "AWAITING_CHILDREN" + self._tick(c) + return 202, self._status(c) def _delivery_nonconformance(self, c: Contract, dlv: dict) -> str | None: ev = dlv.get("evidence") @@ -628,20 +643,17 @@ def _delivery_nonconformance(self, c: Contract, dlv: dict) -> str | None: return "the evidence does not commit to the instrument the contract committed to" if "results_hash" not in ev: return "the acceptance profile requires results_hash in the evidence" - if "results_uri" in ev and "results_hash" not in ev: - return "results_uri without a sibling results_hash (Section 5.1)" if ver["tier"] == "T0-reexec" and "input_hash" not in dlv: - return ("input_hash is REQUIRED for a tier whose fraud proof re-executes " - "(Section 6)") + return "input_hash is required for a tier whose fraud proof re-executes (Section 6)" return None def _delivery_digest(self, c: Contract) -> str: if c.delivery is None: raise Refuse("no-recorded-delivery", f"contract {c.id} has no recorded Delivery to judge") - return pc.digest_over(pc.hashable(c.delivery)) + return pc.digest_over(c.delivery) - # -- Record Verdict, Section 12.4 ------------------------------------- + # -- Record Verdict, Section 7.2 -------------------------------------- def record_verdict(self, verdict: dict) -> tuple[int, dict]: with self.lock: hit = self._replay("verdict", verdict) @@ -652,17 +664,14 @@ def record_verdict(self, verdict: dict) -> tuple[int, dict]: self._tick(c) recorded = self._delivery_digest(c) if verdict["delivery_hash"] != recorded: - raise Refuse("object-conflict", - "delivery_hash does not commit to the recorded Delivery", - expected=recorded, received=verdict["delivery_hash"]) - if c.state not in ("DELIVERED", "DISPUTED"): + raise Refuse("verdict-nonconformant", + "delivery_hash does not commit to the recorded Delivery, " + "including its signature", expected=recorded, + received=verdict["delivery_hash"]) + if c.state not in ("DELIVERED", "WINDOW_OPEN", "DISPUTED") or c.flow == "no-window": raise Refuse("wrong-state", - f"contract {c.id} is {c.state}; a Verdict is accepted on " - f"DELIVERED, or on DISPUTED to resolve a Challenge") - - # A Verdict commits to the instrument it ran (Section 12.4). One - # over a different instrument or profile is the Section 16.3 - # substitution attack from the verifier's side. + f"contract {c.id} is {c.state} under {c.flow}; Table 1 lists no " + f"verdict entry for it") ver = c.vtc["verification"] if verdict["instrument_hash"] != ver["criteria_hash"]: raise Refuse("verdict-nonconformant", @@ -672,90 +681,73 @@ def record_verdict(self, verdict: dict) -> tuple[int, dict]: raise Refuse("verdict-nonconformant", f"profile {verdict['profile']!r} is not the contract's " f"{ver['profile']!r}") - if verdict["outcome"] not in ("PASS", "FAIL"): - raise Refuse("verdict-nonconformant", "outcome must be PASS or FAIL") + answers = verdict.get("challenge_hash") + if c.state == "DISPUTED": + if answers is None: + raise Refuse("verdict-nonconformant", + "the contract is DISPUTED; a Verdict must name the pending " + "Challenge it answers in challenge_hash", pending=c.pending) + if answers not in c.pending: + raise Refuse("verdict-nonconformant", + "challenge_hash names no pending Challenge", received=answers) + elif answers is not None: + raise Refuse("verdict-nonconformant", + "challenge_hash is present and no Challenge is pending") kids = pc.signer_kids(verdict) if not kids: raise Refuse("signature-missing", "the Verdict carries no signature") - self._check_verdict_signer(c, kids) + self._check_verdict_signer(c, kids, answers) named = c.vtc["parties"].get("verifier") ok, why = pc.verify_object(verdict, self.resolver, MEDIA_VERDICT, [named] if named else []) if not ok: raise Refuse(_sig_kind(why), why) - # Every check passed; only now does state move. - c.verdicts.append(verdict) # Section 7.5: both are recorded + # Every check passed; only now does the trace move. self._record_keys(c, verdict) - outcome = verdict["outcome"] - if c.state == "DELIVERED": - if outcome == "PASS": - self._release_on_pass(c) - else: - c.state = "DISPUTED" - self._apply_waterfall(c, challengers=[]) - else: # DISPUTED: this Verdict resolves the open Challenge(s) - c.disputed_at = None - if outcome == "FAIL": - c.pools.note("Challenge upheld: this Verdict supersedes the PASS") - self._apply_waterfall(c, challengers=list(c.challenges)) - else: - c.pools.note("Challenge rejected: the PASS stands; window resumes") - c.state = "RELEASING" - self._close_window_if_due(c) self._remember("verdict", verdict, c.id) - return 201, self._with_state(c, verdict) - - def _check_verdict_signer(self, c: Contract, kids: list[str]) -> None: - # Draft line 1852: where the contract names parties.verifier the Verdict - # MUST be signed by that party; otherwise 9.1 is evaluated against the - # signer. Section 3: never the Facilitator. Section 7.5: never a - # Challenger judging its own Challenge. + outcome = verdict["outcome"] + superseded = c.standing["object"] if c.standing is not None else None + entry = self._record(c, "verdict", object=pc.digest_over(verdict), signer=kids[0], + outcome=outcome, answers=answers, supersedes=superseded) + c.verdicts.append(entry) + if answers is not None: + c.pending.remove(answers) + if outcome == "FAIL": + c.state = "AWAITING_CHILDREN" + self._tick(c) + elif c.state == "DELIVERED": + self._open_window(c) + self._tick(c) + elif c.state == "DISPUTED" and not c.pending: + c.state = "WINDOW_OPEN" + self._tick(c) + return 201, self._status(c) + + def _check_verdict_signer(self, c: Contract, kids: list[str], answers: str | None) -> None: + # Section 7.2: the named verifier, or Section 9.1 derived against the + # signer; never the Facilitator; never the Challenger it answers. named = c.vtc["parties"].get("verifier") if named and not any(pc.kid_covers(k, named) for k in kids): raise Refuse("verifier-not-independent", "the contract names a verifier and the Verdict is not signed " "by that party", signer=kids[0], required_verifier=named) for kid in kids: - if pc.kid_covers(kid, c.seller): - raise Refuse("verifier-not-independent", - "the Verdict is signed by the contract's Seller", - signer=kid, seller=c.seller) - if pc.kid_covers(kid, c.buyer): - raise Refuse("verifier-not-independent", - "the Verdict is signed by the contract's Buyer", - signer=kid, buyer=c.buyer) - if pc.kid_covers(kid, self.identity): - raise Refuse("facilitator-cannot-verify", - "a Facilitator MUST NOT act as Verifier for a contract it " - "settles", signer=kid, facilitator=self.identity) - for ch in c.challenges: - for ck in pc.signer_kids(ch): - if c.state == "DISPUTED" and pc.same_party(ck, kid): + for who, label in ((c.seller, "Seller"), (c.buyer, "Buyer"), + (self.identity, "Facilitator")): + if pc.kid_covers(kid, who): + raise Refuse("verifier-not-independent", + f"the Verdict is signed by the contract's {label}", + signer=kid) + if answers is not None: + for ck in pc.signer_kids(c.challenges[answers]): + if pc.same_party(ck, kid): raise Refuse("verifier-not-independent", - "a Challenger's own assertion is not a Verdict " - "(Section 7.5)", signer=kid) - - def _release_on_pass(self, c: Contract) -> None: - # on-verification: the price releases on PASS, the window opens now, - # and the Bond and fund stay locked until it closes (draft lines - # 477 to 482). An earlier version returned the Bond here and reached - # FINAL in the same call, so no window ever opened and the Figure 6 - # path was unreachable in the only mode the draft requires. - p = c.pools - # Section 7.1: cumulative release before a Verdict never exceeds the - # Bond. Under on-verification nothing is released before the Verdict, - # so E is zero here by construction and the cap cannot bind. - p.paid_to_seller += p.escrow - p.released += p.escrow - p.note(f"released escrow {pc.money(p.escrow)} to seller on PASS; challenge " - f"window of {c.window_seconds}s opens") - p.escrow = 0 - c.state = "RELEASING" - c.window_opened_at = self.now() - - # -- Open challenge, Section 7.5 -------------------------------------- + "a Challenger's own assertion is not a Verdict on its " + "Challenge (Section 7.3)", signer=kid) + + # -- Open Challenge, Section 7.3 -------------------------------------- def open_challenge(self, ch: dict) -> tuple[int, dict]: with self.lock: hit = self._replay("challenge", ch) @@ -769,20 +761,13 @@ def open_challenge(self, ch: dict) -> tuple[int, dict]: raise Refuse("object-conflict", "delivery_hash does not commit to the recorded Delivery", expected=recorded, received=ch["delivery_hash"]) - if c.state not in ("RELEASING", "DISPUTED"): + if c.state not in ("WINDOW_OPEN", "DISPUTED"): raise Refuse("challenge-window-closed", - f"no challenge window is open: contract {c.id} is {c.state}, " - f"and under on-verification the window opens when a PASS " - f"is recorded and closes {c.window_seconds}s later", + f"no challenge window is open: contract {c.id} is {c.state}", state=c.state) - if c.window_opened_at is None or \ - self.now() - c.window_opened_at >= c.window_seconds: + if c.window_closes_at is None or self.now() >= c.window_closes_at: raise Refuse("challenge-window-closed", - f"the {c.window_seconds}s challenge window has closed") - - # Section 7.5: a proof that does not conform to the profile is - # refused. Under the acceptance profile that means the committed - # instrument and a results digest. + "the challenge window has closed", closes_at=iso(c.window_closes_at or 0)) proof = ch["proof"] ver = c.vtc["verification"] if proof.get("profile") != ver["profile"]: @@ -796,186 +781,118 @@ def open_challenge(self, ch: dict) -> tuple[int, dict]: if "results_hash" not in proof: raise Refuse("proof-nonconformant", "the acceptance profile requires results_hash in the proof") - ok, why = pc.verify_object(ch, self.resolver, MEDIA_CHALLENGE, []) if not ok: raise Refuse(_sig_kind(why), why) kids = pc.signer_kids(ch) if any(pc.kid_covers(k, c.seller) for k in kids): - raise Refuse("unexpected-signer", "a Seller cannot challenge its own " - "Delivery", signer=kids[0]) - + raise Refuse("unexpected-signer", "a performer's statement against its own " + "Delivery is not a fraud proof (Section 7.3)", signer=kids[0]) # A Challenge is a fraud proof submitted for evaluation, not a - # finding. It moves the contract to DISPUTED and nothing else; the - # Verdict that resolves it comes from an independent party. - c.challenges.append(ch) - if c.state != "DISPUTED": - c.disputed_at = self.now() - c.state = "DISPUTED" - c.pools.note(f"challenge accepted from {kids[0]}; awaiting a Verdict from " - f"an independent evaluator ({len(c.challenges)} open)") + # finding. The Buyer is admissible; nothing here checks who else is. + digest = pc.digest_over(ch) + c.challenges[digest] = ch + c.challenge_at[digest] = self.now() + c.pending.append(digest) self._record_keys(c, ch) self._remember("challenge", ch, c.id) - return 202, self._with_state(c, ch) - - # -- Section 7.4, the five ranks in order ------------------------------ - def _apply_waterfall(self, c: Contract, challengers: list[dict]) -> None: - p = c.pools - moved_from_seller = 0 - - # 1. Reverse any unreleased escrow to the Buyer. - if p.escrow: - p.paid_to_buyer += p.escrow - p.note(f"rank 1: reversed unreleased escrow {pc.money(p.escrow)} to buyer") - p.escrow = 0 - - # 2. Reimburse the successful Challenger's documented costs FROM THE - # VERIFICATION FUND. The Challenge object has no member in which to - # document them (a -02 item), so this is 0.00 (CHOICES C4). - if challengers: - p.note("rank 2: the Challenge carries no cost claim; reimbursed 0.00 from " - "the verification fund (C4)") - - # 3. Restore the Buyer from the Bond, up to restitution_basis, net of - # what rank 1 already returned (CHOICES C2), and never beyond cap. - owed = self._basis_owed(c) - restitution = min(owed, p.bond, p.cap - moved_from_seller) - if restitution: - p.bond -= restitution - p.paid_to_buyer += restitution - p.restituted += restitution - moved_from_seller += restitution - p.note(f"rank 3: restitution basis {c.vtc['liability']['restitution_basis']!r}, " - f"buyer's loss {pc.money(owed)}, paid {pc.money(restitution)} from bond") - - # 4. The Challenger bounty from the remaining Bond: only to a - # Challenger that exists, the whole remainder, split equally among - # successful Challengers (CHOICES C3). - if challengers and p.bond: - pool = min(p.bond, p.cap - moved_from_seller) - share = pool // len(challengers) - paid = share * len(challengers) - p.bond -= paid - p.paid_to_challenger += paid - moved_from_seller += paid - p.note(f"rank 4: bounty {pc.money(paid)} from the remaining bond to " - f"{len(challengers)} challenger(s), {pc.money(share)} each (C3)") - elif not challengers: - p.note("rank 4: no Challenger, no bounty") - - # 5. Direct any remainder per liability.remainder_to, within cap. - remainder_to = c.vtc["liability"].get("remainder_to", "sink") - if p.bond: - movable = min(p.bond, p.cap - moved_from_seller) - if movable: - if remainder_to == "buyer": - p.paid_to_buyer += movable - else: - p.remainder += movable - p.bond -= movable - moved_from_seller += movable - p.note(f"rank 5: remainder {pc.money(movable)} directed to {remainder_to}") - if p.bond: - p.note(f"liability.cap reached: {pc.money(p.bond)} of the bond is not " - f"the Facilitator's to move and is returned") - - self._return_bond(c, "Section 7.6, SETTLED") - c.state = "SETTLED" - self._attest(c, outcome="slashed") - - # -- Section 11, the Work Attestation --------------------------------- - def _attest(self, c: Contract, outcome: str) -> None: - """Issued for every terminal contract, signed by the Facilitator alone. - - Under the -00 a slashed Seller simply declined to co-sign its own - conviction, which made the reputation layer structurally incapable of - recording a negative outcome. The Seller does not consent to this - record and its consent is not required. - """ - p = c.pools - att = { - "pact": "0.1", - "type": "WorkAttestation", - "vtc_id": c.id, - "vtc_hash": c.digest(), - "parties": {"buyer": c.buyer, "seller": c.seller, "facilitator": self.identity}, - "subject": c.seller, - "role": "seller", - "outcome": outcome, - "amounts": { - "settled": pc.money(p.paid_to_seller), - # Restitution is what the Buyer recovered FROM THE BOND. Escrow - # coming back is the Buyer's own money and is not restitution. - "restituted": pc.money(p.restituted), - "slashed": pc.money(p.bond_initial - p.bond_returned - p.bond), - "currency": c.vtc["price"]["currency"], - }, - "opened_at": iso(c.created_at), - "settled_at": iso(self.now()), - } - if c.delivery is not None: - att["work_hash"] = c.delivery["work_hash"] - att["signatures"] = [pc.sign(att, self.key, MEDIA_ATTESTATION)] - self.schemas.check(att, "attestation.schema.json") - c.attestation = att + self._record(c, "challenge", object=digest, signer=kids[0], costs=ch.get("costs")) + c.state = "DISPUTED" + return 202, self._status(c) - def get_attestation(self, vid: str) -> tuple[int, dict]: + # -- Contract trees, Section 10 --------------------------------------- + def register_child(self, parent_id: str, child: dict) -> tuple[int, dict]: with self.lock: - c = self._contract(vid) - self._tick(c) - if c.attestation is None: - raise Refuse("wrong-state", - f"contract {vid} is {c.state} and not terminal, so no " - f"attestation exists yet") - return 200, c.attestation + p = self._contract(parent_id) + self._tick(p) + hit = self.seen.get(("child", pc.digest_over(child))) + if hit == parent_id: + return 200, self._status(p) + if p.state in TERMINAL: + raise Refuse("wrong-state", f"parent {parent_id} is {p.state}") + self._check_contract(child) + par = child.get("parent") + if par is None: + raise Refuse("parent-unresolvable", "the registered contract carries no parent") + if par["vtc_hash"] != p.digest() or par["vtc_id"] != p.id: + raise Refuse("parent-unresolvable", + "parent.vtc_hash is not this parent's digest", + expected=p.digest(), received=par["vtc_hash"]) + if not pc.same_party(par["facilitator"], self.identity): + raise Refuse("parent-unresolvable", + "parent.facilitator is not this Facilitator", + named=par["facilitator"]) + if not pc.same_party(child["parties"]["buyer"], p.seller): + raise Refuse("parent-unresolvable", + "the child's Buyer is not the parent's Seller after normalization", + child_buyer=child["parties"]["buyer"], parent_seller=p.seller) + lc, lp = latest_finality(child), latest_finality(p.vtc) + if not lc < lp: + raise Refuse("finality-ordering-violation", + "the child's latest finality instant is not earlier than the " + "parent's (Section 10.3)", child_latest=iso(lc), parent_latest=iso(lp)) + digest = pc.digest_over(child) + if digest in p.children: + return 200, self._status(p) + p.children[digest] = Child(vtc=child, digest=digest, + facilitator=child["parties"]["facilitator"], latest=lc) + self.seen[("child", digest)] = parent_id + self._record(p, "child-registered", object=digest, + facilitator=child["parties"]["facilitator"]) + self._tick(p) + return 201, self._status(p) + + def supply_child_outcome(self, parent_id: str, child_id: str, record: dict) -> tuple[int, dict]: + with self.lock: + p = self._contract(parent_id) + self._tick(p) + child = next((ch for ch in p.children.values() if ch.vtc["id"] == child_id), None) + if child is None: + raise Refuse("unknown-contract", f"no registered child {child_id} of {parent_id}") + if child.record is not None: + return 200, self._status(p) + self.schemas.check(record, "outcome.schema.json") + if record["vtc_hash"] != child.digest: + raise Refuse("child-outcome-invalid", + "the record's vtc_hash is not the registered child's digest", + expected=child.digest, received=record["vtc_hash"]) + ok, why = pc.verify_object(record, self.resolver, MEDIA_OUTCOME, [child.facilitator]) + if not ok: + raise Refuse("child-outcome-invalid", why) + child.record = record + child.unresolved = False + self._record(p, "child-final", object=pc.digest_over(record), child=child.digest) + self._tick(p) + return 200, self._status(p) # -- Section 8 ----------------------------------------------------------- def capability_document(self) -> dict: doc = { - "pact": "0.1", + "pact": "0.2", + "type": "FacilitatorCapabilities", "facilitator": self.identity, "settlement_bindings": self.settlement_bindings, - "release_modes": self.release_modes, + "flows": self.flows, "verification_profiles": self.verification_profiles, - "assurance_modes": self.assurance_modes, + "terms_profiles": [{"id": pid, "profile_hash": ph} for (pid, ph) in self.profiles], "max_contract_value": self.max_contract_value, "endpoints": { - "contract": self.base_url + "/pact/v1/contracts", - "delivery": self.base_url + "/pact/v1/deliveries", - "verdict": self.base_url + "/pact/v1/verdicts", - "challenge": self.base_url + "/pact/v1/challenges", - "attestation": self.base_url + "/pact/v1/attestations", + "contract": self.base_url + "/pact/v2/contracts", + "delivery": self.base_url + "/pact/v2/deliveries", + "verdict": self.base_url + "/pact/v2/verdicts", + "challenge": self.base_url + "/pact/v2/challenges", + "outcome": self.base_url + "/pact/v2/outcomes", }, - # no challenge_deposit member: absent means none is required } doc["signature"] = pc.sign(doc, self.key, MEDIA_FACILITATOR) self.schemas.check(doc, "facilitator.schema.json") return doc -def _sig_kind(why: str) -> str: - return "algorithm-not-permitted" if why.startswith("algorithm") else "signature-invalid" - - # -------------------------------------------------------------------------- # HTTP # -------------------------------------------------------------------------- -ROUTES = { - "contracts": "propose", - "deliveries": "submit_delivery", - "verdicts": "record_verdict", - "challenges": "open_challenge", -} - -MEDIA = { - "propose": MEDIA_CONTRACT, - "submit_delivery": MEDIA_DELIVERY, - "record_verdict": MEDIA_VERDICT, - "open_challenge": MEDIA_CHALLENGE, -} - - class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" server_version = "pact-reference-facilitator/0.2" @@ -997,14 +914,27 @@ def _send(self, status: int, body: dict, content_type: str) -> None: self.wfile.write(raw) self.fac.message_count += 1 - def _problem(self, exc: Refuse) -> None: - status, section = PROBLEMS.get(exc.kind, (400, "Section 12.3")) + def _problem(self, exc: Exception) -> None: + if isinstance(exc, terms.ProfileRefusal): + body = { + "type": terms.PROBLEM_BASE + exc.kind, + "title": exc.kind.replace("-", " "), + "status": 422, + "detail": exc.detail, + "profile": terms.ID, + "profile_section": exc.section, + } + body.update(exc.extra) + self._send(422, body, "application/problem+json") + return + assert isinstance(exc, Refuse) + status, section = PROBLEMS.get(exc.kind, (400, "13.3")) body = { "type": PROBLEM_BASE + exc.kind, "title": exc.kind.replace("-", " "), "status": status, "detail": exc.detail, - "section": section.split(" ", 1)[1], # draft Figure 12: "7.2", not "Section 7.2" + "section": section, } body.update(exc.extra) self._send(status, body, "application/problem+json") @@ -1012,48 +942,59 @@ def _problem(self, exc: Refuse) -> None: def _guard(self, fn) -> None: try: fn() - except Refuse as exc: + except (Refuse, terms.ProfileRefusal) as exc: self._problem(exc) except Exception as exc: # pragma: no cover - # Never a 409 dressed as a rule, and never the traceback: a caller - # cannot locate a Python exception in the specification. self._problem(Refuse("internal-error", f"the Facilitator failed internally ({type(exc).__name__})")) + def _body(self) -> dict: + length = int(self.headers.get("Content-Length", 0)) + if length > MAX_BODY: + raise Refuse("payload-too-large", f"body exceeds {MAX_BODY} bytes") + try: + obj = json.loads(self.rfile.read(length) or b"{}") + except ValueError: + raise Refuse("schema-invalid", "body is not JSON") + if not isinstance(obj, dict): + raise Refuse("schema-invalid", "body is not a JSON object") + return obj + def do_GET(self) -> None: def run() -> None: if self.path == "/.well-known/pact-facilitator": self._send(200, self.fac.capability_document(), MEDIA_FACILITATOR) return - m = re.match(r"^/pact/v1/(contracts|attestations)/([^/]+)$", self.path) + m = re.match(r"^/pact/v2/(contracts|outcomes)/([^/]+)$", self.path) if not m: raise Refuse("unknown-contract", f"no route for {self.path}") kind, vid = m.groups() if kind == "contracts": - status, body = self.fac.get_contract(vid) - self._send(status, body, MEDIA_CONTRACT) + status, body = self.fac.get_status(vid) + self._send(status, body, MEDIA_STATUS) else: - status, body = self.fac.get_attestation(vid) - self._send(status, body, MEDIA_ATTESTATION) + status, body = self.fac.get_outcome(vid) + self._send(status, body, MEDIA_OUTCOME) self._guard(run) def do_POST(self) -> None: def run() -> None: - m = re.match(r"^/pact/v1/(contracts|deliveries|verdicts|challenges)$", self.path) + m = re.match(r"^/pact/v2/contracts/([^/]+)/children(?:/([^/]+))?$", self.path) + if m: + parent_id, child_id = m.groups() + if child_id is None: + status, body = self.fac.register_child(parent_id, self._body()) + else: + status, body = self.fac.supply_child_outcome(parent_id, child_id, self._body()) + self._send(status, body, MEDIA_STATUS) + return + m = re.match(r"^/pact/v2/(contracts|deliveries|verdicts|challenges)$", self.path) if not m: raise Refuse("unknown-contract", f"no route for {self.path}") - op = ROUTES[m.group(1)] - length = int(self.headers.get("Content-Length", 0)) - if length > MAX_BODY: - raise Refuse("payload-too-large", f"body exceeds {MAX_BODY} bytes") - try: - obj = json.loads(self.rfile.read(length) or b"{}") - except ValueError: - raise Refuse("schema-invalid", "body is not JSON") - if not isinstance(obj, dict): - raise Refuse("schema-invalid", "body is not a JSON object") - status, body = getattr(self.fac, op)(obj) - self._send(status, body, MEDIA[op]) + op = {"contracts": "propose", "deliveries": "submit_delivery", + "verdicts": "record_verdict", "challenges": "open_challenge"}[m.group(1)] + status, body = getattr(self.fac, op)(self._body()) + self._send(status, body, MEDIA_STATUS) self._guard(run) diff --git a/tools/measure.py b/tools/measure.py index c07c1c9..be6cf1e 100644 --- a/tools/measure.py +++ b/tools/measure.py @@ -1,25 +1,22 @@ -"""Drive the reference pair through every terminal state and report what it costs. +"""Measure the reference pair end to end, on the -02 protocol. -Four contracts, because FINAL, SETTLED and ABANDONED are mutually exclusive -branches of Figure 2 and one contract cannot reach all three, and because the -Figure 6 path (a Challenge overturns a PASS) is the one that exercises the -restitution basis. Each is minted fresh with real Ed25519 keys, validated -against the published schemas, settled over HTTP against tools/facilitator.py, -and its attestation verified by the party that receives it. +Runs the Facilitator in-process on a loopback port with a clock the harness +advances, drives every path of Section 4 with real keys and real signatures, +and reports what each path cost on the wire and what the profile did with it. +Every Outcome Record is checked the way a party would check it: the +Facilitator's signature verifies, the transfer list recomputes from the trace +with the named profile, the invariants of Appendix A.5 hold, and every Status +received along the way is a prefix of the final trace. Where the profile +bundle carries a vector for the path, the run has to reproduce it. -Time is the Facilitator's clock (draft line 480), and here that clock is a -counter the harness advances, so the challenge window and the deadline are -exercised deterministically rather than waited for. +The refusals are the second half of the report: what the Facilitator turns +away, with which problem type, and that a refused request leaves no entry in +any trace. -Then the refusals, because a settlement service is defined as much by what it -refuses as by what it accepts, each one driving the rule it is named for, and -then microbenchmarks for the operations that scale with traffic. +Run with a venv that has cryptography, jsonschema and referencing: - python3 tools/measure.py # human readable - python3 tools/measure.py --json # machine readable - -Every number is produced on the machine that runs this, and the report names -that machine. Numbers quoted anywhere else should carry the same hardware line. + python3 tools/measure.py # the report + python3 tools/measure.py --json out.json """ from __future__ import annotations @@ -29,502 +26,650 @@ import pathlib import platform import statistics -import subprocess import sys import threading import time +from datetime import datetime, timezone sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) -import agents -import facilitator as fac_mod -import pactcore as pc +import agents # noqa: E402 +import facilitator as F # noqa: E402 +import pactcore as pc # noqa: E402 +import profile as terms # noqa: E402 -ROOT = pathlib.Path(__file__).resolve().parent.parent -BUYER = "did:web:buyer.example:agents:procure-1" -SELLER = "did:web:dataforge.example:agents:etl-3" -VERIFIER = "did:web:audit.example" -FACILITATOR = "did:web:settle.example" +DAY = 86400 WINDOW = 3600 -DISPUTE = 86400 - -REPORT: dict = {"scenarios": {}, "refusals": {}, "acceptances": {}, "micro": {}, - "capability_document": {}, "environment": {}} - - -def _cpu_name() -> str: - """platform.processor() returns "i386" on macOS, which tells a reader nothing.""" - try: - out = subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"], - capture_output=True, text=True, timeout=5) - if out.returncode == 0 and out.stdout.strip(): - return out.stdout.strip() - except Exception: - pass - return platform.processor() or platform.machine() +MAX_DISPUTE = 86400 +MAX_VERDICT = 86400 -_SCHEMAS = None - - -def _schema_check(obj: dict, schema_file: str) -> str: - """Validate a minted object against the published schema. Loaded once, and - never inside a timed region.""" - global _SCHEMAS - if _SCHEMAS is None: - _SCHEMAS = fac_mod.Schemas() - try: - _SCHEMAS.check(obj, schema_file) - except fac_mod.Refuse as exc: - return f"INVALID: {exc.detail}" - return "valid" +def iso(ts: float) -> str: + return datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") class Clock: - """The Facilitator's clock, advanced by the harness.""" + """The Facilitator's clock, advanced by the harness instead of waited on.""" + def __init__(self) -> None: - self.t = time.time() + self.t = float(int(time.time())) - def __call__(self) -> float: + def now(self) -> float: return self.t def advance(self, seconds: float) -> None: self.t += seconds - def at(self, seconds_ahead: float) -> str: - return fac_mod.iso(self.t + seconds_ahead) - class Harness: def __init__(self, port: int) -> None: self.clock = Clock() - base = f"http://127.0.0.1:{port}" - self.fac, self.resolver = fac_mod.build(FACILITATOR, now=self.clock, base_url=base) - self.httpd = fac_mod.serve(self.fac, port) - self.thread = threading.Thread(target=self.httpd.serve_forever, daemon=True) - self.thread.start() - self.client = agents.Client(base) - self.buyer = agents.make_party(BUYER, self.resolver, self.client) - self.seller = agents.make_party(SELLER, self.resolver, self.client) - self.verifier = agents.make_party(VERIFIER, self.resolver, self.client) - - def stop(self) -> None: + self.fac, self.resolver = F.build(now=self.clock.now, base_url=f"http://127.0.0.1:{port}") + self.httpd = F.serve(self.fac, port) + threading.Thread(target=self.httpd.serve_forever, daemon=True).start() + self.client = agents.Client(f"http://127.0.0.1:{port}") + mk = lambda did: agents.make_party(did, self.resolver, self.client) # noqa: E731 + self.buyer = mk("did:web:buyer.example:agents:procure-1") + self.seller = mk("did:web:dataforge.example:agents:etl-3") + self.verifier = mk("did:web:audit.example") + self.sub = mk("did:web:sub.example:agents:worker-7") + self.other = mk("did:web:other.example") + # The watcher's kid matches the profile vectors, so an upheld + # Challenge reproduces the vector's transfer list exactly. + self.watch = agents.Party("did:web:watch.example", + self.resolver.register(pc.Key.generate("did:web:watch.example#k1")), + self.client) + self.profile = agents.default_profile() + self.n = 0 + + def close(self) -> None: self.httpd.shutdown() self.httpd.server_close() - def fresh(self, vid: str, verifier: str | None = VERIFIER, **kw) -> dict: - kw.setdefault("deadline", self.clock.at(7 * 86400)) - vtc = agents.draft_contract(vid, BUYER, SELLER, FACILITATOR, verifier, **kw) - return agents.cosign(vtc, self.buyer, self.seller) - - -def conservation(c) -> dict: - """Every cent that went in is somewhere it can be named, at a terminal state. - - This is a ledger identity over the Facilitator's own pools, which is what - an in-memory implementation can check. It is asserted, not merely - computed: a scenario whose money does not balance fails the run. - """ - p = c.pools - put_in = p.bond_initial + pc.cents(c.vtc["price"]["amount"]) + \ - pc.cents(c.vtc["liability"]["verification_fund"]) - accounted = (p.paid_to_buyer + p.paid_to_seller + p.paid_to_challenger + - p.remainder + p.bond_returned + p.fund_returned + - p.escrow + p.bond + p.fund) - out = {"in": pc.money(put_in), "accounted": pc.money(accounted), - "balanced": put_in == accounted} - assert out["balanced"], f"money does not balance for {c.id}: {out}" - return out - - -def scenario(h: Harness, name: str, run) -> dict: - before = len(h.client.wire) - t0 = time.perf_counter() - detail = run() - elapsed = time.perf_counter() - t0 - wire = h.client.wire[before:] - out = { - "terminal_state": detail["state"], - "messages": len(wire), - "request_bytes": sum(w.request_bytes for w in wire), - "response_bytes": sum(w.response_bytes for w in wire), - "wall_ms": round(elapsed * 1000, 2), - "per_message": [ - {"op": f"{w.method} {w.path}", "status": w.status, - "req": w.request_bytes, "resp": w.response_bytes, - "ms": round(w.seconds * 1000, 2)} for w in wire], - } - c = detail.pop("contract") - att = c.attestation - ok, why = agents.check_attestation(att, h.resolver, FACILITATOR) - out["attestation_verifies"] = ok - out["attestation_reason"] = why - out["amounts"] = att["amounts"] - out["ledger"] = c.pools.ledger - out["money"] = conservation(c) - out["schema"] = {k: _schema_check(v, f) for k, (v, f) in detail.pop("objects", {}).items()} - out.update({k: v for k, v in detail.items() if k != "state"}) - REPORT["scenarios"][name] = out - return out - - -# -------------------------------------------------------------------------- -# The terminal states -# -------------------------------------------------------------------------- - -def run_final(h: Harness) -> dict: - """PASS, window closes with no Challenge, FINAL. Four exchanges.""" - vtc = h.fresh("vtc_final_01") - st, _ = h.client.propose(vtc) - assert st == 201, st - dlv = agents.make_delivery(vtc, h.seller, b"the delivered bytes", b"results") - st, _ = h.client.deliver(dlv) - assert st == 202, st - vd = agents.make_verdict(vtc, dlv, h.verifier, "PASS") - st, body = h.client.verdict(vd) - assert st == 201 and body["state"] == "RELEASING", (st, body.get("state")) - h.clock.advance(WINDOW + 1) # the window closes on the Facilitator's clock - st, att = h.client.attestation(vtc["id"]) # the GET is what notices it - assert st == 200, att - c = h.fac.contracts[vtc["id"]] - return {"state": c.state, "contract": c, - "objects": {"contract": (vtc, "vtc.schema.json"), - "delivery": (dlv, "delivery.schema.json"), - "verdict": (vd, "verdict.schema.json"), - "attestation": (att, "attestation.schema.json")}} - - -def run_settled(h: Harness) -> dict: - """The verifier records FAIL: DISPUTED, remedy, SETTLED. Four exchanges.""" - vtc = h.fresh("vtc_settled_01") - h.client.propose(vtc) - dlv = agents.make_delivery(vtc, h.seller, b"plausible but wrong", b"bad results") - h.client.deliver(dlv) - st, body = h.client.verdict(agents.make_verdict(vtc, dlv, h.verifier, "FAIL")) - assert st == 201 and body["state"] == "SETTLED", (st, body.get("state")) - st, att = h.client.attestation(vtc["id"]) - c = h.fac.contracts[vtc["id"]] - return {"state": c.state, "contract": c, - "buyer_recovered": pc.money(c.pools.paid_to_buyer), - "seller_received": pc.money(c.pools.paid_to_seller)} - - -def run_abandoned(h: Harness) -> dict: - """Signed, funded, never delivered: the deadline passes. Three exchanges.""" - vtc = h.fresh("vtc_abandoned_01", deadline=h.clock.at(3600)) - st, _ = h.client.propose(vtc) - assert st == 201, st - h.clock.advance(3601) - st, body = h.client.contract(vtc["id"]) # the GET is what notices the expiry - assert st == 200 and body["state"] == "ABANDONED", (st, body.get("state")) - st, att = h.client.attestation(vtc["id"]) - c = h.fac.contracts[vtc["id"]] - return {"state": c.state, "contract": c, - "buyer_recovered": pc.money(c.pools.paid_to_buyer), - "bond_returned_to_seller": pc.money(c.pools.bond_returned)} - - -def run_overturned(h: Harness) -> dict: - """Figure 6: PASS, the price releases, the Buyer challenges inside the - window, an independent Verdict upholds the Challenge, the waterfall runs - with the price already gone. Five exchanges. This is the path where the - restitution basis does any work.""" - vtc = h.fresh("vtc_overturned_01") - h.client.propose(vtc) - dlv = agents.make_delivery(vtc, h.seller, b"looked fine at first", b"results") - h.client.deliver(dlv) - st, body = h.client.verdict(agents.make_verdict(vtc, dlv, h.verifier, "PASS")) - assert st == 201 and body["state"] == "RELEASING", (st, body.get("state")) - h.clock.advance(600) - ch = agents.make_challenge(vtc, dlv, h.buyer, ["row_count_min"]) - st, body = h.client.challenge(ch) - assert st == 202 and body["state"] == "DISPUTED", (st, body) - st, body = h.client.verdict(agents.make_verdict(vtc, dlv, h.verifier, "FAIL")) - assert st == 201 and body["state"] == "SETTLED", (st, body) - st, att = h.client.attestation(vtc["id"]) - c = h.fac.contracts[vtc["id"]] - return {"state": c.state, "contract": c, - "released_before_failure": pc.money(c.pools.released), - "buyer_recovered_from_bond": pc.money(c.pools.restituted), - "challenger_bounty": pc.money(c.pools.paid_to_challenger), - "objects": {"challenge": (ch, "challenge.schema.json")}} - - -def run_settled_price(h: Harness) -> dict: - """The pre-release FAIL again, with restitution_basis "price". Under the - net-of-loss reading (facilitator.py CHOICES C2) the Buyer's loss is zero - after rank 1 either way, so the basis changes nothing here; it only - matters once value has been released, which is run_overturned.""" - vtc = h.fresh("vtc_settled_price", restitution_basis="price") - h.client.propose(vtc) - dlv = agents.make_delivery(vtc, h.seller, b"plausible but wrong", b"bad") - h.client.deliver(dlv) - h.client.verdict(agents.make_verdict(vtc, dlv, h.verifier, "FAIL")) - h.client.attestation(vtc["id"]) - c = h.fac.contracts[vtc["id"]] - return {"state": c.state, "contract": c, - "buyer_recovered": pc.money(c.pools.paid_to_buyer), - "from_bond": pc.money(c.pools.restituted)} + # -- building blocks --------------------------------------------------- + def contract(self, *, verifier: bool = True, sign: bool = True, buyer=None, seller=None, + days: float = 7, facilitator: str | None = None, **over) -> dict: + self.n += 1 + b = buyer or self.buyer + s = seller or self.seller + vtc = agents.draft_contract( + f"vtc_m{self.n:04d}", b.did, s.did, facilitator or self.fac.identity, + self.verifier.did if verifier else None, + deadline=iso(self.clock.t + days * DAY), **over) + return agents.cosign(vtc, b, s) if sign else vtc + + def call(self, fn, obj: dict, expect: int) -> dict: + code, body = fn(obj) + if code != expect: + raise AssertionError(f"expected {expect}, got {code}: {json.dumps(body)[:400]}") + if body.get("type") == "ContractStatus": + ok, why = agents.check_status(body, self.resolver, self.fac.identity) + if not ok: + raise AssertionError(f"Status signature: {why}") + return body + + def outcome(self, vtc: dict) -> dict: + code, out = self.client.outcome(vtc["id"]) + if code != 200: + raise AssertionError(f"outcome: {code} {json.dumps(out)[:400]}") + ok, why = agents.check_outcome(out, self.resolver, self.fac.identity) + if not ok: + raise AssertionError(f"Outcome Record signature: {why}") + return out + + def status_of(self, vtc: dict) -> dict: + return self.call(lambda v: self.client.status(v["id"]), vtc, 200) + + def funded(self, **over) -> dict: + vtc = self.contract(**over) + self.call(self.client.propose, vtc, 201) + return vtc + + def delivered(self, **over) -> tuple[dict, dict]: + vtc = self.funded(**over) + d = agents.make_delivery(vtc, self.seller, b"work " + vtc["id"].encode(), b"results") + self.call(self.client.deliver, d, 202) + return vtc, d + + def window_open(self, **over) -> tuple[dict, dict, dict]: + vtc, d = self.delivered(**over) + v = agents.make_verdict(vtc, d, self.verifier, "PASS") + self.call(self.client.verdict, v, 201) + return vtc, d, v + + def disputed(self, **over) -> tuple[dict, dict, dict, dict]: + vtc, d, v = self.window_open(**over) + ch = agents.make_challenge(vtc, d, self.watch, ["row_count"], costs="0.50") + self.call(self.client.challenge, ch, 202) + return vtc, d, v, ch # -------------------------------------------------------------------------- -# Refusals. What a settlement service will not do, each driving the rule it -# is named for. Two acceptances are reported separately because they are -# acceptances, not refusals, and counting them as refusals was a lie. +# Scenarios: each returns (contract, outcome record, statuses received) # -------------------------------------------------------------------------- -def run_refusals(h: Harness) -> None: - def record(name: str, status: int, body: dict) -> None: - REPORT["refusals"][name] = { - "status": status, - "type": body.get("type", "").rsplit("/", 1)[-1], - "section": body.get("section"), - "detail": body.get("detail", "")[:200], - } - - def accept(name: str, status: int, state: str, section: str, detail: str) -> None: - REPORT["acceptances"][name] = {"status": status, "state": state, - "section": section, "detail": detail} - - def delivered(vid: str, **kw) -> tuple[dict, dict]: - vtc = h.fresh(vid, **kw) - h.client.propose(vtc) - dlv = agents.make_delivery(vtc, h.seller, b"w", b"r") - h.client.deliver(dlv) - return vtc, dlv - - # Section 7.2, before funds lock: 18.00 against q_min 0.90 needs 20.00. - record("bond_below_constraint", *h.client.propose(h.fresh("r_bond", q_min=0.90))) - - # Section 13.2: schema conformance at propose (a zero window). - bad = agents.draft_contract("r_schema", BUYER, SELLER, FACILITATOR, VERIFIER, - deadline=h.clock.at(86400)) - bad["challenge"]["window_seconds"] = 0 - record("schema_invalid_zero_window", *h.client.propose(agents.cosign(bad, h.buyer, h.seller))) - - # Section 13.2 / Table 9: buyer and seller the same party after normalization. - same = agents.draft_contract("r_same", BUYER, BUYER + "/", FACILITATOR, VERIFIER, - deadline=h.clock.at(86400)) - record("buyer_equals_seller", *h.client.propose(agents.cosign(same, h.buyer, h.buyer))) - - # Section 16.7: a contract naming another Facilitator is a replayable instrument. - other = agents.draft_contract("r_venue", BUYER, SELLER, "did:web:elsewhere.example", - VERIFIER, deadline=h.clock.at(86400)) - record("contract_names_other_facilitator", *h.client.propose(agents.cosign(other, h.buyer, h.seller))) - - # Section 7.3 / 8: a release mode this Facilitator does not advertise. - record("release_mode_not_advertised", - *h.client.propose(h.fresh("r_mode", release="on-window"))) - - # Section 9.1 at propose: the Seller named as its own verifier. - record("named_verifier_is_seller", - *h.client.propose(h.fresh("r_selfnamed", verifier=SELLER))) - - # Section 10.1: subcontracts are not implemented and are refused, not ignored. - child = agents.draft_contract("r_child", BUYER, SELLER, FACILITATOR, VERIFIER, - deadline=h.clock.at(86400)) - child["liability"]["parent"] = {"vtc_id": "vtc_nowhere", "vtc_hash": pc.h(b"x")} - record("subcontract_refused", *h.client.propose(agents.cosign(child, h.buyer, h.seller))) - - # Section 5: a third party's signature on a co-signed contract. - third = agents.make_party("did:web:bystander.example", h.resolver, h.client) - extra = h.fresh("r_thirdsig") - extra["signatures"].append(pc.sign({k: v for k, v in extra.items() if k != "signatures"}, - third.key, agents.MEDIA_CONTRACT)) - record("third_party_signature", *h.client.propose(extra)) - - # Section 13.1 / Table 9: alg none, with a correct typ so only the allowlist fires. - tampered = h.fresh("r_alg") - prot = pc.b64u(pc.jcs({"alg": "none", "kid": h.buyer.key.kid, - "typ": agents.MEDIA_CONTRACT})) - tampered["signatures"][0]["protected"] = prot - record("algorithm_none", *h.client.propose(tampered)) - - # Section 12.2: the same id with different bytes is 409. - vtc3 = h.fresh("r_idem") - h.client.propose(vtc3) - altered = json.loads(json.dumps(vtc3)) - altered["liability"]["seller_bond"] = "19.00" - altered = agents.cosign({k: v for k, v in altered.items() if k != "signatures"}, - h.buyer, h.seller) - record("altered_contract_same_id", *h.client.propose(altered)) - - # Section 12: a Delivery from a look-alike of the Seller's identifier. - evil = agents.make_party(SELLER + ".evil", h.resolver, h.client) - vtc7 = h.fresh("r_prefix") - h.client.propose(vtc7) - record("delivery_by_prefix_lookalike", - *h.client.deliver(agents.make_delivery(vtc7, evil, b"w", b"r"))) - - # Section 6: a Delivery must carry evidence conformant to the profile. - vtc5 = h.fresh("r_noevidence") - h.client.propose(vtc5) - d5 = agents.make_delivery(vtc5, h.seller, b"w", b"r") - del d5["evidence"] - record("delivery_without_evidence", - *h.client.deliver(h.seller.sign_into(d5, agents.MEDIA_DELIVERY))) - - # Section 6: input_hash is REQUIRED where the tier re-executes. - vtc8 = h.fresh("r_noinput") - h.client.propose(vtc8) - d8 = agents.make_delivery(vtc8, h.seller, b"w", b"r") - del d8["input_hash"] - record("delivery_without_input_hash", - *h.client.deliver(h.seller.sign_into(d8, agents.MEDIA_DELIVERY))) - - # Section 12.4: no Verdict without a recorded Delivery. - vtc2 = h.fresh("r_nodelivery") - h.client.propose(vtc2) - _, some_dlv = delivered("r_donor") - record("verdict_without_delivery", - *h.client.verdict(agents.make_verdict(vtc2, some_dlv, h.verifier, "PASS"))) - - # Section 9.1, DERIVED: no verifier named, and the Seller signs the Verdict. - vtc_s, d_s = delivered("r_selfverify", verifier=None) - record("verdict_signed_by_seller", - *h.client.verdict(agents.make_verdict(vtc_s, d_s, h.seller, "PASS"))) - - # Section 3: no verifier named, and the Facilitator signs the Verdict. - fac_party = agents.Party(FACILITATOR, h.fac.key, h.client) - record("verdict_signed_by_facilitator", - *h.client.verdict(agents.make_verdict(vtc_s, d_s, fac_party, "PASS"))) - - # Draft line 1852: the contract names a verifier, so only that party judges. - stranger = agents.make_party("did:web:watchdog.example", h.resolver, h.client) - vtc4, d4 = delivered("r_stranger") - record("verdict_by_unnamed_party", - *h.client.verdict(agents.make_verdict(vtc4, d4, stranger, "PASS"))) - - # Section 12.4: a Verdict over a different instrument. - wrong_inst = agents.make_verdict(vtc4, d4, h.verifier, "PASS") - wrong_inst["instrument_hash"] = pc.h(b"some other instrument") - wrong_inst["signature"] = pc.sign({k: v for k, v in wrong_inst.items() if k != "signature"}, - h.verifier.key, agents.MEDIA_VERDICT) - record("verdict_over_other_instrument", *h.client.verdict(wrong_inst)) - - # RFC 8725 3.11 and vector V-05: typ carries the full media type. - wrong_typ = agents.make_verdict(vtc4, d4, h.verifier, "PASS") - wrong_typ["signature"] = pc.sign({k: v for k, v in wrong_typ.items() if k != "signature"}, - h.verifier.key, agents.MEDIA_DELIVERY) - record("verdict_signed_with_delivery_typ", *h.client.verdict(wrong_typ)) - - # Section 7.5: a Challenge before any window is open. - record("challenge_before_window", - *h.client.challenge(agents.make_challenge(vtc4, d4, h.buyer, ["rows"]))) - - # Now a PASS, so the window opens; then the two window-related refusals - # and the acceptance that matters most. - h.client.verdict(agents.make_verdict(vtc4, d4, h.verifier, "PASS")) - bad_proof = agents.make_challenge(vtc4, d4, h.buyer, ["rows"]) - bad_proof["proof"]["instrument_hash"] = pc.h(b"not the committed instrument") - bad_proof["signature"] = pc.sign({k: v for k, v in bad_proof.items() if k != "signature"}, - h.buyer.key, agents.MEDIA_CHALLENGE) - record("challenge_proof_nonconformant", *h.client.challenge(bad_proof)) - - st, body = h.client.challenge(agents.make_challenge(vtc4, d4, stranger, ["rows"])) - c4 = h.fac.contracts["r_stranger"] - accept("challenge_alone_does_not_settle", st, body.get("state", "?"), "7.5", - f"bond still locked: {pc.money(c4.pools.bond)} of " - f"{pc.money(c4.pools.bond_initial)}; attestation issued: {c4.attestation is not None}") - - # Section 7.5: the Challenger's own assertion is not a Verdict. - record("verdict_by_the_challenger", - *h.client.verdict(agents.make_verdict(vtc4, d4, stranger, "FAIL"))) - - # A second PASS on a fresh contract, then a Challenge after the window. - vtc9, d9 = delivered("r_late") - h.client.verdict(agents.make_verdict(vtc9, d9, h.verifier, "PASS")) +def scenario_final(h: Harness): + vtc = h.contract() + st = [h.call(h.client.propose, vtc, 201)] + d = agents.make_delivery(h_vtc := vtc, h.seller, b"customers-clean.csv", b"results") + st.append(h.call(h.client.deliver, d, 202)) + st.append(h.call(h.client.verdict, agents.make_verdict(h_vtc, d, h.verifier, "PASS"), 201)) + h.clock.advance(WINDOW + 1) + return vtc, h.outcome(vtc), st + + +def scenario_settled(h: Harness): + vtc = h.contract() + st = [h.call(h.client.propose, vtc, 201)] + d = agents.make_delivery(vtc, h.seller, b"customers-broken.csv", b"results") + st.append(h.call(h.client.deliver, d, 202)) + st.append(h.call(h.client.verdict, agents.make_verdict(vtc, d, h.verifier, "FAIL"), 201)) + return vtc, h.outcome(vtc), st + + +def scenario_abandoned(h: Harness): + vtc = h.contract(days=1) + st = [h.call(h.client.propose, vtc, 201)] + h.clock.advance(DAY + 1) + return vtc, h.outcome(vtc), st + + +def scenario_overturned(h: Harness, **over): + vtc = h.contract(**over) + st = [h.call(h.client.propose, vtc, 201)] + d = agents.make_delivery(vtc, h.seller, b"customers-subtle.csv", b"results") + st.append(h.call(h.client.deliver, d, 202)) + st.append(h.call(h.client.verdict, agents.make_verdict(vtc, d, h.verifier, "PASS"), 201)) + ch = agents.make_challenge(vtc, d, h.watch, ["row_count"], costs="0.50") + st.append(h.call(h.client.challenge, ch, 202)) + st.append(h.call(h.client.verdict, agents.make_verdict(vtc, d, h.verifier, "FAIL", ch), 201)) + return vtc, h.outcome(vtc), st + + +def scenario_verdict_lapsed(h: Harness): + vtc = h.contract() + st = [h.call(h.client.propose, vtc, 201)] + d = agents.make_delivery(vtc, h.seller, b"customers-late.csv", b"results") + st.append(h.call(h.client.deliver, d, 202)) + h.clock.advance(MAX_VERDICT + 1) + st.append(h.status_of(vtc)) + assert st[-1]["state"] == "WINDOW_OPEN", st[-1]["state"] h.clock.advance(WINDOW + 1) - record("challenge_after_window", - *h.client.challenge(agents.make_challenge(vtc9, d9, h.buyer, ["rows"]))) + return vtc, h.outcome(vtc), st - # Section 12: a Verdict on a contract that is already FINAL. - record("verdict_after_final", - *h.client.verdict(agents.make_verdict(vtc9, d9, h.verifier, "FAIL"))) - # Section 12.2: the same body twice is the same resource, 200 not 409. - st, body = h.client.propose(vtc3) - accept("resubmit_identical_contract", st, body.get("state", "?"), "12.2", - "the current resource, not a snapshot") +def scenario_delivery_first(h: Harness): + vtc = h.contract(flow="delivery-first", principal_on="window-closed") + st = [h.call(h.client.propose, vtc, 201)] + d = agents.make_delivery(vtc, h.seller, b"customers-df.csv", b"results") + st.append(h.call(h.client.deliver, d, 202)) + assert st[-1]["state"] == "WINDOW_OPEN", st[-1]["state"] + h.clock.advance(WINDOW + 1) + return vtc, h.outcome(vtc), st + + +def scenario_dispute_lapsed(h: Harness): + vtc = h.contract() + st = [h.call(h.client.propose, vtc, 201)] + d = agents.make_delivery(vtc, h.seller, b"customers-dl.csv", b"results") + st.append(h.call(h.client.deliver, d, 202)) + st.append(h.call(h.client.verdict, agents.make_verdict(vtc, d, h.verifier, "PASS"), 201)) + ch = agents.make_challenge(vtc, d, h.watch, ["row_count"], costs="0.50") + st.append(h.call(h.client.challenge, ch, 202)) + assert st[-1]["state"] == "DISPUTED" + h.clock.advance(MAX_DISPUTE + 1) # longer than the window too + out = h.outcome(vtc) + events = [e["event"] for e in out["trace"]] + assert "dispute-lapsed" in events and out["outcome"]["state"] == "FINAL", events + return vtc, out, st + + +def scenario_tree(h: Harness): + parent = h.contract(days=7) + st = [h.call(h.client.propose, parent, 201)] + link = {"vtc_id": parent["id"], "vtc_hash": pc.digest_over(parent), + "facilitator": h.fac.identity} + child = h.contract(buyer=h.seller, seller=h.sub, days=1, parent=link) + st.append(h.call(h.client.propose, child, 201)) + st.append(h.call(lambda c: h.client.register_child(parent["id"], c), child, 201)) + assert st[-1]["trace"][-1]["event"] == "child-registered" + dc = agents.make_delivery(child, h.sub, b"child work", b"child results") + st.append(h.call(h.client.deliver, dc, 202)) + st.append(h.call(h.client.verdict, agents.make_verdict(child, dc, h.verifier, "PASS"), 201)) + h.clock.advance(WINDOW + 1) + child_out = h.outcome(child) + dp = agents.make_delivery(parent, h.seller, b"parent work", b"parent results") + st.append(h.call(h.client.deliver, dp, 202)) + st.append(h.call(h.client.verdict, agents.make_verdict(parent, dp, h.verifier, "PASS"), 201)) + h.clock.advance(WINDOW + 1) + out = h.outcome(parent) + root = "sha256:" + pc.mth([bytes.fromhex(pc.digest_over(child_out)[7:])]).hex() + assert out.get("children_merkle_root") == root, "children_merkle_root does not recompute" + assert [e["event"] for e in out["trace"]].count("child-final") == 1 + return parent, out, st + + +def scenario_tree_unresolved(h: Harness): + """A child registered from another venue that never reports: the parent + waits until L(child) and then closes with child-unresolved.""" + parent = h.contract(days=7) + st = [h.call(h.client.propose, parent, 201)] + link = {"vtc_id": parent["id"], "vtc_hash": pc.digest_over(parent), + "facilitator": h.fac.identity} + child = h.contract(buyer=h.seller, seller=h.sub, days=1, parent=link, + facilitator=h.other.did) + st.append(h.call(lambda c: h.client.register_child(parent["id"], c), child, 201)) + dp = agents.make_delivery(parent, h.seller, b"parent work 2", b"parent results") + st.append(h.call(h.client.deliver, dp, 202)) + st.append(h.call(h.client.verdict, agents.make_verdict(parent, dp, h.verifier, "PASS"), 201)) + h.clock.advance(WINDOW + 1) + st.append(h.status_of(parent)) + assert st[-1]["state"] == "AWAITING_CHILDREN", st[-1]["state"] + h.clock.advance(F.latest_finality(child) - h.clock.t + 1) + out = h.outcome(parent) + events = [e["event"] for e in out["trace"]] + empty_root = "sha256:" + pc.mth([]).hex() # Section 12.2: D empty, the member present + assert "child-unresolved" in events and out.get("children_merkle_root") == empty_root, events + return parent, out, st + + +SCENARIOS = [ + ("FINAL (verdict-first, PASS, window closes)", scenario_final, "FINAL: PASS"), + ("SETTLED (the Verifier records FAIL)", scenario_settled, "SETTLED: the Verifier"), + ("ABANDONED (deadline, no Delivery)", scenario_abandoned, "ABANDONED"), + ("SETTLED (PASS overturned by a Challenge)", scenario_overturned, "SETTLED on an upheld Challenge, Figure"), + ("SETTLED (overturned, restitution_basis price)", + lambda h: scenario_overturned(h, restitution_basis="price"), "SETTLED on an upheld Challenge, basis"), + ("FINAL after verdict-lapsed", scenario_verdict_lapsed, "FINAL after verdict-lapsed"), + ("FINAL under delivery-first, principal at window-closed", scenario_delivery_first, + "FINAL under delivery-first"), + ("FINAL after dispute-lapsed (the PASS stands)", scenario_dispute_lapsed, None), + ("FINAL with one child, in-venue (Merkle root)", scenario_tree, None), + ("FINAL with one child unresolved at L(child)", scenario_tree_unresolved, None), +] + + +def run_scenario(h: Harness, name: str, fn, vector: str | None) -> dict: + start = len(h.client.wire) + t0 = time.perf_counter() + vtc, out, statuses = fn(h) + ms = (time.perf_counter() - t0) * 1000 + wire = h.client.wire[start:] + prof = h.profile + transfers = out["terms_result"]["transfers"] + assert prof.schedule(vtc, out["trace"]) == transfers, f"{name}: transfers do not recompute" + ok, why = prof.check(vtc, transfers, terminal=True) + assert ok, f"{name}: {why}" + for s in statuses: + if s["vtc_id"] == vtc["id"]: + assert agents.prefix_of(s["trace"], out["trace"]), f"{name}: a Status was not a prefix" + last = out["trace"][-1] + assert last["event"] == "terminal" and last["state"] == out["outcome"]["state"] + match = None + if vector is not None: + vec = next(v for v in prof.vectors() if v["name"].startswith(vector)) + match = ([e["event"] for e in vec["trace"]] == [e["event"] for e in out["trace"]] + and vec["transfers"] == transfers) + assert match, f"{name}: does not reproduce the profile vector {vec['name']!r}" + by_code: dict[str, str] = {} + for t in transfers: + if t["code"] in ("principal", "reverse", "restitution", "costs", "bounty", "remainder"): + by_code[t["code"]] = t["amount"] + return { + "name": name, "state": out["outcome"]["state"], + "challenge_upheld": out["outcome"]["challenge_upheld"], + "exchanges": len(wire), + "bytes_out": sum(w.request_bytes for w in wire), + "bytes_back": sum(w.response_bytes for w in wire), + "ms": round(ms, 1), + "messages": [(w.method, w.path.rsplit("/", 1)[-1] if w.method == "GET" else w.path.split("/pact/v2/")[-1], + w.status, w.request_bytes, w.response_bytes) for w in wire], + "events": [e["event"] for e in out["trace"]], + "transfers": transfers, "summary": by_code, + "vector": vector, "vector_reproduced": match, + } # -------------------------------------------------------------------------- -# Capability document, Section 8 +# Refusals: (name, expected status, expected problem kind, profile problem?) # -------------------------------------------------------------------------- -def run_capability(h: Harness) -> None: - st, doc = h.client.capability() - ok, why = pc.verify_object(doc, h.resolver, fac_mod.MEDIA_FACILITATOR, [FACILITATOR]) - REPORT["capability_document"] = { - "status": st, - "schema": _schema_check(doc, "facilitator.schema.json"), - "signature_verifies": ok, - "release_modes": doc.get("release_modes"), - "settlement_bindings": doc.get("settlement_bindings"), - } +def refusals(h: Harness) -> list[tuple[str, int, str, bool, object]]: + c = h.client + P = agents.Party + + def alg_none_contract(): + vtc = h.contract(sign=False) + header = pc.b64u(json.dumps({"alg": "none", "kid": h.buyer.key.kid, + "typ": agents.MEDIA_CONTRACT}).encode()) + vtc["signatures"] = agents.sort_signatures([ + {"protected": header, "signature": ""}, + pc.sign(vtc, h.seller.key, agents.MEDIA_CONTRACT)]) + return c.propose(vtc) + + def third_party_signature(): + vtc = h.contract(sign=False) + return c.propose(agents.cosign(vtc, h.buyer, h.seller, h.watch)) + + def unsorted(): + vtc = h.contract() + vtc["signatures"] = list(reversed(vtc["signatures"])) + return c.propose(vtc) + + def altered_same_id(): + vtc = h.funded() + again = agents.cosign({k: v for k, v in vtc.items() if k != "signatures"} | { + "task": dict(vtc["task"], spec_uri="https://buyer.example/specs/other.json")}, + h.buyer, h.seller) + return c.propose(again) + + def lookalike_delivery(): + vtc = h.funded() + evil = agents.make_party("did:web:dataforge.example.evil:agents:etl-3", h.resolver, c) + return c.deliver(agents.make_delivery(vtc, evil, b"w", b"r")) + + def delivery_without_evidence(): + vtc = h.funded() + d = agents.make_delivery(vtc, h.seller, b"w", b"r") + d.pop("evidence") + d.pop("signature") + return c.deliver(h.seller.sign_into(d, agents.MEDIA_DELIVERY)) + + def delivery_without_input_hash(): + vtc = h.funded() + d = agents.make_delivery(vtc, h.seller, b"w", b"r") + d.pop("input_hash") + d.pop("signature") + return c.deliver(h.seller.sign_into(d, agents.MEDIA_DELIVERY)) + + def second_delivery(): + vtc, d = h.delivered() + return c.deliver(agents.make_delivery(vtc, h.seller, b"another", b"r")) + + def verdict_without_delivery(): + vtc = h.funded() + fake = agents.make_delivery(vtc, h.seller, b"never posted", b"r") + return c.verdict(agents.make_verdict(vtc, fake, h.verifier, "PASS")) + + def verdict_by_seller(): + vtc, d = h.delivered() + return c.verdict(agents.make_verdict(vtc, d, h.seller, "PASS")) + + def verdict_by_facilitator(): + vtc, d = h.delivered(verifier=False) + fp = P(h.fac.identity, h.fac.key, c) + return c.verdict(agents.make_verdict(vtc, d, fp, "PASS")) + + def verdict_by_unnamed_party(): + vtc, d = h.delivered() + return c.verdict(agents.make_verdict(vtc, d, h.watch, "PASS")) + + def verdict_other_instrument(): + vtc, d = h.delivered() + v = agents.make_verdict(vtc, d, h.verifier, "PASS") + v["instrument_hash"] = pc.h(b"another instrument") + v.pop("signature") + return c.verdict(h.verifier.sign_into(v, agents.MEDIA_VERDICT)) + + def verdict_wrong_delivery_hash(): + vtc, d = h.delivered() + v = agents.make_verdict(vtc, d, h.verifier, "PASS") + v["delivery_hash"] = pc.digest_over({k: x for k, x in d.items() if k != "signature"}) + v.pop("signature") + return c.verdict(h.verifier.sign_into(v, agents.MEDIA_VERDICT)) + + def verdict_with_delivery_typ(): + vtc, d = h.delivered() + v = agents.make_verdict(vtc, d, h.verifier, "PASS") + v.pop("signature") + return c.verdict(h.verifier.sign_into(v, agents.MEDIA_DELIVERY)) + + def challenge_before_window(): + vtc, d = h.delivered() + return c.challenge(agents.make_challenge(vtc, d, h.watch, ["x"])) + + def challenge_by_seller(): + vtc, d, _ = h.window_open() + return c.challenge(agents.make_challenge(vtc, d, h.seller, ["x"])) + + def challenge_nonconformant(): + vtc, d, _ = h.window_open() + ch = agents.make_challenge(vtc, d, h.watch, ["x"]) + ch["proof"]["instrument_hash"] = pc.h(b"other") + ch.pop("signature") + return c.challenge(h.watch.sign_into(ch, agents.MEDIA_CHALLENGE)) + + def verdict_by_the_challenger(): + vtc, d, v, ch = h.disputed(verifier=False) + return c.verdict(agents.make_verdict(vtc, d, h.watch, "FAIL", ch)) + + def verdict_in_disputed_without_challenge_hash(): + vtc, d, v, ch = h.disputed() + return c.verdict(agents.make_verdict(vtc, d, h.verifier, "FAIL")) + + def challenge_after_window(): + vtc, d, _ = h.window_open() + h.clock.advance(WINDOW + 1) + return c.challenge(agents.make_challenge(vtc, d, h.watch, ["x"])) + + def verdict_after_terminal(): + vtc, d, _ = h.window_open() + h.clock.advance(WINDOW + 1) + h.outcome(vtc) + return c.verdict(agents.make_verdict(vtc, d, h.verifier, "FAIL")) + + def child_wrong_buyer(): + parent = h.funded() + link = {"vtc_id": parent["id"], "vtc_hash": pc.digest_over(parent), + "facilitator": h.fac.identity} + child = h.contract(buyer=h.buyer, seller=h.sub, days=1, parent=link) + return c.register_child(parent["id"], child) + + def child_timing(): + parent = h.funded(days=1) + link = {"vtc_id": parent["id"], "vtc_hash": pc.digest_over(parent), + "facilitator": h.fac.identity} + child = h.contract(buyer=h.seller, seller=h.sub, days=1, parent=link) + return c.register_child(parent["id"], child) + + def child_outcome_wrong_hash(): + parent = h.funded() + link = {"vtc_id": parent["id"], "vtc_hash": pc.digest_over(parent), + "facilitator": h.fac.identity} + child = h.contract(buyer=h.seller, seller=h.sub, days=1, parent=link, + facilitator=h.other.did) + h.call(lambda x: c.register_child(parent["id"], x), child, 201) + record = { + "pact": "0.2", "type": "OutcomeRecord", "vtc_id": child["id"], + "vtc_hash": pc.h(b"not the child"), "parties": child["parties"], + "outcome": {"state": "FINAL", "challenge_upheld": False}, + "trace": [{"event": "accepted", "at": iso(h.clock.t), "object": pc.h(b"x")}, + {"event": "terminal", "at": iso(h.clock.t), "state": "FINAL", + "challenge_upheld": False}], + "terms_result": {"profile": terms.ID, "profile_hash": h.profile.profile_hash, + "currency": "USDC", "transfers": []}, + } + record["signatures"] = [pc.sign(record, h.other.key, agents.MEDIA_OUTCOME)] + return c.supply_child_outcome(parent["id"], child["id"], record) + + return [ + # propose + ("bond below the assurance constraint (q_min 0.5)", 422, "assurance-constraint-unsatisfied", True, + lambda: c.propose(h.contract(q_min=0.5))), + ("bond above cap", 422, "parameters-inconsistent", True, + lambda: c.propose(h.contract(bond="200.00"))), + ("window_seconds 0", 422, "schema-invalid", False, + lambda: c.propose(h.contract(window_seconds=0))), + ("undefined member in the contract", 422, "schema-invalid", False, + lambda: c.propose(agents.cosign(h.contract(sign=False) | {"bonus": True}, h.buyer, h.seller))), + ("buyer equals seller", 422, "parties-not-distinct", False, + lambda: c.propose(h.contract(buyer=h.seller, seller=h.seller))), + ("price with three decimals", 422, "amount-invalid", False, + lambda: c.propose(h.contract(price="180.005", bond="18.00"))), + ("deadline already past", 422, "deadline-invalid", False, + lambda: c.propose(h.contract(days=-1))), + ("another Facilitator named", 422, "facilitator-mismatch", False, + lambda: c.propose(h.contract(facilitator="did:web:elsewhere.example"))), + ("named verifier is the seller", 422, "verifier-not-independent", False, + lambda: c.propose(agents.cosign(h.contract(sign=False) | { + "parties": dict(h.contract(sign=False)["parties"], verifier=h.seller.did)}, h.buyer, h.seller))), + ("settlement binding not advertised", 422, "settlement-unsupported", False, + lambda: c.propose(h.contract(settlement="https://elsewhere.example/bindings/x"))), + ("flow no-window not implemented", 422, "flow-unsupported", False, + lambda: c.propose(h.contract(flow="no-window", principal_on="delivered", bond="200.00", price="180.00") + if False else h.contract(flow="no-window", principal_on="delivered"))), + ("terms profile_hash not advertised", 422, "terms-unsupported", False, + lambda: c.propose(h.contract(profile_hash=pc.h(b"other bundle")))), + ("terms parameters fail the profile schema", 422, "terms-parameters-invalid", False, + lambda: c.propose(h.contract(bond="eighteen"))), + ("contract signed with alg none", 400, "algorithm-not-permitted", False, alg_none_contract), + ("a third party co-signs the contract", 422, "unexpected-signer", False, third_party_signature), + ("signature set out of order", 422, "signatures-unordered", False, unsorted), + ("same id, different contract", 409, "object-conflict", False, altered_same_id), + # delivery + ("Delivery signed by a lookalike seller", 422, "unexpected-signer", False, lookalike_delivery), + ("Delivery without evidence", 422, "evidence-nonconformant", False, delivery_without_evidence), + ("Delivery without input_hash under T0-reexec", 422, "evidence-nonconformant", False, + delivery_without_input_hash), + ("second Delivery in DELIVERED", 409, "wrong-state", False, second_delivery), + # verdict + ("Verdict with no recorded Delivery", 409, "no-recorded-delivery", False, verdict_without_delivery), + ("Verdict signed by the seller", 422, "verifier-not-independent", False, verdict_by_seller), + ("Verdict signed by the Facilitator", 422, "verifier-not-independent", False, verdict_by_facilitator), + ("Verdict by a party the contract does not name", 422, "verifier-not-independent", False, + verdict_by_unnamed_party), + ("Verdict over another instrument", 422, "verdict-nonconformant", False, verdict_other_instrument), + ("Verdict whose delivery_hash omits the signature", 422, "verdict-nonconformant", False, + verdict_wrong_delivery_hash), + ("Verdict signed with the Delivery typ", 401, "signature-invalid", False, verdict_with_delivery_typ), + ("Verdict in DISPUTED without challenge_hash", 422, "verdict-nonconformant", False, + verdict_in_disputed_without_challenge_hash), + ("Verdict by the Challenger it answers", 422, "verifier-not-independent", False, + verdict_by_the_challenger), + ("Verdict after the terminal entry", 409, "wrong-state", False, verdict_after_terminal), + # challenge + ("Challenge before the window opens", 409, "challenge-window-closed", False, challenge_before_window), + ("Challenge after the window closes", 409, "challenge-window-closed", False, challenge_after_window), + ("Challenge signed by the seller", 422, "unexpected-signer", False, challenge_by_seller), + ("Challenge whose proof names another instrument", 422, "proof-nonconformant", False, + challenge_nonconformant), + # trees + ("child whose Buyer is not the parent's Seller", 422, "parent-unresolvable", False, child_wrong_buyer), + ("child with L(child) not before L(parent)", 422, "finality-ordering-violation", False, child_timing), + ("child Outcome Record over the wrong contract", 422, "child-outcome-invalid", False, + child_outcome_wrong_hash), + # retrieval + ("GET an unknown contract", 404, "unknown-contract", False, lambda: c.status("vtc_nobody")), + ("GET the Outcome Record before the terminal entry", 409, "wrong-state", False, + lambda: c.outcome(h.funded()["id"])), + ] + + +def acceptances(h: Harness) -> list[tuple[str, object]]: + c = h.client + + def challenge_alone_does_not_settle(): + vtc, d, v, ch = h.disputed() + st = h.status_of(vtc) + return st["state"] == "DISPUTED" and st["trace"][-1]["event"] == "challenge" + + def resubmit_identical_contract(): + vtc = h.funded() + code, body = c.propose(vtc) + return code == 200 and body["type"] == "ContractStatus" and body["state"] == "FUNDED" + + def buyer_challenge_admissible(): + vtc, d, _ = h.window_open() + code, body = c.challenge(agents.make_challenge(vtc, d, h.buyer, ["x"], costs=None)) + return code == 202 and body["state"] == "DISPUTED" + + def refusal_leaves_no_entry(): + vtc, d = h.delivered() + before = h.status_of(vtc)["trace"] + code, _ = c.verdict(agents.make_verdict(vtc, d, h.seller, "PASS")) + after = h.status_of(vtc)["trace"] + return code == 422 and before == after + + def pass_in_window_changes_nothing(): + vtc, d, v = h.window_open() + again = agents.make_verdict(vtc, d, h.verifier, "PASS") + again["evaluated_at"] = "2099-01-01T00:00:00Z" # different bytes, or it is a replay + again.pop("signature") + code, body = c.verdict(h.verifier.sign_into(again, agents.MEDIA_VERDICT)) + return code == 201 and body["state"] == "WINDOW_OPEN" and body["trace"][-1]["supersedes"] == pc.digest_over(v) + + return [ + ("a Challenge alone settles nothing", challenge_alone_does_not_settle), + ("resubmitting an identical contract returns 200 and the current Status", + resubmit_identical_contract), + ("a Buyer's Challenge is admissible", buyer_challenge_admissible), + ("a refused request leaves no entry in the trace", refusal_leaves_no_entry), + ("a second PASS inside the window changes the state of nothing", + pass_in_window_changes_nothing), + ] + + +def run_refusals(h: Harness) -> tuple[list[dict], list[dict]]: + rows = [] + for name, status, kind, is_profile, fn in refusals(h): + code, body = fn() + base = terms.PROBLEM_BASE if is_profile else F.PROBLEM_BASE + where = body.get("profile_section") if is_profile else body.get("section") + ok = (code == status and body.get("type") == base + kind and bool(where) + and (not is_profile or body.get("profile") == terms.ID)) + rows.append({"name": name, "expected": (status, kind), "got": (code, body.get("type", "?")), + "section": where, "ok": ok}) + accepted = [] + for name, fn in acceptances(h): + accepted.append({"name": name, "ok": bool(fn())}) + return rows, accepted # -------------------------------------------------------------------------- -# Microbenchmarks +# Capability document and micro-benchmarks # -------------------------------------------------------------------------- -def bench(fn, n: int = 2000) -> dict: - for _ in range(50): +def run_capability(h: Harness) -> dict: + code, doc = h.client.capability() + ok, why = pc.verify_object(doc, h.resolver, agents.MEDIA_FACILITATOR, [h.fac.identity]) + assert code == 200 and ok, why + listed = [p["profile_hash"] for p in doc["terms_profiles"]] + return {"signed": ok, "flows": doc["flows"], "terms_profiles": doc["terms_profiles"], + "bytes": len(json.dumps(doc, separators=(",", ":"))), + "advertised_profile_reproduces": h.profile.profile_hash in listed} + + +def bench(fn, n: int) -> float: + times = [] + for _ in range(n): + t = time.perf_counter() fn() - samples = [] - for _ in range(7): - t0 = time.perf_counter() - for _ in range(n): - fn() - samples.append((time.perf_counter() - t0) / n * 1e6) - return {"median_us": round(statistics.median(samples), 2), - "min_us": round(min(samples), 2), "calls": n} - - -def run_micro(h: Harness) -> None: - vtc = h.fresh("vtc_bench") - dlv = agents.make_delivery(vtc, h.seller, b"w" * 4096, b"r" * 1024) - key = h.buyer.key - - # Labels say what is timed. "sign" and "verify" both include canonicalizing - # the contract to rebuild the detached payload, because that is what a - # Facilitator does per signature; the raw Ed25519 primitive is a fraction - # of each and is reported on its own so the reader can subtract. - REPORT["micro"]["canonicalize_contract"] = bench(lambda: pc.jcs(vtc)) - REPORT["micro"]["canonicalize_and_digest_contract"] = bench( - lambda: pc.digest_over(pc.hashable(vtc))) - REPORT["micro"]["sign_contract_incl_canonicalization"] = bench( - lambda: pc.sign(vtc, key, agents.MEDIA_CONTRACT), n=500) - entry = pc.sign(vtc, key, agents.MEDIA_CONTRACT) - REPORT["micro"]["verify_contract_signature_end_to_end"] = bench( - lambda: pc.verify_entry(vtc, entry, h.resolver), n=500) - msg = b"x" * 1500 - sig = key.sign_bytes(msg) - REPORT["micro"]["ed25519_sign_primitive_1500B"] = bench(lambda: key.sign_bytes(msg), n=500) - REPORT["micro"]["ed25519_verify_primitive_1500B"] = bench( - lambda: key.verify_bytes(sig, msg), n=500) - REPORT["micro"]["normalize_identifier"] = bench(lambda: pc.norm(SELLER)) - REPORT["micro"]["assurance_constraint_exact_decimal"] = bench( - lambda: pc.assurance_holds("180.00", "18.00", "0.9091", "0")) - - # RFC 9162 root over N leaves, pactcore.mth. The Facilitator does not build - # trees (subcontracts are not implemented); this is the primitive a - # Section 10 implementation would call per parent attestation. - for n in (2, 8, 64): - leaves = [pc.jcs({"child": i}) for i in range(n)] - REPORT["micro"][f"rfc9162_root_{n}_leaves"] = bench( - lambda leaves=leaves: pc.mth(leaves), n=200 if n > 8 else 1000) - - REPORT["micro"]["canonical_bytes"] = { - "contract": len(pc.jcs(pc.hashable(vtc))), - "delivery": len(pc.jcs(pc.hashable(dlv))), + times.append(time.perf_counter() - t) + return statistics.median(times) * 1e6 + + +def run_micro(h: Harness) -> dict: + vtc = h.contract() + d = agents.make_delivery(vtc, h.seller, b"w", b"r") + v = agents.make_verdict(vtc, d, h.verifier, "PASS") + signable = pc.signable(v) + vec = h.profile.vectors()[1] + leaves = [bytes.fromhex(pc.h(str(i).encode())[7:]) for i in range(64)] + return { + "canonicalize contract (us)": bench(lambda: pc.jcs(vtc), 2000), + "canonicalize + digest (us)": bench(lambda: pc.digest_over(vtc), 2000), + "sign Verdict, Ed25519, incl. canonicalization (us)": + bench(lambda: pc.sign(signable, h.verifier.key, agents.MEDIA_VERDICT), 500), + "verify Verdict end to end (us)": + bench(lambda: pc.verify_object(v, h.resolver, agents.MEDIA_VERDICT, [h.verifier.did]), 500), + "normalize identifier (us)": bench(lambda: pc.norm("did:web:Buyer.example:agents:procure-1#k1"), 5000), + "profile schedule over the overturned trace (us)": + bench(lambda: h.profile.schedule(vec["contract"] | vtc, vec["trace"]), 500), + "schema-validate a contract (us)": bench(lambda: h.fac.schemas.check(vtc, "vtc.schema.json"), 200), + "RFC 9162 root, 2 leaves (us)": bench(lambda: pc.mth(leaves[:2]), 2000), + "RFC 9162 root, 8 leaves (us)": bench(lambda: pc.mth(leaves[:8]), 2000), + "RFC 9162 root, 64 leaves (us)": bench(lambda: pc.mth(leaves), 500), } - # There is deliberately NO T0-reexec figure. An earlier version timed - # examples/acceptance-harness/test_acceptance.py by running it as a plain - # script, which executes no tests at all; the number was pytest's import - # time. An honest figure needs the harness invoked through pytest, which - # currently fails because pytest_addoption sits in a test module rather - # than a conftest.py, and moving it changes criteria_hash: a -02 item. # -------------------------------------------------------------------------- @@ -532,72 +677,59 @@ def run_micro(h: Harness) -> None: def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--port", type=int, default=8412) - ap.add_argument("--json", action="store_true") + ap.add_argument("--json", type=pathlib.Path, help="write the full result here") args = ap.parse_args() - - REPORT["environment"] = { - "python": platform.python_version(), - "platform": platform.platform(), - "processor": _cpu_name(), - "signature_algorithm": "Ed25519 (EdDSA, RFC 8037)", - "note": "single host, loopback HTTP, in-memory store, no payment rail; the " - "Facilitator's clock is advanced by the harness", - } - h = Harness(args.port) try: - _schema_check({}, "vtc.schema.json") # load schemas outside any timed region - run_capability(h) - scenario(h, "FINAL", lambda: run_final(h)) - scenario(h, "SETTLED", lambda: run_settled(h)) - scenario(h, "ABANDONED", lambda: run_abandoned(h)) - scenario(h, "OVERTURNED_PASS", lambda: run_overturned(h)) - scenario(h, "SETTLED_basis_price", lambda: run_settled_price(h)) - run_refusals(h) - run_micro(h) + scenarios = [run_scenario(h, name, fn, vec) for name, fn, vec in SCENARIOS] + refused, accepted = run_refusals(h) + cap = run_capability(h) + micro = run_micro(h) finally: - h.stop() - + h.close() + + hw = f"{platform.machine()}, {platform.system()} {platform.release()}, Python {platform.python_version()}" + print(f"reference pair, draft-laxsharma-pact-02; {hw}; loopback, in-memory, clock advanced by the harness") + print(f"terms profile {terms.ID}\n profile_hash {h.profile.profile_hash}\n") + print("PATHS") + print(f" {'path':58} {'exch':>4} {'out':>6} {'back':>6} {'ms':>6} state / value moved") + for s in scenarios: + summary = ", ".join(f"{k} {v}" for k, v in s["summary"].items()) or "none" + vec = "" if s["vector_reproduced"] is None else (" [vector reproduced]" if s["vector_reproduced"] else " [VECTOR MISMATCH]") + print(f" {s['name']:58} {s['exchanges']:4d} {s['bytes_out']:6d} {s['bytes_back']:6d} {s['ms']:6.1f} " + f"{s['state']}{' upheld' if s['challenge_upheld'] else ''}: {summary}{vec}") + print("\n every Outcome Record: Facilitator signature verifies; transfers recompute from the trace with the") + print(" named profile; no-overdraft and closure hold; every Status received is a prefix of the final trace.") + for s in scenarios[:1] + scenarios[3:4]: + print(f"\n messages, {s['name']}:") + for m in s["messages"]: + print(f" {m[0]:4} {m[1]:34} {m[2]} {m[3]:5d} out {m[4]:5d} back") + print("\nREFUSALS") + bad = 0 + for r in refused: + flag = "ok " if r["ok"] else "BAD" + bad += not r["ok"] + print(f" {flag} {r['name']:56} {r['got'][0]} {r['got'][1].rsplit(':', 1)[-1]:36} section {r['section']}") + print(f" {len(refused)} refusals, {bad} wrong") + print("\nACCEPTANCES") + for a in accepted: + print(f" {'ok ' if a['ok'] else 'BAD'} {a['name']}") + bad += not a["ok"] + print("\nCAPABILITY DOCUMENT") + print(f" signed: {cap['signed']}; flows {cap['flows']}; {len(cap['terms_profiles'])} terms profile(s), " + f"advertised profile reproduces its vectors: {cap['advertised_profile_reproduces']}; {cap['bytes']} bytes") + print("\nMICRO (median)") + for k, v in micro.items(): + print(f" {k:56} {v:8.1f}") if args.json: - print(json.dumps(REPORT, indent=1)) - return - - e = REPORT["environment"] - print(f"reference pair on {e['processor']}, Python {e['python']}, {e['signature_algorithm']}") - print(f"{e['note']}\n") - - cd = REPORT["capability_document"] - print(f"CAPABILITY DOCUMENT status {cd['status']}, schema {cd['schema']}, " - f"signature verifies: {cd['signature_verifies']}\n") - - print("TERMINAL STATES") - for name, s in REPORT["scenarios"].items(): - print(f" {name:<20} {s['messages']} messages, {s['request_bytes']}B out / " - f"{s['response_bytes']}B back, {s['wall_ms']}ms, attestation verifies: " - f"{s['attestation_verifies']}, money balanced: {s['money']['balanced']}") - print(f" amounts {s['amounts']}") - bad = [k for k, v in s.get("schema", {}).items() if v != "valid"] - if bad: - print(f" SCHEMA FAILURES: {bad}") - print() - - print(f"REFUSALS ({len(REPORT['refusals'])})") - for name, r in REPORT["refusals"].items(): - print(f" {name:<34} {r['status']} {r['type']:<34} {r['section']}") - print(f"\nACCEPTANCES ({len(REPORT['acceptances'])})") - for name, r in REPORT["acceptances"].items(): - print(f" {name:<34} {r['status']} state {r['state']:<12} {r['section']} {r['detail']}") - print() - - print("MICROBENCHMARKS, median microseconds per call") - for name, m in REPORT["micro"].items(): - if isinstance(m, dict) and "median_us" in m: - print(f" {name:<40} {m['median_us']:>10.2f} us") - cb = REPORT["micro"].get("canonical_bytes", {}) - if cb: - print(f" canonical bytes contract {cb['contract']}, " - f"delivery {cb['delivery']}") - print("\n no T0-reexec figure is reported; see the comment in run_micro") + args.json.write_text(json.dumps({ + "hardware": hw, "profile": terms.ID, "profile_hash": h.profile.profile_hash, + "scenarios": scenarios, "refusals": refused, "acceptances": accepted, + "capability": cap, "micro": micro}, indent=1)) + print(f"\nwrote {args.json}") + if bad: + print(f"\n{bad} check(s) failed") + sys.exit(1) if __name__ == "__main__": diff --git a/tools/mint_examples.py b/tools/mint_examples.py new file mode 100644 index 0000000..808dde9 --- /dev/null +++ b/tools/mint_examples.py @@ -0,0 +1,273 @@ +"""Mint the committed examples under examples/, reproducibly. + +The -01 examples carried placeholder signatures because the posted draft +printed their digests and nothing could be changed. -02 recomputes every +digest, so the examples are signed for real, with Ed25519 keys derived from +public seeds, and Ed25519 signatures are deterministic: anyone running this +script gets byte-identical files and therefore the digests Section 15 of the +draft prints. The seeds are not secrets and the keys must never be used for +anything but these examples. + + python3 tools/mint_examples.py # rewrite examples/ and print digests + python3 tools/mint_examples.py --check # recompute and compare, write nothing +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) + +import agents +import pactcore as pc +import profile as terms + +ROOT = pathlib.Path(__file__).resolve().parent.parent +EX = ROOT / "examples" +CONTENT = EX / "task-content" + +BUYER = "did:web:buyer.example:agents:procure-1" +SELLER = "did:web:dataforge.example:agents:etl-3" +FACILITATOR = "did:web:settle.example" +VERIFIER = "did:web:audit.example" +CHALLENGER = "did:web:watch.example" + +# The Facilitator's clock for the worked example, Section 15 and Appendix A. +T_ACCEPTED = "2026-11-01T10:00:00Z" +T_DELIVERED = "2026-11-10T08:30:12Z" +T_VERDICT = "2026-11-10T09:14:30Z" +T_CLOSES = "2026-11-10T10:14:30Z" +T_CHALLENGE = "2026-11-10T09:40:00Z" +T_VERDICT2 = "2026-11-10T09:58:05Z" + + +def key_for(did: str) -> pc.Key: + seed = hashlib.sha256(b"pact-spec example key, public seed: " + did.encode()).digest() + return pc.Key.from_seed(f"{did}#k1", seed) + + +def dump(obj: dict) -> str: + return json.dumps(obj, indent=2, ensure_ascii=False) + "\n" + + +def build() -> dict[str, dict]: + keys = {name: key_for(did) for name, did in + (("buyer", BUYER), ("seller", SELLER), ("facilitator", FACILITATOR), + ("verifier", VERIFIER), ("challenger", CHALLENGER))} + profile = terms.BondedRestitution() + + harness_hash = pc.manifest_digest(EX / "acceptance-harness") + content_hash = {p.name: pc.h(p.read_bytes()) for p in CONTENT.iterdir() if p.suffix != ".md"} + + taskspec = { + "description": "Deduplicate a 2,100,000-row customer CSV; normalize country " + "fields to ISO-3166 alpha-2; merge conflicting records by " + "most-recent timestamp.", + "inputs": { + "schema_uri": "https://buyer.example/specs/customers.schema.json", + "schema_hash": content_hash["customers.schema.json"], + "sample_uri": "https://buyer.example/specs/sample-10k.csv", + "sample_hash": content_hash["sample-10k.csv"], + }, + "deliverable": { + "format": "csv", + "schema_uri": "https://buyer.example/specs/output.schema.json", + "schema_hash": content_hash["output.schema.json"], + }, + "acceptance": { + "harness_uri": "https://buyer.example/specs/acceptance-tests.tar", + "harness_hash": harness_hash, + "thresholds": {"dup_rate_max": 0.001, "schema_valid_rate": 1.0}, + }, + "constraints": {"tools_prohibited": ["external-APIs"], "confidential": False}, + } + spec_hash = pc.digest_over(taskspec) + + vtc = { + "pact": "0.2", + "type": "VerifiableTaskContract", + "id": "vtc_9f2c11", + "parties": {"buyer": BUYER, "seller": SELLER, "facilitator": FACILITATOR, + "verifier": VERIFIER}, + "task": {"spec_hash": spec_hash, + "spec_uri": "https://buyer.example/specs/taskspec.json", + "deadline": "2026-11-14T00:00:00Z"}, + "price": {"amount": "180.00", "currency": "USDC", + "settlement": "https://settle.example/bindings/ledger-1", + "network": "eip155:8453"}, + "verification": {"tier": "T0-reexec", "profile": "acceptance", + "criteria_hash": harness_hash, "max_verdict_seconds": 86400}, + "flow": "verdict-first", + "terms": { + "profile": profile.id, + "profile_hash": profile.profile_hash, + "parameters": { + "seller_bond": "18.00", "verification_fund": "0.50", "cap": "180.00", + "restitution_basis": "released", "remainder_to": "sink", + "principal_on": "verdict", + "assurance": {"mode": "certain", "q_min": 1.0}, + }, + }, + "challenge": {"window_seconds": 3600, "max_dispute_seconds": 86400}, + } + vtc["signatures"] = agents.sort_signatures([ + pc.sign(vtc, keys["buyer"], agents.MEDIA_CONTRACT), + pc.sign(vtc, keys["seller"], agents.MEDIA_CONTRACT)]) + vtc_hash = pc.digest_over(vtc) + + work = (CONTENT / "sample-10k.csv").read_bytes() # stands in for the deliverable + results = b'{"schema_valid_rate": 1.0, "dup_rate": 0.0}\n' + delivery = { + "pact": "0.2", "type": "Delivery", "vtc_id": vtc["id"], "vtc_hash": vtc_hash, + "work_hash": pc.h(work), + "work_uri": "https://cdn.dataforge.example/o/" + pc.h(work)[7:11], + "input_hash": content_hash["sample-10k.csv"], + "evidence": {"profile": "acceptance", "instrument_hash": harness_hash, + "results_hash": pc.h(results), + "results_uri": "https://cdn.dataforge.example/o/" + pc.h(results)[7:11]}, + } + delivery["signature"] = pc.sign(delivery, keys["seller"], agents.MEDIA_DELIVERY) + delivery_hash = pc.digest_over(delivery) + + verdict = { + "pact": "0.2", "type": "Verdict", "vtc_id": vtc["id"], + "delivery_hash": delivery_hash, "outcome": "PASS", "profile": "acceptance", + "instrument_hash": harness_hash, "results_hash": pc.h(results), + "evaluated_at": "2026-11-10T09:14:22Z", + } + verdict["signature"] = pc.sign(verdict, keys["verifier"], agents.MEDIA_VERDICT) + verdict_hash = pc.digest_over(verdict) + + challenge = { + "pact": "0.2", "type": "Challenge", "vtc_id": vtc["id"], + "delivery_hash": delivery_hash, + "proof": {"profile": "acceptance", "instrument_hash": harness_hash, + "results_hash": pc.h(b"independent re-execution: 41 duplicate rows"), + "results_uri": "https://watch.example/o/a91e", + "failing_checks": ["schema_valid_rate", "row_count_min"]}, + "costs": {"amount": "1.20", "currency": "USDC"}, + } + challenge["signature"] = pc.sign(challenge, keys["challenger"], agents.MEDIA_CHALLENGE) + challenge_hash = pc.digest_over(challenge) + + verdict2 = { + "pact": "0.2", "type": "Verdict", "vtc_id": vtc["id"], + "delivery_hash": delivery_hash, "challenge_hash": challenge_hash, + "outcome": "FAIL", "profile": "acceptance", "instrument_hash": harness_hash, + "results_hash": pc.h(b"independent re-execution: 41 duplicate rows"), + "evaluated_at": "2026-11-10T09:57:40Z", + } + verdict2["signature"] = pc.sign(verdict2, keys["verifier"], agents.MEDIA_VERDICT) + verdict2_hash = pc.digest_over(verdict2) + + trace = [ + {"event": "accepted", "at": T_ACCEPTED, "object": vtc_hash}, + {"event": "funded", "at": T_ACCEPTED}, + {"event": "delivered", "at": T_DELIVERED, "object": delivery_hash}, + {"event": "verdict", "at": T_VERDICT, "object": verdict_hash, + "signer": keys["verifier"].kid, "outcome": "PASS"}, + {"event": "window-opened", "at": T_VERDICT, "closes_at": T_CLOSES}, + {"event": "challenge", "at": T_CHALLENGE, "object": challenge_hash, + "signer": keys["challenger"].kid, "costs": challenge["costs"]}, + {"event": "verdict", "at": T_VERDICT2, "object": verdict2_hash, + "signer": keys["verifier"].kid, "outcome": "FAIL", + "answers": challenge_hash, "supersedes": verdict_hash}, + {"event": "children-final", "at": T_VERDICT2}, + {"event": "terminal", "at": T_VERDICT2, "state": "SETTLED", "challenge_upheld": True}, + ] + + status = { + "pact": "0.2", "type": "ContractStatus", "vtc_id": vtc["id"], "vtc_hash": vtc_hash, + "state": "WINDOW_OPEN", "trace": trace[:5], "issued_at": T_VERDICT, + } + status["signature"] = pc.sign(status, keys["facilitator"], agents.MEDIA_STATUS) + + transfers = profile.schedule(vtc, trace) + ok, why = profile.check(vtc, transfers, terminal=True) + assert ok, why + outcome = { + "pact": "0.2", "type": "OutcomeRecord", "vtc_id": vtc["id"], "vtc_hash": vtc_hash, + "parties": vtc["parties"], + "outcome": {"state": "SETTLED", "challenge_upheld": True}, + "work_hash": delivery["work_hash"], + "trace": trace, + "terms_result": {"profile": profile.id, "profile_hash": profile.profile_hash, + "currency": "USDC", "transfers": transfers}, + } + outcome["signatures"] = [pc.sign(outcome, keys["facilitator"], agents.MEDIA_OUTCOME)] + + capability = { + "pact": "0.2", "type": "FacilitatorCapabilities", "facilitator": FACILITATOR, + "settlement_bindings": [{"id": "https://settle.example/bindings/ledger-1", + "networks": ["eip155:8453"], "assets": ["USDC"]}], + "flows": ["verdict-first", "delivery-first"], + "verification_profiles": ["acceptance", "bisection"], + "terms_profiles": [{"id": profile.id, "profile_hash": profile.profile_hash}], + "max_contract_value": {"amount": "50000.00", "currency": "USDC"}, + "endpoints": { + "contract": "https://settle.example/pact/v2/contracts", + "delivery": "https://settle.example/pact/v2/deliveries", + "verdict": "https://settle.example/pact/v2/verdicts", + "challenge": "https://settle.example/pact/v2/challenges", + "outcome": "https://settle.example/pact/v2/outcomes", + }, + } + capability["signature"] = pc.sign(capability, keys["facilitator"], agents.MEDIA_FACILITATOR) + + public_keys = {name: {"kid": k.kid, "kty": "OKP", "crv": "Ed25519", + "x": pc.b64u(pc.public_bytes(k))} for name, k in keys.items()} + + files = { + "taskspec.json": taskspec, "vtc.json": vtc, "delivery.json": delivery, + "verdict.json": verdict, "challenge.json": challenge, "verdict-on-challenge.json": verdict2, + "status.json": status, "outcome.json": outcome, + "well-known/pact-facilitator.json": capability, + "keys/public-keys.json": public_keys, + } + digests = {"spec_hash": spec_hash, "criteria_hash": harness_hash, + "profile_hash": profile.profile_hash, "vtc_hash": vtc_hash, + "delivery_hash": delivery_hash, "verdict_hash": verdict_hash, + "challenge_hash": challenge_hash, "verdict2_hash": verdict2_hash} + return {"files": files, "digests": digests} + + +KEYS_README = """# Example keys + +`public-keys.json` holds the public keys that verify the signatures on the +committed examples, as JWKs (RFC 7517). The private keys are derived in +`tools/mint_examples.py` from public seeds, so they are not secrets and the +signatures are reproducible byte for byte; nothing signed with them means +anything outside this repository. +""" + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--check", action="store_true", help="compare with the files on disk") + args = ap.parse_args() + built = build() + changed = [] + for rel, obj in built["files"].items(): + path = EX / rel + text = dump(obj) + if args.check: + if not path.exists() or path.read_text() != text: + changed.append(rel) + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + if not args.check: + (EX / "keys" / "README.md").write_text(KEYS_README) + for name, value in built["digests"].items(): + print(f"{name:<16}{value}") + if args.check: + print("differs from disk: " + (", ".join(changed) if changed else "nothing")) + sys.exit(1 if changed else 0) + + +if __name__ == "__main__": + main() diff --git a/tools/pactcore.py b/tools/pactcore.py index 1d1479f..1dee7e2 100644 --- a/tools/pactcore.py +++ b/tools/pactcore.py @@ -84,10 +84,63 @@ def _utf16_key_order(obj: Any) -> Any: return obj +def _es6_number(f: float) -> str: + """Serialize a float the way ECMAScript Number::toString does, which is + what RFC 8785 section 3.2.2.3 requires. Python's own repr gives the same + shortest round-trip digits but places them differently: 1.0 becomes + "1.0", 1e20 becomes "1e+20" and 1e-7 becomes "1e-07", and each of those + is a different byte string, so a different digest, from what a conformant + canonicalizer produces. An earlier revision of this file used repr and + every digest it printed over an object with a float was wrong.""" + if f != f or f in (float("inf"), float("-inf")): + raise ValueError("RFC 8785 does not serialize NaN or Infinity") + if f == 0: + return "0" + d = Decimal(repr(f)) + sign = "-" if d < 0 else "" + t = abs(d).as_tuple() + digits = "".join(map(str, t.digits)) + n = t.exponent + len(digits) # value = 0.digits x 10^n + digits = digits.rstrip("0") + k = len(digits) + if k <= n <= 21: + s = digits + "0" * (n - k) + elif 0 < n <= 21: + s = digits[:n] + "." + digits[n:] + elif -6 < n <= 0: + s = "0." + "0" * (-n) + digits + else: + e = n - 1 + s = digits[0] + ("." + digits[1:] if k > 1 else "") + "e" + ("+" if e >= 0 else "-") + str(abs(e)) + return sign + s + + +def _emit(obj: Any) -> str: + if obj is None: + return "null" + if obj is True: + return "true" + if obj is False: + return "false" + if isinstance(obj, int): + return str(obj) + if isinstance(obj, float): + return _es6_number(obj) + if isinstance(obj, str): + return json.dumps(obj, ensure_ascii=False) + if isinstance(obj, (list, tuple)): + return "[" + ",".join(_emit(v) for v in obj) + "]" + if isinstance(obj, dict): + return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + _emit(obj[k]) + for k in sorted(obj, key=lambda s: s.encode("utf-16-be"))) + "}" + raise TypeError(f"not JSON: {type(obj).__name__}") + + def jcs(obj: Any) -> bytes: - """Restricted RFC 8785 canonical serialization. See the module docstring.""" - return json.dumps(_utf16_key_order(obj), separators=(",", ":"), - ensure_ascii=False).encode("utf-8") + """RFC 8785 canonical serialization: keys in UTF-16 code unit order, + numbers as ECMAScript prints them, strings escaped as JSON requires and + nothing else, no whitespace. See the module docstring.""" + return _emit(obj).encode("utf-8") def h(b: bytes) -> str: @@ -104,24 +157,36 @@ def digest_over(obj: Any) -> str: # PACT objects carry their signatures two ways, and both are in the schemas. -# The contract and the Work Attestation take an array, because more than one -# party signs them. Delivery, Verdict and Challenge take a single object, -# because exactly one party does. +# The contract and the Outcome Record take an array, because more than one +# party may sign them. Delivery, Verdict, Challenge, Status and the capability +# document take a single object, because exactly one party does. SIGNING_MEMBERS = ("signatures", "signature") -# The Facilitator adds `state` to a response body. Section 12 says it is not -# part of the signed object and MUST NOT be included when the object is -# canonicalized or hashed, so it is stripped everywhere alongside signatures. -UNSIGNED_MEMBERS = SIGNING_MEMBERS + ("state",) - def signable(obj: dict) -> dict: - return {k: v for k, v in obj.items() if k not in UNSIGNED_MEMBERS} + """The signing input's object: everything but the signing member.""" + return {k: v for k, v in obj.items() if k not in SIGNING_MEMBERS} def hashable(obj: dict) -> dict: - """The object as it is committed to: signatures kept, transport state dropped.""" - return {k: v for k, v in obj.items() if k != "state"} + """The object as it is committed to: everything, signatures included. + + -02 Section 2 defines one digest construction over the whole object. The + -01 response bodies carried an unsigned `state` member that had to be + stripped here; the -02 Status object replaced it and nothing is stripped. + """ + return dict(obj) + + +def manifest_digest(dirpath) -> str: + """The bundle commitment of Section 5.1: SHA-256(JCS(M)) where M maps each + file's path, relative to the bundle root with "/" separators, to the + SHA-256 of its bytes, over every file in the bundle.""" + import pathlib + root = pathlib.Path(dirpath) + manifest = {p.relative_to(root).as_posix(): h(p.read_bytes()) + for p in sorted(root.rglob("*")) if p.is_file()} + return h(jcs(manifest)) def signature_entries(obj: dict) -> list[dict]: @@ -203,21 +268,6 @@ def signatures_ordered(obj: dict) -> tuple[bool, str]: # The assurance constraint, Section 7.2 # -------------------------------------------------------------------------- -def required_bond(price: float, q: float, released: float = 0.0) -> float: - """B >= P(1-q)/q + E. - - E, the amount already paid out before a Verdict is recorded, is the only - term this specification contributes; the rest is the classical deterrence - bound (Polinsky and Shavell; Belenkiy et al. Theorem 1; Mamageishvili and - Felten for rollup validators). Optimistic release both pays a defecting - Seller and puts that payment beyond recovery, so the required Bond rises - with it one for one. - """ - if q <= 0: - raise ValueError("q must be greater than zero") - return price * (1.0 - q) / q + released - - def assurance_holds(price: str | float, bond: str | float, q_min: str | float, released: str | float = "0") -> bool: """B >= P(1-q)/q + E, evaluated exactly. @@ -270,6 +320,30 @@ class Key: private: Any = None public: Any = None + @classmethod + def from_seed(cls, kid: str, seed: bytes) -> "Key": + """An Ed25519 key from 32 seed bytes. Used only to mint the committed + examples reproducibly; the seeds are public and so are the keys.""" + if not HAVE_CRYPTO: + raise RuntimeError("signing needs the `cryptography` package") + sk = Ed25519PrivateKey.from_private_bytes(seed) + return cls(kid=kid, alg="EdDSA", private=sk, public=sk.public_key()) + + @classmethod + def from_public_bytes(cls, kid: str, alg: str, raw: bytes) -> "Key": + """A verify-only key from the raw public bytes public_bytes() emits.""" + if not HAVE_CRYPTO: + raise RuntimeError("verification needs the `cryptography` package") + if alg == "EdDSA": + pub = Ed25519PublicKey.from_public_bytes(raw) + elif alg == "ES256": + pub = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), raw) + elif alg == "ES384": + pub = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP384R1(), raw) + else: + raise ValueError(f"unsupported alg {alg}") + return cls(kid=kid, alg=alg, private=None, public=pub) + @classmethod def generate(cls, kid: str, alg: str = "EdDSA") -> "Key": if not HAVE_CRYPTO: @@ -504,30 +578,3 @@ def money(c: int) -> str: return f"{c / 100:.2f}" -@dataclass -class Pools: - """The three pools of Section 7.1, in cents. - - Keeping the Verification Fund separate from the Bond is not tidiness. Under - the -00 a Challenger was reimbursed from the slashed Bond, so reimbursement - was capped by the Bond, and for any re-execution profile the cost of - producing a fraud proof approximates the cost of the work itself. That MUST - was unsatisfiable in the ordinary case. - """ - escrow: int = 0 - bond: int = 0 - fund: int = 0 - bond_initial: int = 0 - bond_returned: int = 0 - fund_returned: int = 0 - cap: int = 0 # liability.cap: the most the Facilitator may move from the Seller - released: int = 0 # E in the constraint of Section 7.2 - paid_to_buyer: int = 0 - restituted: int = 0 - paid_to_seller: int = 0 - paid_to_challenger: int = 0 - remainder: int = 0 - ledger: list[str] = field(default_factory=list) - - def note(self, line: str) -> None: - self.ledger.append(line) diff --git a/tools/profile.py b/tools/profile.py new file mode 100644 index 0000000..3d54132 --- /dev/null +++ b/tools/profile.py @@ -0,0 +1,404 @@ +"""The bonded-restitution terms profile, evaluated the way -02 Section 5.3 says. + +A terms profile is a total, deterministic function from a contract and a trace +prefix to the entries it emits at the last event of that prefix. This module +is that function for the one profile in the repository, the non-normative +Appendix A of draft-laxsharma-pact-02, whose bundle lives under +profiles/bonded-restitution/. The Facilitator calls it at every event and +never decides anything about value itself; validate.py calls it to reproduce +the vectors; nothing in the Internet-Draft depends on what it computes. + +Every figure here is the -01 draft's settlement content: the assurance +constraint as the admission rule, the three pools as accounts, the five-rank +remedy as the SETTLED schedule, and the choices the -01 left open made where +the reference implementation had already made them. + + python3 tools/profile.py --vectors # regenerate the bundle's vectors.json + python3 tools/profile.py --hash # print profile_hash +""" + +from __future__ import annotations + +import argparse +import json +import pathlib +from decimal import ROUND_UP, Decimal +from typing import Any + +import pactcore as pc + +ROOT = pathlib.Path(__file__).resolve().parent.parent +BUNDLE = ROOT / "profiles" / "bonded-restitution" +ID = "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution" +PROBLEM_BASE = ID + ":problem:" +INTERNAL = ("escrow", "bond", "fund") +CENT = Decimal("0.01") + + +class ProfileRefusal(Exception): + """A refusal arising from a rule of the profile, not of the document. + + Reported per -02 Section 13.3 with `profile` and `profile_section` in the + problem body instead of `section`. + """ + + def __init__(self, kind: str, detail: str, section: str, **extra: Any) -> None: + self.kind = kind + self.detail = detail + self.section = section + self.extra = extra + super().__init__(detail) + + +def _d(x: Any) -> Decimal: + return Decimal(str(x)) + + +def _money(d: Decimal) -> str: + return f"{d.quantize(CENT):f}" + + +class BondedRestitution: + id = ID + problem_base = PROBLEM_BASE + + def __init__(self, bundle: pathlib.Path = BUNDLE) -> None: + self.bundle = pathlib.Path(bundle) + self.schema = json.loads((self.bundle / "parameters.schema.json").read_text()) + self.profile_hash = pc.manifest_digest(self.bundle) + self._validator = None + try: + from jsonschema import Draft202012Validator + self._validator = Draft202012Validator(self.schema) + except ImportError: # pragma: no cover + pass + + # -- Section 5.3: the parameters validate against the profile's schema -- + def parameters_error(self, params: Any) -> str | None: + if self._validator is None: + return None if isinstance(params, dict) else "parameters is not an object" + errors = sorted(self._validator.iter_errors(params), key=lambda e: list(e.path)) + if not errors: + return None + e = errors[0] + where = "/".join(str(x) for x in e.path) or "(root)" + return f"parameters do not validate against the profile's schema at {where}: {e.message}" + + # -- Appendix A.4: admission at `accepted` ---------------------------- + def admit(self, vtc: dict) -> None: + prm = vtc["terms"]["parameters"] + price = _d(vtc["price"]["amount"]) + bond = _d(prm["seller_bond"]) + fund = _d(prm["verification_fund"]) + cap = _d(prm["cap"]) + if bond > cap or fund > cap: + raise ProfileRefusal("parameters-inconsistent", + "seller_bond and verification_fund cannot exceed cap", + "A.4", cap=prm["cap"]) + mode = prm["assurance"]["mode"] + if mode == "open": + raise ProfileRefusal("assurance-constraint-unsatisfied", + "open challenge is a backstop, not a source of q; this " + "profile refuses it as the sole declared assurance", "A.4") + q = _d(prm["assurance"]["q_min"]) + released_before_verdict = price if prm["principal_on"] == "delivered" else Decimal(0) + if not pc.assurance_holds(price, bond, q, released_before_verdict): + need = (price * (1 - q) / q + released_before_verdict).quantize(CENT, rounding=ROUND_UP) + raise ProfileRefusal( + "assurance-constraint-unsatisfied", + f"seller_bond {prm['seller_bond']} is below the minimum {need:f} required " + f"for q_min {q:f} at price {vtc['price']['amount']} with E " + f"{_money(released_before_verdict)}", "A.4", + required_bond=f"{need:f}", declared_bond=prm["seller_bond"], + q_min=prm["assurance"]["q_min"], price=vtc["price"]["amount"]) + + # -- Appendix A.3: accounts ----------------------------------------------- + @staticmethod + def accounts(vtc: dict) -> dict[str, Decimal]: + return {name: Decimal(0) for name in INTERNAL} + + # -- Appendix A.5: the schedule over a whole trace ------------------------ + def schedule(self, vtc: dict, trace: list[dict]) -> list[dict]: + prm = vtc["terms"]["parameters"] + currency = vtc["price"]["currency"] + price = _d(vtc["price"]["amount"]) + bond0 = _d(prm["seller_bond"]) + fund0 = _d(prm["verification_fund"]) + cap = _d(prm["cap"]) + fee = _d(prm.get("verifier_fee", "0.00")) + basis = prm["restitution_basis"] + remainder_to = prm.get("remainder_to", "sink") + principal_on = prm["principal_on"] + + bal = self.accounts(vtc) + out: list[dict] = [] + released = Decimal(0) + from_seller = Decimal(0) # what has left the Seller's accounts, for cap + verdicts: list[dict] = [] + challenges: dict[str, dict] = {} # digest -> entry + + def emit(i: int, src: str, dst: str, amount: Decimal, code: str) -> None: + nonlocal released, from_seller + if amount <= 0: + return + if src in bal: + assert bal[src] >= amount, f"schedule would overdraw {src}" + bal[src] -= amount + if dst in bal: + bal[dst] += amount + if code == "principal": + released += amount + if src == "bond": + from_seller += amount + out.append({"event": i, "from": src, "to": dst, + "amount": _money(amount), "code": code}) + + def standing() -> dict | None: + return verdicts[-1] if verdicts else None + + for i, e in enumerate(trace): + ev = e["event"] + if ev == "funded": + emit(i, "buyer", "escrow", price, "lock") + emit(i, "seller", "bond", bond0, "bond") + emit(i, "buyer", "fund", fund0, "fund") + elif ev == "delivered": + if principal_on == "delivered": + emit(i, "escrow", "seller", bal["escrow"], "principal") + elif ev == "challenge": + challenges[e["object"]] = e + elif ev == "verdict": + verdicts.append(e) + emit(i, "fund", "verifier", min(fee, bal["fund"]), "verification") + if (e["outcome"] == "PASS" and "answers" not in e + and principal_on == "verdict"): + emit(i, "escrow", "seller", bal["escrow"], "principal") + elif ev == "window-closed": + st = standing() + if principal_on == "window-closed" and not (st and st["outcome"] == "FAIL"): + emit(i, "escrow", "seller", bal["escrow"], "principal") + elif ev == "terminal": + state = e["state"] + if state == "FINAL": + emit(i, "escrow", "seller", bal["escrow"], "principal") + emit(i, "bond", "seller", bal["bond"], "return") + emit(i, "fund", "buyer", bal["fund"], "fund-return") + elif state == "ABANDONED": + emit(i, "escrow", "buyer", bal["escrow"], "reverse") + emit(i, "bond", "seller", bal["bond"], "return") + emit(i, "fund", "buyer", bal["fund"], "fund-return") + else: # SETTLED, five ranks + st = standing() + upheld = bool(e.get("challenge_upheld")) and st is not None and "answers" in st + challenger_account = None + costs = Decimal(0) + if upheld: + ch = challenges.get(st["answers"]) + if ch is not None: + challenger_account = "challenger:" + ch.get("signer", st["answers"]) + c = ch.get("costs") + if c and c.get("currency") == currency: + costs = _d(c["amount"]) + reversed_ = bal["escrow"] + emit(i, "escrow", "buyer", reversed_, "reverse") # rank 1 + if upheld and challenger_account: + emit(i, "fund", challenger_account, min(costs, bal["fund"]), "costs") # 2 + loss = released if basis == "released" else price - reversed_ + room = max(Decimal(0), cap - from_seller) + emit(i, "bond", "buyer", min(bal["bond"], room, loss), "restitution") # 3 + if upheld and challenger_account: + room = max(Decimal(0), cap - from_seller) + emit(i, "bond", challenger_account, min(bal["bond"], room), "bounty") # 4 + room = max(Decimal(0), cap - from_seller) + emit(i, "bond", "buyer" if remainder_to == "buyer" else "sink", + min(bal["bond"], room), "remainder") # 5 + # anything the cap kept in the bond is not this profile's to move + emit(i, "bond", "seller", bal["bond"], "return") + emit(i, "fund", "buyer", bal["fund"], "fund-return") + return out + + def step(self, vtc: dict, trace: list[dict]) -> list[dict]: + """The entries emitted at the last event of `trace`.""" + last = len(trace) - 1 + return [t for t in self.schedule(vtc, trace) if t["event"] == last] + + # -- Section 12.1: the two arithmetic facts over a list -------------------- + def check(self, vtc: dict, transfers: list[dict], terminal: bool) -> tuple[bool, str]: + bal = self.accounts(vtc) + for t in transfers: + amount = _d(t["amount"]) + if amount <= 0: + return False, f"non-positive amount {t['amount']}" + if t["from"] in bal: + if bal[t["from"]] < amount: + return False, f"entry overdraws {t['from']} ({t['code']})" + bal[t["from"]] -= amount + if t["to"] in bal: + bal[t["to"]] += amount + if terminal: + for name, v in bal.items(): + if v != 0: + return False, f"internal account {name} holds {_money(v)} after the last entry" + return True, "ok" + + # -- the bundle's vectors.json ------------------------------------------ + def vectors(self) -> list[dict]: + return json.loads((self.bundle / "vectors.json").read_text()) + + def reproduces(self) -> tuple[bool, str]: + for v in self.vectors(): + got = self.schedule(v["contract"], v["trace"]) + if got != v["transfers"]: + return False, f"vector {v['name']}: schedule differs from the bundle" + ok, why = self.check(v["contract"], got, terminal=v["trace"][-1]["event"] == "terminal") + if not ok: + return False, f"vector {v['name']}: {why}" + return True, "ok" + + +# -------------------------------------------------------------------------- +# Vector generation. The traces here are the canonical paths of the draft's +# figures; timestamps are fixed and the profile never reads them. +# -------------------------------------------------------------------------- + +def _contract(**over: Any) -> dict: + prm = {"seller_bond": "18.00", "verification_fund": "0.50", "cap": "180.00", + "restitution_basis": "released", "remainder_to": "sink", + "principal_on": "verdict", "assurance": {"mode": "certain", "q_min": 1.0}} + prm.update(over.pop("parameters", {})) + c = {"price": {"amount": "180.00", "currency": "USDC"}, + "flow": "verdict-first", "terms": {"profile": ID, "parameters": prm}} + c.update(over) + return c + + +T = ["2026-11-01T10:00:00Z", "2026-11-10T08:30:12Z", "2026-11-10T09:14:30Z", + "2026-11-10T09:40:00Z", "2026-11-10T09:58:05Z", "2026-11-10T10:14:31Z", + "2026-11-14T00:00:01Z"] +H = {k: "sha256:" + k.ljust(64, "0") for k in + ("vtc", "delivery", "verdict1", "verdict2", "challenge")} +CHALLENGER = "did:web:watch.example#k1" +VERIFIER = "did:web:audit.example#k1" + + +def _trace_final() -> list[dict]: + return [ + {"event": "accepted", "at": T[0], "object": H["vtc"]}, + {"event": "funded", "at": T[0]}, + {"event": "delivered", "at": T[1], "object": H["delivery"]}, + {"event": "verdict", "at": T[2], "object": H["verdict1"], "signer": VERIFIER, + "outcome": "PASS"}, + {"event": "window-opened", "at": T[2], "closes_at": "2026-11-10T10:14:30Z"}, + {"event": "window-closed", "at": T[5]}, + {"event": "children-final", "at": T[5]}, + {"event": "terminal", "at": T[5], "state": "FINAL", "challenge_upheld": False}, + ] + + +def _trace_overturned() -> list[dict]: + return [ + {"event": "accepted", "at": T[0], "object": H["vtc"]}, + {"event": "funded", "at": T[0]}, + {"event": "delivered", "at": T[1], "object": H["delivery"]}, + {"event": "verdict", "at": T[2], "object": H["verdict1"], "signer": VERIFIER, + "outcome": "PASS"}, + {"event": "window-opened", "at": T[2], "closes_at": "2026-11-10T10:14:30Z"}, + {"event": "challenge", "at": T[3], "object": H["challenge"], "signer": CHALLENGER, + "costs": {"amount": "1.20", "currency": "USDC"}}, + {"event": "verdict", "at": T[4], "object": H["verdict2"], "signer": VERIFIER, + "outcome": "FAIL", "answers": H["challenge"], "supersedes": H["verdict1"]}, + {"event": "children-final", "at": T[4]}, + {"event": "terminal", "at": T[4], "state": "SETTLED", "challenge_upheld": True}, + ] + + +def _trace_settled_by_verifier() -> list[dict]: + return [ + {"event": "accepted", "at": T[0], "object": H["vtc"]}, + {"event": "funded", "at": T[0]}, + {"event": "delivered", "at": T[1], "object": H["delivery"]}, + {"event": "verdict", "at": T[2], "object": H["verdict1"], "signer": VERIFIER, + "outcome": "FAIL"}, + {"event": "children-final", "at": T[2]}, + {"event": "terminal", "at": T[2], "state": "SETTLED", "challenge_upheld": False}, + ] + + +def _trace_abandoned() -> list[dict]: + return [ + {"event": "accepted", "at": T[0], "object": H["vtc"]}, + {"event": "funded", "at": T[0]}, + {"event": "deadline-passed", "at": T[6]}, + {"event": "children-final", "at": T[6]}, + {"event": "terminal", "at": T[6], "state": "ABANDONED", "challenge_upheld": False}, + ] + + +def _trace_verdict_lapsed() -> list[dict]: + return [ + {"event": "accepted", "at": T[0], "object": H["vtc"]}, + {"event": "funded", "at": T[0]}, + {"event": "delivered", "at": T[1], "object": H["delivery"]}, + {"event": "verdict-lapsed", "at": "2026-11-11T08:30:13Z"}, + {"event": "window-opened", "at": "2026-11-11T08:30:13Z", "closes_at": "2026-11-11T09:30:13Z"}, + {"event": "window-closed", "at": "2026-11-11T09:30:14Z"}, + {"event": "children-final", "at": "2026-11-11T09:30:14Z"}, + {"event": "terminal", "at": "2026-11-11T09:30:14Z", "state": "FINAL", "challenge_upheld": False}, + ] + + +def _trace_delivery_first() -> list[dict]: + return [ + {"event": "accepted", "at": T[0], "object": H["vtc"]}, + {"event": "funded", "at": T[0]}, + {"event": "delivered", "at": T[1], "object": H["delivery"]}, + {"event": "window-opened", "at": T[1], "closes_at": "2026-11-10T09:30:12Z"}, + {"event": "window-closed", "at": "2026-11-10T09:30:13Z"}, + {"event": "children-final", "at": "2026-11-10T09:30:13Z"}, + {"event": "terminal", "at": "2026-11-10T09:30:13Z", "state": "FINAL", "challenge_upheld": False}, + ] + + +def build_vectors(profile: BondedRestitution) -> list[dict]: + cases = [ + ("FINAL: PASS, window closes, Figure 1", _contract(), _trace_final()), + ("SETTLED on an upheld Challenge, Figure 5", _contract(), _trace_overturned()), + ("SETTLED: the Verifier records FAIL", _contract(), _trace_settled_by_verifier()), + ("ABANDONED: deadline with no Delivery", _contract(), _trace_abandoned()), + ("SETTLED on an upheld Challenge, basis price", + _contract(parameters={"restitution_basis": "price"}), _trace_overturned()), + ("FINAL after verdict-lapsed", _contract(), _trace_verdict_lapsed()), + ("FINAL under delivery-first, principal at window-closed", + _contract(flow="delivery-first", parameters={"principal_on": "window-closed"}), + _trace_delivery_first()), + ] + out = [] + for name, contract, trace in cases: + transfers = profile.schedule(contract, trace) + ok, why = profile.check(contract, transfers, terminal=True) + assert ok, f"{name}: {why}" + out.append({"name": name, "contract": contract, "trace": trace, "transfers": transfers}) + return out + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--vectors", action="store_true", help="rewrite vectors.json from the schedule") + ap.add_argument("--hash", action="store_true", help="print profile_hash and exit") + args = ap.parse_args() + profile = BondedRestitution() + if args.vectors: + vectors = build_vectors(profile) + (BUNDLE / "vectors.json").write_text(json.dumps(vectors, indent=2) + "\n") + profile = BondedRestitution() # the hash covers the file just written + print(f"wrote {len(vectors)} vectors; profile_hash {profile.profile_hash}") + return + if args.hash: + print(profile.profile_hash) + return + ok, why = profile.reproduces() + print(f"profile {ID}\nprofile_hash {profile.profile_hash}\nvectors reproduce: {ok} ({why})") + + +if __name__ == "__main__": + main() diff --git a/tools/validate.py b/tools/validate.py index 49eb6c9..98581a2 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -1,568 +1,465 @@ #!/usr/bin/env python3 -"""Validate PACT examples against schemas and verify hash commitments. - -Checks performed: - 1. Every example validates against its JSON Schema. - 2. vtc.task.spec_hash equals sha256(JCS(taskspec.json)). - 3. vtc verification.criteria_hash equals the acceptance-instrument - digest: sha256(JCS({relative path: sha256(bytes)})) over - examples/acceptance-harness/. - 4. taskspec.acceptance.harness_hash equals that same digest, so the - instrument is committed from inside the TaskSpec as well as by the - VTC. - 5. delivery_hash in Verdict and Challenge equals sha256(JCS(delivery)) - with the signature member included, matching facilitator.py; the - v0.1.0 validator excluded it and the two tools disagreed. - 6. vtc_hash in Delivery, Verdict, Challenge and Attestation equals - sha256(JCS(vtc)) with the signatures member included (-01 Section 6). - 7. Rules the schemas cannot express: parties are distinct, one - signature per named party, and every JOSE protected header carries - alg, kid and typ with an allowed algorithm. - 8. Canonicalization: jcs() orders object keys the way RFC 8785 - requires, which is not the way json.dumps(sort_keys=True) does. - 9. The assurance constraint of -01 Section 7.2, on the worked figures - carried in -01 Section 14. - 10. Negative vectors, including the Section 13.3 conformance vectors - that JSON Schema cannot express. - 11. Signature sets and ECDSA encoding, ahead of the text (facilitator - CHOICES C9): the P-256 and P-384 orders behind the low-S rule are - proved by computing n*G, an unsorted signature set is rejected - (V-21) and a high-S signature is rejected (V-22). - -Caveat on canonicalization: jcs() below is a restricted implementation of -RFC 8785, correct for the value types these examples use (strings, -integers, floats with exact short decimal representations, booleans, -nulls, and nested objects and arrays of those). - -Object keys are sorted by UTF-16 code unit, as RFC 8785 section 3.2.3 -requires. This is worth stating because the obvious shortcut is wrong: -json.dumps(sort_keys=True) sorts by Unicode code point, and code point -order agrees with UTF-16 order throughout the Basic Multilingual Plane -and diverges above it, where UTF-16 encodes a key as a surrogate pair -beginning U+D800 and therefore sorts it below keys in U+E000..U+FFFF. -An implementation carrying that shortcut passes an ASCII or BMP vector by -accident and fails on a supplementary-plane key. Check 8 pins the case. - -It remains a restricted implementation and not a conforming general RFC -8785 one: in particular it does not implement the ECMAScript number -serialization rules for the full float range. A passing run therefore -evidences self-consistency of these examples, not canonicalization -interoperability with another implementation. -""" -import json, hashlib, sys, pathlib, base64 -import pactcore as pc # identifier normalization, the exact constraint, signature rules -from jsonschema import Draft202012Validator -from referencing import Registry, Resource +"""Check the committed examples against draft-laxsharma-pact-02. -ROOT = pathlib.Path(__file__).resolve().parent.parent -fails = [] +Every value the draft prints comes from examples/ and profiles/, and this +program is how the repository knows those files still say what the document +says. It checks each object against its schema, recomputes every digest the +objects commit to, verifies every signature with the public keys in +examples/keys/, replays the profile's vectors, and runs the negative +conformance vectors of Section 14.3 through the reference Facilitator. -ALLOWED_ALGS = {"ES256", "ES384", "EdDSA"} + python3 tools/validate.py +Needs jsonschema and referencing; needs cryptography for the signature +section and the vectors that go through the Facilitator (those print [skip] +without it and are not counted). +""" -def _utf16_key_order(obj): - """Recursively reorder object keys by UTF-16 code unit (RFC 8785 3.2.3). - - Comparing UTF-16 big-endian encodings bytewise is equivalent to - comparing sequences of UTF-16 code units, which is what the RFC - specifies. json.dumps preserves dict insertion order, so building the - dict in the right order is enough; sort_keys must NOT also be set, - since that would re-sort by code point. - """ - if isinstance(obj, dict): - return {k: _utf16_key_order(obj[k]) - for k in sorted(obj, key=lambda s: s.encode("utf-16-be"))} - if isinstance(obj, list): - return [_utf16_key_order(v) for v in obj] - return obj +from __future__ import annotations +import copy +import hashlib +import json +import pathlib +import sys -def jcs(obj) -> bytes: - # Restricted JCS (RFC 8785); see the caveat in the module docstring. - return json.dumps(_utf16_key_order(obj), separators=(",", ":"), - ensure_ascii=False).encode() - - -def h(b: bytes) -> str: - return "sha256:" + hashlib.sha256(b).hexdigest() - - -def load(p): - return json.loads((ROOT / p).read_text()) - - -def check(name, cond, detail=""): - status = "ok " if cond else "FAIL" - print(f"[{status}] {name}" + (f" ({detail})" if detail and not cond else "")) - if not cond: - fails.append(name) - - -def instrument_digest(dirpath: pathlib.Path) -> str: - """Digest of an acceptance instrument bundle. - - A manifest of per-file digests rather than an archive digest, so the - commitment does not depend on tar or zip metadata (ordering, - timestamps, permissions), which is not stable across producers. - """ - manifest = {} - for p in sorted(dirpath.rglob("*")): - if p.is_file(): - manifest[p.relative_to(dirpath).as_posix()] = h(p.read_bytes()) - return h(jcs(manifest)) - - -def b64url_decode(s: str) -> dict: - pad = "=" * (-len(s) % 4) - return json.loads(base64.urlsafe_b64decode(s + pad)) - - -# --- schema registry (local $ref resolution) --- -registry = Registry() -for sp in (ROOT / "schemas").glob("*.schema.json"): - sch = json.loads(sp.read_text()) - registry = registry.with_resource(sp.name, Resource.from_contents(sch)) - registry = registry.with_resource(sch["$id"], Resource.from_contents(sch)) - - -def validator_for(schema_file): - sch = json.loads((ROOT / "schemas" / schema_file).read_text()) - return Draft202012Validator(sch, registry=registry) - - -def validate(example, schema_file, quiet=False): - v = validator_for(schema_file) - errs = sorted(v.iter_errors(example), key=lambda e: e.path) - if not quiet: - for e in errs: - print(" ", "/".join(map(str, e.path)), "-", e.message) - return not errs - - -ts = load("examples/taskspec.json") -vtc = load("examples/vtc.json") -dlv = load("examples/delivery.json") -vdt = load("examples/verdict.json") -att = load("examples/attestation.json") -fac = load("examples/well-known/pact-facilitator.json") -chl = load("examples/challenge.json") -harness_digest = instrument_digest(ROOT / "examples/acceptance-harness") - -print("== schema conformance ==") -check("taskspec matches schema", validate(ts, "taskspec.schema.json")) -check("vtc matches schema", validate(vtc, "vtc.schema.json")) -check("delivery matches schema", validate(dlv, "delivery.schema.json")) -check("verdict matches schema", validate(vdt, "verdict.schema.json")) -check("attestation matches schema", validate(att, "attestation.schema.json")) -check("facilitator matches schema", validate(fac, "facilitator.schema.json")) -check("challenge matches schema", validate(chl, "challenge.schema.json")) - -print() -print("== canonicalization ==") - -# RFC 8785 section 3.2.3 orders object keys by UTF-16 code unit. U+E000 -# is below U+10000 by code point, and above it by UTF-16 code unit, since -# U+10000 encodes as the surrogate pair D800 DC00. A canonicalizer built -# on json.dumps(sort_keys=True) gets this backwards and no ASCII vector -# will reveal it. -_supp = {"\ue000": 1, "\U00010000": 2} -_want = ('{"' + "\U00010000" + '":2,"' + "\ue000" + '":1}').encode() -check("JCS orders keys by UTF-16 code unit, not code point", - jcs(_supp) == _want, f"got {jcs(_supp)!r}, want {_want!r}") - -# The same rule has to hold at every depth, not just at the root. -_nested = {"z": [{"\ue000": 1, "\U00010000": 2}]} -_want_nested = ('{"z":[{"' + "\U00010000" + '":2,"' + "\ue000" + '":1}]}').encode() -check("JCS key order applies inside nested objects and arrays", - jcs(_nested) == _want_nested, - f"got {jcs(_nested)!r}, want {_want_nested!r}") - -print() -print("== hash commitments ==") -check("vtc.spec_hash == sha256(JCS(taskspec))", - vtc["task"]["spec_hash"] == h(jcs(ts))) -check("vtc.criteria_hash == instrument digest", - vtc["verification"]["criteria_hash"] == harness_digest) -check("taskspec.acceptance.harness_hash == instrument digest", - ts["acceptance"]["harness_hash"] == harness_digest) - -# -01 Section 6: vtc_hash covers the contract INCLUDING its signature set, -# so the commitment proves who agreed and not merely what was written. -# The -00 excluded signatures, which let entries be appended or stripped -# without invalidating the commitment. -vtc_hash = h(jcs(vtc)) -check("delivery.vtc_hash == sha256(JCS(vtc, signatures included))", - dlv["vtc_hash"] == vtc_hash) -check("attestation.vtc_hash == sha256(JCS(vtc, signatures included))", - att["vtc_hash"] == vtc_hash) -check("verdict.delivery_hash == sha256(JCS(delivery, signature included))", - vdt["delivery_hash"] == h(jcs(dlv))) -check("delivery.evidence.instrument_hash == vtc.criteria_hash", - dlv["evidence"]["instrument_hash"] == vtc["verification"]["criteria_hash"]) -check("verdict.instrument_hash == vtc.criteria_hash", - vdt["instrument_hash"] == vtc["verification"]["criteria_hash"]) -check("challenge.delivery_hash == sha256(JCS(delivery, signature included))", - chl["delivery_hash"] == h(jcs(dlv))) -check("challenge.proof.instrument_hash == vtc.criteria_hash", - chl["proof"]["instrument_hash"] == vtc["verification"]["criteria_hash"]) - -print() -print("== rules the schemas cannot express ==") - -check("vtc parties are distinct", - vtc["parties"]["buyer"] != vtc["parties"]["seller"]) - -def signer_kids(obj): - out = [] - for s in obj.get("signatures", []): - try: - out.append(b64url_decode(s["protected"]).get("kid", "")) - except Exception: - out.append("") - return out - -def party_covered(kids, did): - return any(k.split("#", 1)[0] == did for k in kids) - -vtc_kids = signer_kids(vtc) -check("vtc carries one signature per named party", - party_covered(vtc_kids, vtc["parties"]["buyer"]) - and party_covered(vtc_kids, vtc["parties"]["seller"]) - and len(vtc_kids) == len({k.split("#", 1)[0] for k in vtc_kids})) - -def headers_well_formed(obj, typ): - for s in obj.get("signatures", []): +ROOT = pathlib.Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "tools")) + +import pactcore as pc # noqa: E402 +import profile as terms # noqa: E402 + +try: + import cryptography # noqa: F401 + HAVE_CRYPTO = True +except ImportError: + HAVE_CRYPTO = False + +from jsonschema import Draft202012Validator # noqa: E402 +from referencing import Registry, Resource # noqa: E402 + +EX = ROOT / "examples" +MEDIA = { + "vtc": "application/vnd.pact.contract+json", + "delivery": "application/vnd.pact.delivery+json", + "verdict": "application/vnd.pact.verdict+json", + "challenge": "application/vnd.pact.challenge+json", + "status": "application/vnd.pact.status+json", + "outcome": "application/vnd.pact.outcome+json", + "facilitator": "application/vnd.pact.facilitator+json", +} + + +class Report: + def __init__(self) -> None: + self.passed = 0 + self.failed = 0 + self.skipped = 0 + self.sections: list[tuple[str, int]] = [] + self._current = None + self._count = 0 + + def section(self, title: str) -> None: + if self._current is not None: + self.sections.append((self._current, self._count)) + self._current, self._count = title, 0 + print(f"\n{title}") + + def check(self, cond: bool, label: str, detail: str = "") -> bool: + self._count += 1 + if cond: + self.passed += 1 + print(f" [ok] {label}") + else: + self.failed += 1 + print(f" [FAIL] {label}" + (f": {detail}" if detail else "")) + return bool(cond) + + def skip(self, label: str) -> None: + self.skipped += 1 + print(f" [skip] {label} (needs cryptography)") + + def done(self) -> int: + if self._current is not None: + self.sections.append((self._current, self._count)) + total = self.passed + self.failed + print(f"\n{total} checks: {self.passed} passed, {self.failed} failed" + + (f", {self.skipped} skipped" if self.skipped else "")) + print(" " + "; ".join(f"{n} {t}" for t, n in self.sections)) + return 1 if self.failed else 0 + + +def load(name: str) -> dict: + return json.loads((EX / name).read_text()) + + +def main() -> int: + r = Report() + taskspec = load("taskspec.json") + vtc = load("vtc.json") + delivery = load("delivery.json") + verdict = load("verdict.json") + challenge = load("challenge.json") + verdict2 = load("verdict-on-challenge.json") + status = load("status.json") + outcome = load("outcome.json") + facdoc = load("well-known/pact-facilitator.json") + keys = load("keys/public-keys.json") + prof = terms.BondedRestitution() + + # -- 1. schemas --------------------------------------------------------- + r.section("schema conformance") + docs = {p.name: json.loads(p.read_text()) for p in (ROOT / "schemas").glob("*.schema.json")} + registry = Registry().with_resources([(n, Resource.from_contents(s)) for n, s in docs.items()]) + registry = registry.with_resources([(s["$id"], Resource.from_contents(s)) for s in docs.values()]) + validators = {n: Draft202012Validator(s, registry=registry) for n, s in docs.items()} + + def conforms(obj, schema) -> tuple[bool, str]: + errs = sorted(validators[schema].iter_errors(obj), key=lambda e: list(e.path)) + if not errs: + return True, "" + e = errs[0] + return False, f"{e.message[:120]} at /{'/'.join(map(str, e.path))}" + + for name, obj, schema in [("taskspec.json", taskspec, "taskspec.schema.json"), + ("vtc.json", vtc, "vtc.schema.json"), + ("delivery.json", delivery, "delivery.schema.json"), + ("verdict.json", verdict, "verdict.schema.json"), + ("challenge.json", challenge, "challenge.schema.json"), + ("verdict-on-challenge.json", verdict2, "verdict.schema.json"), + ("status.json", status, "status.schema.json"), + ("outcome.json", outcome, "outcome.schema.json"), + ("well-known/pact-facilitator.json", facdoc, "facilitator.schema.json")]: + ok, why = conforms(obj, schema) + r.check(ok, f"{name} validates against {schema}", why) + r.check(prof.parameters_error(vtc["terms"]["parameters"]) is None, + "vtc.terms.parameters validate against the profile's parameters.schema.json") + + # -- 2. canonicalization ----------------------------------------------- + r.section("canonicalization (RFC 8785)") + r.check(pc.jcs({"b": 1, "a": [1, 2], "aa": "x"}) == b'{"a":[1,2],"aa":"x","b":1}', + "members sorted, no whitespace") + r.check(pc.jcs({"q": 1.0, "n": 180, "s": "é
"}) == '{"n":180,"q":1,"s":"é
"}'.encode(), + "V-25 ES6 number formatting (the float 1.0 serializes as 1) and raw non-ASCII") + r.check(pc.jcs([1e21, 1e20, 1e-7, 0.000001, 0.5, -0.0, 123.456, 5e-324]) == + b"[1e+21,100000000000000000000,1e-7,0.000001,0.5,0,123.456,5e-324]", + "V-25 at the edges: exponent form only above 1e21 and below 1e-6, negative zero as 0") + r.check(pc.jcs({"\U0001F600": 1, "fi": 2}) == '{"\U0001F600":1,"fi":2}'.encode(), + "keys ordered by UTF-16 code units, not code points (V-18)") + + # -- 3. digests --------------------------------------------------------- + r.section("hash commitments") + spec_hash = pc.digest_over(taskspec) + r.check(vtc["task"]["spec_hash"] == spec_hash, "task.spec_hash is the digest over taskspec.json") + criteria = pc.manifest_digest(EX / "acceptance-harness") + r.check(vtc["verification"]["criteria_hash"] == criteria, + "verification.criteria_hash is the manifest digest over examples/acceptance-harness/") + r.check(taskspec["acceptance"]["harness_hash"] == criteria, + "taskspec.acceptance.harness_hash is the same manifest digest") + for member, path in (("inputs.schema_hash", "customers.schema.json"), + ("inputs.sample_hash", "sample-10k.csv"), + ("deliverable.schema_hash", "output.schema.json")): + a, b = member.split(".") + r.check(taskspec[a][b] == pc.h((EX / "task-content" / path).read_bytes()), + f"taskspec.{member} is the digest of task-content/{path}") + profile_hash = pc.manifest_digest(ROOT / "profiles" / "bonded-restitution") + r.check(prof.profile_hash == profile_hash, "the profile's hash is the manifest digest over its bundle") + r.check(vtc["terms"]["profile_hash"] == profile_hash, "vtc.terms.profile_hash commits to that bundle") + r.check(any(p["id"] == terms.ID and p["profile_hash"] == profile_hash for p in facdoc["terms_profiles"]), + "the capability document advertises the same profile and hash") + r.check(outcome["terms_result"]["profile_hash"] == profile_hash and outcome["terms_result"]["profile"] == terms.ID, + "outcome.terms_result names the same profile and hash") + vtc_hash = pc.digest_over(vtc) + r.check(all(o["vtc_hash"] == vtc_hash for o in (delivery, status, outcome)), + "vtc_hash in delivery, status and outcome is the digest over the signed contract") + delivery_hash = pc.digest_over(delivery) + r.check(all(o["delivery_hash"] == delivery_hash for o in (verdict, challenge, verdict2)), + "delivery_hash in both Verdicts and the Challenge covers the Delivery's signature") + r.check(pc.digest_over({k: v for k, v in delivery.items() if k != "signature"}) != delivery_hash, + "a digest over the unsigned Delivery is a different value (V-23)") + r.check(verdict2["challenge_hash"] == pc.digest_over(challenge), + "verdict-on-challenge.challenge_hash is the digest over the Challenge") + tr = outcome["trace"] + r.check(tr[0]["object"] == vtc_hash and tr[2]["object"] == delivery_hash, + "trace entries accepted and delivered carry the contract and Delivery digests") + r.check(tr[3]["object"] == pc.digest_over(verdict) and tr[5]["object"] == pc.digest_over(challenge), + "trace entries verdict and challenge carry the object digests") + r.check(tr[6]["object"] == pc.digest_over(verdict2) and tr[6]["answers"] == pc.digest_over(challenge) + and tr[6]["supersedes"] == pc.digest_over(verdict), + "the second verdict entry answers the Challenge and supersedes the first Verdict") + r.check(outcome["work_hash"] == delivery["work_hash"] and verdict["results_hash"] == delivery["evidence"]["results_hash"], + "work_hash and results_hash carry through unchanged") + + # -- 4. rules ----------------------------------------------------------- + r.section("rules of the document") + parties = vtc["parties"] + r.check(not pc.same_party(parties["buyer"], parties["seller"]), "buyer and seller are distinct after normalization") + r.check(pc.norm("did:web:a.example:agents:X") != pc.norm("did:web:a.example:agents:x"), + "normalization keeps path case (V-19)") + r.check(pc.norm("https://a.example/") == pc.norm("https://A.example") == pc.norm(" https://a.example# "), + "normalization folds scheme and host, strips fragment, trailing slash and whitespace (V-07)") + ok, why = pc.signatures_ordered(vtc) + r.check(ok, "contract signatures are sorted by normalized kid", why) + kids = pc.signer_kids(vtc) + r.check(len(kids) == 2 and any(pc.kid_covers(k, parties["buyer"]) for k in kids) + and any(pc.kid_covers(k, parties["seller"]) for k in kids), + "exactly one kid covers the buyer and one the seller") + + def headers_ok(obj, typ) -> tuple[bool, str]: + for entry in pc.signature_entries(obj): + hdr = json.loads(pc.b64u_decode(entry["protected"])) + if hdr.get("alg") not in pc.ALLOWED_ALGS: + return False, f"alg {hdr.get('alg')}" + if "kid" not in hdr or hdr.get("typ") != typ: + return False, f"typ {hdr.get('typ')}" + return True, "" + for name, obj, typ in (("vtc", vtc, "vtc"), ("delivery", delivery, "delivery"), ("verdict", verdict, "verdict"), + ("challenge", challenge, "challenge"), ("verdict-on-challenge", verdict2, "verdict"), + ("status", status, "status"), ("outcome", outcome, "outcome"), + ("facilitator document", facdoc, "facilitator")): + ok, why = headers_ok(obj, MEDIA[typ]) + r.check(ok, f"{name}: protected header carries an allowed alg, a kid and typ {MEDIA[typ]}", why) + r.check(all(pc.kid_covers(k, parties["verifier"]) for k in pc.signer_kids(verdict) + pc.signer_kids(verdict2)), + "both Verdicts are signed by the named verifier") + r.check(not any(pc.kid_covers(k, parties["seller"]) for k in pc.signer_kids(challenge)), + "the Challenge is not signed by the seller") + r.check(all(pc.kid_covers(k, parties["facilitator"]) for o in (status, outcome, facdoc) for k in pc.signer_kids(o)), + "Status, Outcome Record and capability document are signed by the Facilitator") + r.check(status["trace"] == outcome["trace"][:len(status["trace"])], + "the Status trace is a prefix of the Outcome Record trace (Section 11)") + r.check(tr[-1]["event"] == "terminal" and tr[-1]["state"] == outcome["outcome"]["state"] == "SETTLED", + "the terminal entry is last and names the recorded state") + standing = [e for e in tr if e["event"] == "verdict"][-1] + r.check(standing["outcome"] == "FAIL" and ("answers" in standing) == outcome["outcome"]["challenge_upheld"], + "challenge_upheld is true exactly when the standing FAIL answers a Challenge") + r.check(status["state"] == "WINDOW_OPEN" and status["trace"][-1]["event"] == "window-opened", + "the example Status is the window-opened moment") + + # -- 5. signatures ------------------------------------------------------- + r.section("signature verification") + resolver = None + if HAVE_CRYPTO: + resolver = pc.KeyResolver() + for role, jwk in keys.items(): + resolver.register(pc.Key.from_public_bytes(jwk["kid"], "EdDSA", pc.b64u_decode(jwk["x"]))) + for name, obj, typ, required in ( + ("vtc", vtc, "vtc", [parties["buyer"], parties["seller"]]), + ("delivery", delivery, "delivery", [parties["seller"]]), + ("verdict", verdict, "verdict", [parties["verifier"]]), + ("challenge", challenge, "challenge", []), + ("verdict-on-challenge", verdict2, "verdict", [parties["verifier"]]), + ("status", status, "status", [parties["facilitator"]]), + ("outcome", outcome, "outcome", [parties["facilitator"]]), + ("facilitator document", facdoc, "facilitator", [parties["facilitator"]])): + ok, why = pc.verify_object(obj, resolver, MEDIA[typ], required) + r.check(ok, f"{name}: every signature verifies with examples/keys/public-keys.json", why) + r.check(pc.digest_over(vtc) == vtc_hash and pc.signable(vtc) == {k: v for k, v in vtc.items() if k != "signatures"}, + "the signing input excludes the signature set and the digest includes it") + else: + for _ in range(9): + r.skip("signature verification") + + # -- 6. the terms profile ---------------------------------------------- + r.section("terms profile: bonded-restitution") + ok, why = prof.reproduces() + r.check(ok, "profiles/bonded-restitution/vectors.json reproduces from the schedule", why) + vecs = prof.vectors() + + def tup(ts): + return [(t["event"], t["from"], t["to"], t["amount"], t["code"]) for t in ts] + final_printed = [(1, "buyer", "escrow", "180.00", "lock"), (1, "seller", "bond", "18.00", "bond"), + (1, "buyer", "fund", "0.50", "fund"), (3, "escrow", "seller", "180.00", "principal"), + (7, "bond", "seller", "18.00", "return"), (7, "fund", "buyer", "0.50", "fund-return")] + overturned_printed = [(1, "buyer", "escrow", "180.00", "lock"), (1, "seller", "bond", "18.00", "bond"), + (1, "buyer", "fund", "0.50", "fund"), (3, "escrow", "seller", "180.00", "principal"), + (8, "fund", "challenger:did:web:watch.example#k1", "0.50", "costs"), + (8, "bond", "buyer", "18.00", "restitution")] + r.check(tup(vecs[0]["transfers"]) == final_printed, "Appendix A.6, the FINAL list, is vector 1 verbatim") + r.check(tup(vecs[1]["transfers"]) == overturned_printed, "Appendix A.6, the overturned list, is vector 2 verbatim") + r.check(outcome["terms_result"]["transfers"] == prof.schedule(vtc, outcome["trace"]), + "outcome.terms_result.transfers is the schedule over the example trace") + ok, why = prof.check(vtc, outcome["terms_result"]["transfers"], terminal=True) + r.check(ok, "the example result satisfies no-overdraft and closure", why) + try: + prof.admit(vtc) + r.check(True, "the example contract passes the admission rule") + except terms.ProfileRefusal as exc: + r.check(False, "the example contract passes the admission rule", exc.detail) + + def admits(bond: str, q: float) -> bool: + v = copy.deepcopy(vtc) + v["terms"]["parameters"]["seller_bond"] = bond + v["terms"]["parameters"]["assurance"]["q_min"] = q try: - hdr = b64url_decode(s["protected"]) - except Exception: - return False - if hdr.get("alg") not in ALLOWED_ALGS: - return False - if not hdr.get("kid"): - return False - if hdr.get("typ") != typ: + prof.admit(v) + return True + except terms.ProfileRefusal: return False - return True - -check("vtc protected headers carry alg/kid/typ", - headers_well_formed(vtc, "application/pact-contract+json")) -check("delivery protected header carries alg/kid/typ", - headers_well_formed({"signatures": [dlv["signature"]]}, - "application/pact-delivery+json")) -check("verdict protected header carries alg/kid/typ", - headers_well_formed({"signatures": [vdt["signature"]]}, - "application/pact-verdict+json")) -check("attestation protected headers carry alg/kid/typ", - headers_well_formed(att, "application/pact-attestation+json")) -check("challenge protected header carries alg/kid/typ", - headers_well_formed({"signatures": [chl["signature"]]}, - "application/pact-challenge+json")) -check("facilitator protected header carries alg/kid/typ", - headers_well_formed({"signatures": [fac["signature"]]}, - "application/pact-facilitator+json")) - -# Section 9.1: identifiers are normalized before comparison, and the -# normalization folds toward "same party". A trailing separator, a case -# variant, or surrounding whitespace must not make one party look like two. -# The function is pactcore's, so this validator and the Facilitator cannot -# disagree about who is who. The copy that lived here folded the whole -# identifier, which merges two did:web paths that differ only in case. -norm = pc.norm - - -check("party comparison normalizes trailing separators and case", - norm("did:web:X.example/") == norm("did:web:x.example")) -check("normalized parties in the example are still distinct", - norm(vtc["parties"]["buyer"]) != norm(vtc["parties"]["seller"])) -check("did:web path case is not folded: Section 9.1 folds scheme and host only", - norm("did:web:x.example:agents:A") != norm("did:web:x.example:agents:a")) - -print() -print("== the assurance constraint (Section 7.2) ==") - - -# B >= P(1-q)/q + E, evaluated exactly by pactcore.assurance_holds: multiplied -# through by q, so no division and no rounding. The float copy that lived here -# carried a 1e-9 slack and passed a bond a hundredth of a cent short. -def holds(contract, released="0"): - return pc.assurance_holds(contract["price"]["amount"], - contract["liability"]["seller_bond"], - contract["assurance"]["q_min"], released) - - -check("example contract satisfies the assurance constraint", holds(vtc)) -check("q_min 0.9091 requires B = 18.00 at P=180, so 17.99 fails", - pc.assurance_holds("180.00", "18.00", "0.9091") - and not pc.assurance_holds("180.00", "17.99", "0.9091")) - -# Worked figures from Section 14. P = 180.00, B = 18.00, E = 0. Each bound is -# checked from both sides, one cent apart. -check("q_min 1.00 requires no bond at P=180", - pc.assurance_holds("180.00", "0.00", "1.00")) -check("q_min 0.90 requires B = 20.00 at P=180, so 19.99 fails", - pc.assurance_holds("180.00", "20.00", "0.90") - and not pc.assurance_holds("180.00", "19.99", "0.90")) -check("q_min 0.50 requires B = 180.00 at P=180, so 179.99 fails", - pc.assurance_holds("180.00", "180.00", "0.50") - and not pc.assurance_holds("180.00", "179.99", "0.50")) - -# Section 7.2: open assurance may not be the sole declared source. -check("'open' is not the example's sole source of assurance", - vtc["assurance"]["mode"] != "open") - -# Section 11: a null children_merkle_root is indistinguishable from a -# withheld subtree, so it must be omitted rather than nulled. -check("attestation omits children_merkle_root rather than nulling it", - att.get("children_merkle_root", "absent") != None) - -# Section 11: the facilitator signature is what makes the record evidence. -fac_kid = att["parties"]["facilitator"] -check("attestation carries a facilitator signature", - any(fac_kid in b64url_decode(sg["protected"]).get("kid", "") - for sg in att["signatures"])) -check("attestation subject appears in parties", - att["subject"] in att["parties"].values()) - -print() -print("== children Merkle root (Section 11.1, RFC 9162 Section 2.1.1) ==") - - -def mth(D): - """Merkle Tree Hash exactly as RFC 9162 Section 2.1.1 defines it (identical - to the RFC 6962 definition it obsoletes), with SHA-256 as the hash. - - MTH({}) = SHA-256() - MTH({d}) = SHA-256(0x00 || d) - MTH(D[n]) = SHA-256(0x01 || MTH(D[0:k]) || MTH(D[k:n])), - k the largest power of two smaller than n. - Leaves and interior nodes carry distinct prefixes; that domain - separation is what gives second-preimage resistance. - """ - if len(D) == 0: - return hashlib.sha256(b"").digest() - if len(D) == 1: - return hashlib.sha256(b"\x00" + D[0]).digest() - k = 1 - while k * 2 < len(D): - k *= 2 - return hashlib.sha256(b"\x01" + mth(D[:k]) + mth(D[k:])).digest() - - -_d = [hashlib.sha256(bytes([i])).digest() for i in range(8)] -_leaf = lambda x: hashlib.sha256(b"\x00" + x).digest() -_node = lambda a, b: hashlib.sha256(b"\x01" + a + b).digest() -check("n=1: root is the domain-separated leaf hash", - mth(_d[:1]) == _leaf(_d[0])) -check("leaf and interior prefixes differ (domain separation)", - _leaf(_d[0]) != hashlib.sha256(b"\x01" + _d[0]).digest()) -check("n=2: root = H(0x01 || leaf(d0) || leaf(d1))", - mth(_d[:2]) == _node(_leaf(_d[0]), _leaf(_d[1]))) -check("n=3: split at k=2, lone third leaf is not promoted unchanged", - mth(_d[:3]) == _node(mth(_d[:2]), _leaf(_d[2]))) -# The seven-leaf tree of RFC 6962 Section 2.1.3, unchanged in RFC 9162: hash = H(k, l), k = H(g, h), -# l = H(i, j), j = leaf(d6). Reproduce that shape exactly. -_g = _node(_leaf(_d[0]), _leaf(_d[1])); _h_ = _node(_leaf(_d[2]), _leaf(_d[3])) -_i = _node(_leaf(_d[4]), _leaf(_d[5])); _j = _leaf(_d[6]) -check("n=7: matches the seven-leaf figure, k=4 then k=2", - mth(_d[:7]) == _node(_node(_g, _h_), _node(_i, _j))) -check("root changes if leaf order changes", - mth(_d[:4]) != mth(list(reversed(_d[:4])))) - -print() -print("== negative vectors (these MUST be rejected) ==") - -def rejects(name, schema_file, mutate): - doc = json.loads(json.dumps(load(mutate[0]))) - mutate[1](doc) - check(name, not validate(doc, schema_file, quiet=True)) - -rejects("empty acceptance object is rejected", "taskspec.schema.json", - ("examples/taskspec.json", lambda d: d.__setitem__("acceptance", {}))) - -rejects("acceptance without thresholds is rejected", "taskspec.schema.json", - ("examples/taskspec.json", lambda d: d["acceptance"].pop("thresholds"))) - -rejects("harness_uri without harness_hash is rejected", "taskspec.schema.json", - ("examples/taskspec.json", lambda d: d["acceptance"].pop("harness_hash"))) - -rejects("zero-length challenge window is rejected", "vtc.schema.json", - ("examples/vtc.json", - lambda d: d["challenge"].__setitem__("window_seconds", 0))) - -rejects("signature with a bare kid sibling is rejected", "vtc.schema.json", - ("examples/vtc.json", - lambda d: d["signatures"][0].__setitem__("kid", "did:web:evil.example#k1"))) - -rejects("single-signature VTC is rejected", "vtc.schema.json", - ("examples/vtc.json", lambda d: d.__setitem__("signatures", - d["signatures"][:1]))) - -rejects("malformed money value is rejected", "vtc.schema.json", - ("examples/vtc.json", - lambda d: d["price"].__setitem__("amount", "195"))) - -rejects("non-sha256 hash value is rejected", "vtc.schema.json", - ("examples/vtc.json", - lambda d: d["task"].__setitem__("spec_hash", "deadbeef"))) - -# A self-dealt contract still validates against the schema, which is why -# the distinctness rule above is enforced in code. Assert that the code -# check catches what the schema cannot. -self_dealt = json.loads(json.dumps(vtc)) -self_dealt["parties"]["seller"] = self_dealt["parties"]["buyer"] -check("self-dealt contract passes schema but fails the code check", - validate(self_dealt, "vtc.schema.json", quiet=True) - and self_dealt["parties"]["buyer"] == self_dealt["parties"]["seller"]) - -# --- Section 13.3 vectors that need code, not schema --- - - -_none = json.loads(json.dumps(vtc)) -_none["signatures"][0]["protected"] = "eyJhbGciOiJub25lIiwia2lkIjoiZGlkOndlYjpidXllci5leGFtcGxlOmFnZW50czpwcm9jdXJlLTEjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi9wYWN0LWNvbnRyYWN0K2pzb24ifQ" -check("V-02 alg 'none' is rejected", - not headers_well_formed(_none, "application/pact-contract+json")) - -_hs = json.loads(json.dumps(vtc)) -_hs["signatures"][0]["protected"] = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImRpZDp3ZWI6YnV5ZXIuZXhhbXBsZTphZ2VudHM6cHJvY3VyZS0xI2sxIiwidHlwIjoiYXBwbGljYXRpb24vcGFjdC1jb250cmFjdCtqc29uIn0" -check("V-03 symmetric alg HS256 is rejected", - not headers_well_formed(_hs, "application/pact-contract+json")) - -_typ = json.loads(json.dumps(vtc)) -_typ["signatures"][0]["protected"] = "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6YnV5ZXIuZXhhbXBsZTphZ2VudHM6cHJvY3VyZS0xI2sxIiwidHlwIjoiYXBwbGljYXRpb24vcGFjdC1kZWxpdmVyeStqc29uIn0" -check("V-05 signature typed for another object is rejected", - not headers_well_formed(_typ, "application/pact-contract+json")) - -_alias = json.loads(json.dumps(vtc)) -_alias["parties"]["seller"] = _alias["parties"]["buyer"] + "/" -check("V-07 parties differing only by a trailing '/' are rejected", - norm(_alias["parties"]["buyer"]) == norm(_alias["parties"]["seller"])) - -_q = json.loads(json.dumps(vtc)) -_q["assurance"] = {"mode": "committed-sample", "q_min": 0.90} -check("V-12 B=18.00 at P=180.00 with q_min 0.90 fails the constraint", - not holds(_q)) - -_q2 = json.loads(json.dumps(vtc)) -_q2["assurance"] = {"mode": "certain", "q_min": 1.00} -check("V-13 B=18.00 at P=180.00 with q_min 1.00 satisfies it", - holds(_q2)) - -_noev = {k: v for k, v in dlv.items() if k != "evidence"} -check("V-14 delivery without evidence is rejected", - not validate(_noev, "delivery.schema.json", quiet=True)) - -_selfv = json.loads(json.dumps(vdt)) -_selfv["signature"]["protected"] = "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6ZGF0YWZvcmdlLmV4YW1wbGU6YWdlbnRzOmV0bC0zI2sxIiwidHlwIjoiYXBwbGljYXRpb24vcGFjdC12ZXJkaWN0K2pzb24ifQ" -_signer = b64url_decode(_selfv["signature"]["protected"])["kid"] -check("V-17 verdict signed by the seller is not independent", - norm(_signer.split("#")[0]) == norm(vtc["parties"]["seller"])) - - -# Section 10.3: a child must be able to finalise inside its parent. -def finality_ok(child, parent): - from datetime import datetime - f = "%Y-%m-%dT%H:%M:%SZ" - c_end = (datetime.strptime(child["task"]["deadline"], f).timestamp() - + child["challenge"]["window_seconds"] - + child["challenge"]["max_dispute_seconds"]) - p_end = (datetime.strptime(parent["task"]["deadline"], f).timestamp() - + parent["challenge"]["window_seconds"]) - return c_end < p_end - - -_child_bad = json.loads(json.dumps(vtc)) -check("V-16 child finalising after the parent's window closes is rejected", - not finality_ok(_child_bad, vtc)) - -_child_ok = json.loads(json.dumps(vtc)) -_child_ok["task"]["deadline"] = "2026-07-25T00:00:00Z" -_child_ok["challenge"] = {"window_seconds": 600, "max_dispute_seconds": 3600} -check("a child that finalises inside the parent's window is accepted", - finality_ok(_child_ok, vtc)) - -_child_parent = json.loads(json.dumps(vtc)) -_child_parent["liability"]["parent"] = {"vtc_id": vtc["id"], - "vtc_hash": vtc_hash} -check("V-15 child whose buyer is not the parent's seller is rejected", - norm(_child_parent["parties"]["buyer"]) != norm(vtc["parties"]["seller"])) - - -# Section 2: an unknown version and an unknown member are both rejected. -_v = json.loads(json.dumps(vtc)); _v["pact"] = "9.9" -check("V-19 unimplemented pact version is rejected", - _v["pact"] not in ("0.1",)) -_u = json.loads(json.dumps(vtc)); _u["extension"] = True -check("V-20 object carrying an undefined member is rejected", - not validate(_u, "vtc.schema.json", quiet=True)) - -print() -print("== signature sets and ECDSA encoding (facilitator CHOICES C9) ==") - -# The low-S rule needs the group order of each curve. These two checks prove -# the constants in pactcore.CURVE_ORDER by computing n * G in affine -# double-and-add, with no library: n * G is the point at infinity exactly when -# n is the order. Curve parameters from FIPS 186-4 D.1.2.3 and D.1.2.4. -P256 = dict(p=2**256 - 2**224 + 2**192 + 2**96 - 1, - gx=0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, - gy=0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5) -P384 = dict(p=2**384 - 2**128 - 2**96 + 2**32 - 1, - gx=int("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A38" - "5502F25DBF55296C3A545E3872760AB7", 16), - gy=int("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C0" - "0A60B1CE1D7E819D7A431D7C90EA0E5F", 16)) - - -def is_group_order(p, gx, gy, n): - a = p - 3 - - def add(P, Q): - if P is None: - return Q - if Q is None: - return P - (x1, y1), (x2, y2) = P, Q - if x1 == x2 and (y1 + y2) % p == 0: - return None - if P == Q: - lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p - else: - lam = (y2 - y1) * pow(x2 - x1, -1, p) % p - x3 = (lam * lam - x1 - x2) % p - return (x3, (lam * (x1 - x3) - y1) % p) - - acc = None - for bit in bin(n)[2:]: - acc = add(acc, acc) - if bit == "1": - acc = add(acc, (gx, gy)) - return acc is None - - -check("P-256 order constant behind the low-S rule is the group order (n*G = O)", - is_group_order(P256["p"], P256["gx"], P256["gy"], pc.CURVE_ORDER["ES256"])) -check("P-384 order constant behind the low-S rule is the group order (n*G = O)", - is_group_order(P384["p"], P384["gx"], P384["gy"], pc.CURVE_ORDER["ES384"])) - -_rev = dict(vtc, signatures=list(reversed(vtc["signatures"]))) -check("V-21 signature set not sorted by normalized kid is rejected", - pc.signatures_ordered(vtc)[0] and not pc.signatures_ordered(_rev)[0]) - -# A high-S encoding is refused before any key is consulted, so the vector -# needs no key material and no `cryptography`. -_k = pc.Key(kid="did:web:v.example#k", alg="ES256", private=None, public=None) -_high = b"\x01" * 32 + (pc.CURVE_ORDER["ES256"] - 1).to_bytes(32, "big") -try: - _k.verify_bytes(_high, b"") - _high_s_refused = False -except pc.InvalidSignature: - _high_s_refused = True -check("V-22 ECDSA signature with s in the high half of the order is rejected", - _high_s_refused) - -print() -if fails: - print(f"{len(fails)} check(s) FAILED") - for f in fails: - print(" -", f) - sys.exit(1) -print("All checks passed.") + r.check(admits("18.00", 0.9091) and not admits("17.99", 0.9091), + "assurance constraint is two-sided at q_min 0.9091: 18.00 admitted, 17.99 refused") + r.check(not admits("18.00", 0.5), "q_min 0.5 with an 18.00 bond on 180.00 is refused") + bad = copy.deepcopy(outcome["terms_result"]["transfers"]) + bad.append({"event": 8, "from": "escrow", "to": "seller", "amount": "0.01", "code": "principal"}) + ok, _ = prof.check(vtc, bad, terminal=True) + r.check(not ok, "an extra transfer from an emptied account fails no-overdraft (V-24)") + r.check(prof.parameters_error({"seller_bond": "abc"}) is not None, + "parameters that fail the profile schema are named (V-13)") + + # -- 7. Merkle ------------------------------------------------------------ + r.section("Merkle tree hash (RFC 9162)") + d = [hashlib.sha256(bytes([i])).digest() for i in range(8)] + H = lambda b: hashlib.sha256(b).digest() # noqa: E731 + r.check(pc.mth([]) == H(b""), "MTH of the empty tree is SHA-256 of the empty string") + r.check(pc.mth(d[:1]) == H(b"\x00" + d[0]), "single leaf is hashed with the 0x00 prefix") + r.check(pc.mth(d[:2]) == H(b"\x01" + H(b"\x00" + d[0]) + H(b"\x00" + d[1])), + "two leaves join under the 0x01 prefix") + r.check(pc.mth(d[:3]) == H(b"\x01" + pc.mth(d[:2]) + pc.mth(d[2:3])), + "three leaves split at k=2, the largest power of two below n") + r.check(pc.mth(d[:5]) == H(b"\x01" + pc.mth(d[:4]) + pc.mth(d[4:5])), "five leaves split at k=4") + r.check(pc.mth(d[:7]) == H(b"\x01" + pc.mth(d[:4]) + pc.mth(d[4:7])), "seven leaves split at k=4") + + # -- 8. negative vectors through the Facilitator -------------------------- + r.section("conformance vectors of Section 14.3, through the reference Facilitator") + r.check(pc.CURVE_ORDER["ES256"] == 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, + "P-256 group order is the SEC 2 value") + r.check(pc.CURVE_ORDER["ES384"] == int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF" + "581A0DB248B0A77AECEC196ACCC52973", 16), + "P-384 group order is the SEC 2 value") + if not HAVE_CRYPTO: + for _ in range(22): + r.skip("negative vector") + return r.done() + + import facilitator as F # noqa: E402 + import mint_examples as mint # noqa: E402 + clock = F.parse_rfc3339("2026-11-01T09:00:00Z") + fac = F.Facilitator("did:web:settle.example", mint.key_for("did:web:settle.example"), resolver, + now=lambda: clock) + kb = mint.key_for(parties["buyer"]) + ks = mint.key_for(parties["seller"]) + kv = mint.key_for(parties["verifier"]) + ksub = pc.Key.generate("did:web:sub.example#k1") + resolver.register(ksub) + + def cosign(obj: dict, *keys_) -> dict: + obj = {k: v for k, v in obj.items() if k != "signatures"} + entries = [pc.sign(obj, k, MEDIA["vtc"]) for k in keys_] + obj["signatures"] = sorted(entries, key=lambda e: pc.norm(json.loads(pc.b64u_decode(e["protected"]))["kid"])) + return obj + + def resign(obj: dict, key, typ: str) -> dict: + obj = {k: v for k, v in obj.items() if k != "signature"} + obj["signature"] = pc.sign(obj, key, typ) + return obj + + def refused(label: str, fn, kinds: tuple[str, ...]) -> None: + try: + fn() + except F.Refuse as exc: + r.check(exc.kind in kinds, f"{label}: refused as {exc.kind}", exc.detail) + return + except terms.ProfileRefusal as exc: + r.check(exc.kind in kinds, f"{label}: refused by the profile as {exc.kind}", exc.detail) + return + r.check(False, f"{label}: refused", "accepted") + + def with_header(obj: dict, **changes) -> dict: + obj = copy.deepcopy(obj) + entry = obj["signatures"][0] + hdr = json.loads(pc.b64u_decode(entry["protected"])) + hdr.update(changes) + entry["protected"] = pc.b64u(json.dumps(hdr, separators=(",", ":")).encode()) + return obj + + fresh = {k: v for k, v in vtc.items() if k != "signatures"} + refused("V-02 alg none", lambda: fac.propose(with_header(vtc, alg="none")), ("algorithm-not-permitted",)) + refused("V-03 alg HS256", lambda: fac.propose(with_header(vtc, alg="HS256")), ("algorithm-not-permitted",)) + def kid_outside() -> dict: + obj = copy.deepcopy(vtc) + entry = obj["signatures"][0] + hdr = json.loads(pc.b64u_decode(entry["protected"])) + entry["kid"] = hdr.pop("kid") + entry["protected"] = pc.b64u(json.dumps(hdr, separators=(",", ":")).encode()) + return obj + refused("V-04 kid moved outside the protected header", lambda: fac.propose(kid_outside()), + ("schema-invalid", "signature-invalid")) + refused("V-05 typ of another media type", lambda: fac.propose(with_header(vtc, typ=MEDIA["delivery"])), + ("signature-invalid",)) + refused("V-06 buyer and seller the same identifier", + lambda: fac.propose(cosign(fresh | {"parties": dict(parties, seller=parties["buyer"])}, kb, ks)), + ("parties-not-distinct",)) + refused("V-07 seller differs from buyer by a trailing slash only", + lambda: fac.propose(cosign(fresh | {"parties": dict(parties, seller=parties["buyer"] + "/")}, kb, ks)), + ("parties-not-distinct",)) + refused("V-08 two buyer signatures, no seller", lambda: fac.propose(cosign(fresh, kb, kb)), + ("signature-missing", "signature-invalid", "unexpected-signer")) + refused("V-09 window_seconds 0", + lambda: fac.propose(cosign(fresh | {"challenge": dict(vtc["challenge"], window_seconds=0)}, kb, ks)), + ("schema-invalid",)) + r.check(not conforms(taskspec | {"acceptance": {}}, "taskspec.schema.json")[0], + "V-10 an empty acceptance object fails the TaskSpec schema") + r.check(not conforms(taskspec | {"acceptance": {k: v for k, v in taskspec["acceptance"].items() if k != "harness_hash"}}, + "taskspec.schema.json")[0], + "V-11 harness_uri without harness_hash fails the TaskSpec schema") + refused("V-12 profile_hash the Facilitator does not advertise", + lambda: fac.propose(cosign(fresh | {"terms": dict(vtc["terms"], profile_hash=pc.h(b"other"))}, kb, ks)), + ("terms-unsupported",)) + refused("V-13 parameters that fail the profile schema", + lambda: fac.propose(cosign(fresh | {"terms": dict(vtc["terms"], parameters={"seller_bond": "abc"})}, kb, ks)), + ("terms-parameters-invalid",)) + case_seller = parties["buyer"].replace("procure-1", "Procure-1") + kcase = resolver.register(pc.Key.generate(case_seller + "#k1")) + code, _ = fac.propose(cosign(fresh | {"id": "vtc_case01", "parties": dict(parties, seller=case_seller)}, kb, kcase)) + r.check(code == 201, "V-19 buyer and seller differing only in did:web path case are accepted as distinct") + refused("V-20 undefined member", lambda: fac.propose(cosign(fresh | {"bonus": True}, kb, ks)), ("schema-invalid",)) + refused("V-21 signatures out of order", + lambda: fac.propose(vtc | {"signatures": list(reversed(vtc["signatures"]))}), ("signatures-unordered",)) + + # ES256 high-S (V-22): sign, flip s to n - s, expect the verifier to refuse it + k256 = pc.Key.generate("did:web:t.example#k1", "ES256") + sig = k256.sign_bytes(b"pact") + n = pc.CURVE_ORDER["ES256"] + high = sig[:32] + (n - int.from_bytes(sig[32:], "big")).to_bytes(32, "big") + try: + k256.verify_bytes(high, b"pact") + r.check(False, "V-22 a high-S ECDSA signature is refused", "accepted") + except Exception: + try: + k256.verify_bytes(sig, b"pact") + r.check(True, "V-22 a high-S ECDSA signature is refused and the low-S original verifies") + except Exception as exc: # pragma: no cover + r.check(False, "V-22 a high-S ECDSA signature is refused", f"low-S original failed too: {exc}") + + # The example contract, then the Delivery-level and Verdict-level vectors + code, _ = fac.propose(vtc) + r.check(code == 201, "V-01 the example contract is accepted by the reference Facilitator") + refused("V-14 Delivery without evidence", + lambda: fac.submit_delivery(resign({k: v for k, v in delivery.items() if k != "evidence"}, ks, MEDIA["delivery"])), + ("evidence-nonconformant",)) + code, _ = fac.submit_delivery(delivery) + r.check(code == 202, "the example Delivery is accepted after the nonconformant one was refused") + refused("V-17 Verdict signed by the seller", lambda: fac.record_verdict(resign(verdict, ks, MEDIA["verdict"])), + ("verifier-not-independent",)) + unsigned_hash = pc.digest_over({k: v for k, v in delivery.items() if k != "signature"}) + refused("V-23 Verdict whose delivery_hash omits the Delivery's signature", + lambda: fac.record_verdict(resign(verdict | {"delivery_hash": unsigned_hash}, kv, MEDIA["verdict"])), + ("verdict-nonconformant",)) + link = {"vtc_id": vtc["id"], "vtc_hash": vtc_hash, "facilitator": parties["facilitator"]} + child_base = copy.deepcopy(fresh) | {"id": "vtc_child01", "parent": link, + "task": dict(vtc["task"], deadline="2026-11-10T00:00:00Z")} + child_wrong_buyer = cosign(child_base | {"parties": dict(parties, buyer=parties["buyer"], seller="did:web:sub.example")}, kb, ksub) + refused("V-15 child whose Buyer is not the parent's Seller", + lambda: fac.register_child(vtc["id"], child_wrong_buyer), ("parent-unresolvable",)) + child_late = cosign(child_base | {"parties": dict(parties, buyer=parties["seller"], seller="did:web:sub.example"), + "task": dict(vtc["task"])}, ks, ksub) + refused("V-16 child whose latest finality is not before the parent's", + lambda: fac.register_child(vtc["id"], child_late), ("finality-ordering-violation",)) + child_ok = cosign(child_base | {"parties": dict(parties, buyer=parties["seller"], seller="did:web:sub.example")}, ks, ksub) + code, st = fac.register_child(vtc["id"], child_ok) + r.check(code == 201 and st["trace"][-1]["event"] == "child-registered", + "a conformant child registers and the parent's trace records it") + return r.done() + + +if __name__ == "__main__": + sys.exit(main()) From 5d2deca8c493793dd5e77143c571e8758134a584 Mon Sep 17 00:00:00 2001 From: Laxmikant Sharma Date: Wed, 16 Sep 2026 16:09:01 -0700 Subject: [PATCH 4/4] v0.2.0 recheck: cross-reference pass over text, schemas, code and the profile bundle A mechanical cross-reference of the -02 text against the schemas, the reference implementation, the examples and the profile bundle, plus five read-only reviews, found about a hundred inconsistencies. All are fixed here. Text: Table 2 restructured (event, where recorded, members) with the Verdict admissibility rule stated once (DELIVERED or WINDOW_OPEN with none standing, or DISPUTED answering a pending Challenge); pending Challenge defined; instants pass when the clock reads them or later; clock-driven entries precede posted records; window-opened shares the `at` of the entry it follows; a named Verifier may answer its own Challenge; the manifest value form and the two profile facts in 12.1 restated; figures regenerated from examples/ with only signatures and parameters elided; 14.1 forbids jwk, jku, x5c, x5u, x5t, x5t#S256, crit and any unprotected header (V-26); signature-invalid and signature-missing are 400; retrieval and issued_at in the capability document; the -01 verification fund is posted by the Seller, following the -01 figure; "proof of nonconformance" replaces "fraud proof". Schemas: strict timestamps, uri patterns, evidence and proof members, facilitator document (issued_at, retrieval, settlement_bindings, endpoints), arbiter withdrawn. Code: pactcore rejects the forbidden header members and unprotected headers, norm() folds only scheme and host; facilitator implements the admissibility rule, the clock ordering, object-conflict on a differing child record, rollback on a profile invariant failure; profile admission order and Seller-posted fund; two admission vectors appended to the bundle (nine vectors); validate 107 checks; measure 44 refusals, 5 acceptances. Every digest was re-minted after the profile README gained its departures section, and mapped into the text by script. --- .github/pull_request_template.md | 2 +- CONTRIBUTING.md | 2 +- LICENSE | 2 +- README.md | 6 +- draft/draft-laxsharma-pact-02.html | 1189 +++---- draft/draft-laxsharma-pact-02.txt | 3018 ++++++++++------- draft/draft-laxsharma-pact-02.xml | 1149 ++++--- examples/challenge.json | 6 +- examples/delivery.json | 10 +- examples/legacy-00/README.md | 4 +- examples/outcome.json | 26 +- examples/status.json | 12 +- examples/verdict-on-challenge.json | 8 +- examples/verdict.json | 6 +- examples/vtc.json | 10 +- examples/well-known/pact-facilitator.json | 10 +- profiles/bonded-restitution/README.md | 54 +- .../bonded-restitution/parameters.schema.json | 18 +- profiles/bonded-restitution/vectors.json | 147 +- schemas/challenge.schema.json | 20 +- schemas/common.schema.json | 7 +- schemas/delivery.schema.json | 22 +- schemas/facilitator.schema.json | 41 +- schemas/taskspec.schema.json | 26 +- schemas/vtc.schema.json | 5 +- tools/README.md | 83 +- tools/agents.py | 6 +- tools/facilitator.py | 131 +- tools/measure.py | 71 +- tools/mint_examples.py | 5 +- tools/pactcore.py | 90 +- tools/profile.py | 47 +- tools/validate.py | 37 +- 33 files changed, 3550 insertions(+), 2720 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d8b9dfd..22b4563 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,7 +8,7 @@ - [ ] `python3 tools/validate.py` passes (required if you touched `examples/` or `schemas/`) -- [ ] `xml2rfc --text draft/draft-laxsharma-pact-00.xml` builds, if you +- [ ] `xml2rfc --text draft/draft-laxsharma-pact-02.xml` builds, if you touched the draft By submitting text you intend for the Internet-Draft, you accept the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5dcb8c0..d3085f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Feedback is the point of a -00 draft. Open an issue for design +Feedback is the point of an Internet-Draft, and this one is at -02. Open an issue for design discussion, a PR for concrete text or schema changes. ## IETF Note Well diff --git a/LICENSE b/LICENSE index f371163..7af9ebf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License 2.0 -Copyright 2026 Lax Sharma +Copyright 2026 Laxmikant Sharma Licensed under the Apache License, Version 2.0 (the "License"); you may not use the files in this repository (excluding the diff --git a/README.md b/README.md index 3160d68..7dc088c 100644 --- a/README.md +++ b/README.md @@ -113,11 +113,7 @@ pip install jsonschema referencing cryptography python3 tools/validate.py ``` -103 checks: 10 schema, 4 canonicalization, 18 hash commitments, 20 rules -the schemas cannot express, 9 signature verifications, 10 on the terms -profile including the two transfer lists printed in Appendix A.6, 6 Merkle -per RFC 9162, and 26 conformance vectors of Section 14.3 run through the -reference Facilitator. `pactcore.jcs` is a full RFC 8785 canonicalizer for +107 checks, in the validator's own words: 10 schema conformance; 4 canonicalization (RFC 8785); 18 hash commitments; 21 rules of the document; 9 signature verification; 11 terms profile: bonded-restitution; 6 Merkle tree hash (RFC 9162); 28 conformance vectors of Section 14.3, through the reference Facilitator. `pactcore.jcs` is a full RFC 8785 canonicalizer for the JSON value types, including UTF-16 key order and ECMAScript number formatting; both are pinned by vectors because both were got wrong once. diff --git a/draft/draft-laxsharma-pact-02.html b/draft/draft-laxsharma-pact-02.html index 3c87d0c..b3e2887 100644 --- a/draft/draft-laxsharma-pact-02.html +++ b/draft/draft-laxsharma-pact-02.html @@ -7,21 +7,12 @@ PACT: Co-Signed Task Contracts, Delivery and Verdict Records, and Outcome Records for Autonomous Agents PACT: Co-Signed Task Contracts, Delivery and Verdict Records, and Outcome Records for Autonomous Agents

Abstract

-

Autonomous agents can already prove who they are, show whose - authority they act under, find one another, call one another, and pay. - What they cannot do with any existing specification is agree on a task +

Autonomous agents can already prove who they are, show whose authority they act under, find and call one another, and pay. What no existing specification lets them do is agree on a task in a form a third party can check, deliver against it, have the delivery judged by someone other than the performer, and carry away a record of the outcome that a stranger can verify. This document specifies PACT, a set of signed JSON records that closes that gap.

-

PACT defines four things: a co-signed task contract whose digest - covers its signature set, so the commitment proves who agreed and not - only what was written; a Verdict record bound by digest to the Delivery - record it judges; a Facilitator-signed event trace and Outcome Record - for every contract, so what happened is recorded once, in one order, - by a party that is not the performer; and a Merkle commitment from a - parent contract's Outcome Record to the Outcome Records of its - subcontracts.

+

PACT defines four things: a co-signed task contract whose digest covers its signature set; a Verdict record bound by digest to the Delivery it judges; a Facilitator-signed event trace and Outcome Record for every contract, recorded once, in one order, by a party other than the performer; and a Merkle commitment from a parent's Outcome Record to its subcontracts' Outcome Records.

Settlement terms are carried by reference to a profile defined outside this document. This document specifies no escrow, custody or release of value, and takes no position on the legal effect of any @@ -1675,13 +1657,9 @@

to have that result judged by a third implementation against criteria fixed before the work began, and no record of the outcome that a fourth implementation can verify without trusting any of the first - three. Receipts record that an action occurred. Audit records - establish whether behaviour matched intent. Payment schemes move value - on the payer's instruction. None of them says what was agreed, what + three. Receipts record that an action occurred, audit records establish whether behaviour matched intent, and payment schemes move value on the payer's instruction. None of them says what was agreed, what was delivered, or whether the one met the other.

-

That gap is not an oversight in those documents; it is outside - their scope, and correctly so. It is the gap this document - addresses, and only that gap.

+

Those documents leave that gap on purpose, since it is outside their scope, and correctly so. It is the gap this document addresses, and only that gap.

@@ -1696,14 +1674,17 @@

trace, signed by a Facilitator, from which one Outcome Record per contract is produced (Section 11, Section 12); and a Merkle commitment from a parent's - Outcome Record to its children's (Section 10).

+ Outcome Record to its children's (Section 12.2).

A contract names its settlement terms by reference: a profile identifier, a digest over the profile's bytes, and a parameter object that this document does not read (Section 5.3). What those terms mean, and everything about who holds or moves value under them, is the profile's to say. This document specifies the records, their digests, who signs each one, the order in which a Facilitator records - events, and a commitment across records. That is the whole of it.

+ events, and a commitment across records. That is the whole of it. A + contract carries a price and names a settlement binding, since a + task contract without them is not one; what happens to the price is + the profile's, and what the binding reports is the binding's.

A deployment relies on other specifications, agreements or arrangements for: the meaning of the terms a contract names; agent identity and key distribution; delegation of authority from a human or @@ -1713,16 +1694,14 @@

reputation system; and the resolution of any disagreement the records do not settle.

Carrying terms by reference is an old pattern in this series. - ACME [RFC8555] carries a terms-of-service URL and - requires a client to assert agreement to it before an account is - created, without defining a single term. A certificate carries its + ACME [RFC8555] carries a terms-of-service URL and, where a server chooses to require it, has the client assert agreement to those terms before an account is created, without defining a single term. A certificate carries its policy as an identifier whose rules live outside the IETF ([RFC5280], Section 4.2.1.4), and the framework for writing those rules [RFC3647] says it does not aim to provide legal advice. The Internet Open Trading Protocol [RFC2801] specified the messages of a trade and left the trade's terms to the parties. PACT follows that line.

-

Two mechanisms present in the -00 revision remain withdrawn: +

Two mechanisms present in the -00 revision [I-D.laxsharma-pact-00] remain withdrawn: contract channels, and the sealed-bid award procedure. The reasons are recorded in [I-D.laxsharma-pact-01] and are not repeated. The change from -01 to this revision is listed in @@ -1745,9 +1724,7 @@

directly. [I-D.hood-agtp-commerce] carries Work Completion Records and an audit-verified settlement timing; [I-D.stone-vcap-ap2-binding] binds verified commerce - settlement to the Agent Payments Protocol. Neither carries a co-signed - contract whose digest covers its signatures, and PACT is designed to - be usable alongside either.

+ settlement to the Agent Payments Protocol. This document binds to neither and is designed to be usable alongside either.

Five bodies of IETF work touch the same records, and the relationship to each is stated here so that it is not left to the reader.

@@ -1796,24 +1773,19 @@

[I-D.ietf-wimse-aims] gives workload and agent identity a home. PACT does not define an identity - format; a kid resolves as Section 14.1.1 says, and - that section is written so that an identity system defined - elsewhere can be named without changing this document. + format; a kid resolves as Section 14.1.1 says. An identity system defined elsewhere is used by naming its identifiers in one of the two forms that section resolves; a further form needs one resolution rule added there, and nothing else in this document changes.
SATP.
[I-D.ietf-satp-core] transfers a digital asset between two gateways with evidence a third party can - verify. An Outcome Record is not an asset transfer and does not - move one; it is a signed statement that certain records were - received in a certain order, and what any of that means for an + verify. An Outcome Record moves no asset. It is a signed statement that certain records were received in a certain order, and what any of that means for an asset is the terms profile's to say.
-

Verification evidence formats for hardware-attested tiers are - specified in [RFC9334] and [RFC9711]. +

Verification evidence for hardware-attested tiers follows the architecture of [RFC9334] and the EAT format of [RFC9711]. Signed, hash-chained action receipts [I-D.sahu-agent-action-receipts], composition of accountability records @@ -1830,11 +1802,9 @@

1.4. The Experiment

-

This document is Experimental. The question it tests is stated - over protocol observables only. Given the same sequence of posted - records and the same clock readings, two independent Facilitator - implementations should produce the same event trace - (Section 11). Given the same trace and the same terms +

This document is Experimental, and an individual submission with no formal standing in the standards process: no working group has adopted it and the IETF has not endorsed it. The question it tests is stated + over protocol observables only. Given the same sequence of posted records, the same clock readings and the same reports from the settlement binding, two independent Facilitator + implementations should produce the same event trace (Section 4.2). Given the same trace and the same terms profile, they should produce the same Outcome Record body (Section 12), byte for byte after canonicalization. The experiment succeeds if two independent Facilitators, serving @@ -1866,8 +1836,7 @@

JCS [RFC8785] before hashing or signing. Implementations MUST order object keys by UTF-16 code unit as [RFC8785] Section 3.2.3 requires. Sorting by Unicode code - point is a common substitution; it agrees with the required order - throughout the Basic Multilingual Plane and diverges above it. Numbers + point is a common substitution; it agrees with the required order until a key outside the Basic Multilingual Plane is compared with one whose first differing unit lies in U+E000 to U+FFFF, where the two orders disagree. Numbers MUST be serialized as [RFC8785] Section 3.2.2.3 requires, which is how ECMAScript prints them: the number one is 1, whatever type held it, and never 1.0.

@@ -1881,8 +1850,7 @@

canonical form of the whole object, including every signature member it carries. Every hash member in this document that names another object (vtc_hash, delivery_hash, challenge_hash, - the object member of a trace entry, and the leaves of - Section 12.2) is that object's digest. A digest that + the object member of a trace entry) is that object's digest, and an element of the list D in Section 12.2 is the 32 bytes that digest's hexadecimal encodes. A digest that excluded signatures would prove what was written and not who agreed to it; the -00 revision had that defect and the -01 revision fixed it for the contract only. This revision applies one construction @@ -1899,24 +1867,21 @@

recognise is inside the commitment and cannot be ignored safely. An implementation MUST reject an object whose pact version it does not implement, and MUST reject an object carrying a member this - document does not define for it, with one exception: the contents of - terms.parameters (Section 5.3) are defined by the - named profile and this document reads none of them. Extension is by a + document does not define for it, with two exceptions: the contents of terms.parameters (Section 5.3), which the named profile defines and this document does not read; and the members of a Delivery's evidence, a Challenge's proof and a TaskSpec's constraints beyond those Section 3 names, which the verification profile defines. Extension is by a new version, not by adding members.

Time. Every timestamp is an RFC 3339 date-time [RFC3339] in UTC with the "Z" designator. The Facilitator's clock governs every deadline and window in this document: the instant at which the Facilitator records an event is the instant that counts, that instant is what the trace carries, and - parties should allow for skew when acting near a boundary. + parties SHOULD allow for skew when acting near a boundary. Section 17.1 says what that clock can and cannot prove.

-

Amounts. An amount is a decimal string with no exponent and a +

Amounts. An amount is a decimal string with no sign, no exponent and a fractional part of two to eighteen digits; comparisons are exact and no rounding is implied. A currency is an asset identifier whose namespace is defined by the settlement binding named in - price.settlement, and need not be an ISO 4217 code. A network - is a ledger identifier in the form the same binding defines. This + price.settlement, and need not be an ISO 4217 code. A network is a ledger identifier in the form the same binding defines; the examples use [CAIP-2] chain identifiers. This document carries amounts; it does not say what any amount is for. Where a record produced under this document lists amounts, as terms_result does (Section 12.1), the meaning @@ -1930,7 +1895,7 @@

2.1. Terminology

-

Four words in this document have meanings elsewhere that are +

Five words in this document have meanings elsewhere that are close enough to mislead, and are defined here once.

Contract:
@@ -1953,10 +1918,7 @@

Evidence:
The evidence member of a Delivery is the set of artefacts a Verifier evaluates, produced by the Seller. - It is not Evidence in the sense of [RFC9334]. The - member name is kept from -01 because renaming it would change every - committed digest for no gain in clarity that this note does not - provide. + It is not Evidence in the sense of [RFC9334]. The member name is kept because it is the ordinary word for what the member holds; the RATS term names a role in an attestation architecture, and this note is the disambiguation.
Facilitator:
@@ -1985,8 +1947,7 @@

that carries it, with its type, whether it is required in that object, and what it commits to. It is a dictionary and not a rulebook: the rule that a record omitting a required member, or carrying one this document - does not define for it, does not conform is stated once in - Section 2; the rules a Facilitator applies when it + does not define for it, does not conform is stated once, in Section 14.2; the rules a Facilitator applies when it accepts or refuses a record are in Section 14 and in the section that defines the record. No sentence in this section requires anything of any party. Where a member's meaning is the named @@ -2065,8 +2026,7 @@

object, required. spec_hash (digest, required) commits to a TaskSpec (Section 5.2); spec_uri (URI, optional) says where its bytes may be - fetched; deadline (timestamp, required) is the instant - after which the deadline-passed event may be recorded + fetched; deadline (timestamp, required) is the instant at or after which the deadline-passed event may be recorded (Section 4.2).
@@ -2086,10 +2046,9 @@

(string, required), profile (string or URI, required; Section 9), criteria_hash (digest, required; the manifest digest of the acceptance instrument per - Section 5.1), max_verdict_seconds (integer, - required; the longest interval after delivered within + Section 5.1), max_verdict_seconds (integer, required, greater than zero; the longest interval after delivered within which a first Verdict is recorded before verdict-lapsed - may be), arbiter (URI, optional). Commits to how a + may be). Commits to how a Delivery is judged and by what.
@@ -2117,8 +2076,7 @@

challenge:
object, required. window_seconds (integer, required, greater than zero) is the - duration of the challenge window; max_dispute_seconds - (integer, required) is the longest interval after a + duration of the challenge window; max_dispute_seconds (integer, required, greater than zero) is the longest interval after a challenge event within which a Verdict on that Challenge is recorded before dispute-lapsed may be.
@@ -2216,17 +2174,14 @@

input_hash:
digest, required for tiers whose - fraud proof re-executes. Commits to the production input actually + proof of nonconformance re-executes. Commits to the production input actually consumed.
evidence:
object, required. Members profiled by - verification.tier and verification.profile; for - the acceptance profile, profile, - instrument_hash, results_hash and - results_uri. Conformance to the profile is a validity + verification.tier and verification.profile; for the acceptance profile, profile, instrument_hash and results_hash (required) and results_uri (optional). Conformance to the profile is a validity condition of the Delivery, not a judgement on the work.
@@ -2274,8 +2229,7 @@

results_hash:
-
digest, required. Commits to the - Verifier's own results. +
digest, required. The digest of the results document the verification profile defines; for acceptance, the bytes of the results file the instrument wrote.
@@ -2306,17 +2260,13 @@

proof:
object, required. Members profiled by - verification.profile; for the acceptance profile, - profile, instrument_hash, results_hash, - results_uri and failing_checks (array of - strings). + verification.profile; for the acceptance profile, profile, instrument_hash and results_hash (required), results_uri and failing_checks (array of strings; optional).
costs:
object, optional. amount and - currency: a figure the Challenger asserts for producing the - proof. This document records it in the trace and reads it for + currency: an amount the Challenger states. This document records it in the trace and reads it for nothing; its meaning is the named terms profile's.
@@ -2330,7 +2280,7 @@

Carried in the Contract Status (Section 11), media type application/vnd.pact.status+json, the Facilitator's - signed response to every accepted request.

+ signed response to every accepted POST.

vtc_id, vtc_hash:
@@ -2349,8 +2299,7 @@

array of objects, required. The event trace so far, in the order recorded (Section 4.2). Each entry carries event (string, required), at - (timestamp, required), object (digest, required where the - event was caused by a posted record), and the event-specific + (timestamp, required), object (digest, required where the event was caused by a posted record, and on dispute-lapsed, where it names the Challenge that lapsed), and the event-specific members listed in Section 4.2.
@@ -2409,8 +2358,7 @@

object, required (Section 12.1). profile and profile_hash (copied from the contract), currency - (string), and transfers (array of objects), each with - from (string), to (string), amount + (string), and transfers (array of objects), each with event (integer, the zero-based index of the trace entry the transfer follows), from (string), to (string), amount (amount) and code (string). The entries are the named profile's output for the trace; this document defines their form and two arithmetic invariants over them, and nothing about their @@ -2443,49 +2391,57 @@

-settlement_bindings:
-
array of objects, - required. Each with id (URI), networks and - assets (arrays of strings). +issued_at:

+
timestamp, required. When the + document was signed. Nothing in a document survives its Facilitator withdrawing a profile; Section 8 says when to fetch it again.
-flows:
-
array of strings, required. The flows - of Section 7.1 the Facilitator implements. +settlement_bindings: +
array of objects, + required. Each with id (URI), networks and assets (arrays of strings), all required.
-verification_profiles:
-
array of strings, - required. +flows: +
array of strings, required. The flows of Section 7.1 the Facilitator implements; Section 7.1 requires verdict-first among them.
+verification_profiles:
+
array of strings, required. A contract naming a profile not listed is refused (Section 13.1). +
+
+
terms_profiles:
-
array of objects, required, +
array of objects, required, with at least one entry. Each with id (URI) and profile_hash (digest): the terms profiles, at the - revisions named, whose schedules this Facilitator evaluates. + revisions named, whose schedules this Facilitator evaluates.
-
+
max_contract_value:
-
object, optional. - amount and currency. +
object, optional. amount and currency; a contract whose price, stated in the same currency, exceeds it is refused, as is one whose price is stated in another currency (Section 13.1).
-
+
challenge_deposit:
-
object, optional. +
object, optional. amount and currency; see - Section 7.3. + Section 7.3.
-
+
+retrieval:
+
string, optional. parties, + the default of Section 17.12, or open. +
+
+
endpoints:
-
object, required. Maps each endpoint - name in Section 13 to an absolute URI. +
object, required. Maps each endpoint + name in Section 13 to an absolute URI.

@@ -2518,8 +2474,7 @@

parties.buyer - the contract; a child registration - (Section 10.2) + the contract Contract Status, Outcome Record @@ -2527,7 +2482,8 @@

parties.seller - the contract; the Delivery + the contract; the Delivery; as the Buyer of a child, + that child's contract (Section 10.2) Contract Status, Outcome Record @@ -2557,8 +2513,7 @@

One identifier may play more than one role across contracts, and - Section 9.1 says which combinations within one - contract a Facilitator refuses.

+ Section 9.1 and Section 14.2 say which combinations within one contract a Facilitator refuses.

@@ -2568,10 +2523,7 @@

4. Protocol Overview

-

A contract passes through four phases. Propose establishes the - record. Agree co-signs it and a Facilitator accepts it. Complete - produces a Delivery and a Verdict on it. Record produces an Outcome - Record. Every step after Agree is an event the Facilitator records on +

A contract passes through four phases: Propose establishes the record, Agree co-signs it and a Facilitator accepts it, Complete produces a Delivery and a Verdict on it, and Record produces an Outcome Record. Every step after Agree is an event the Facilitator records on its own clock, in one order, and the sequence of those events is the contract's trace. The trace is the protocol's central object: the state machine is defined over it, every response a Facilitator gives carries @@ -2607,7 +2559,7 @@

Message flow under the verdict-first flow, without a Challenge -

Every accepted request is answered with a Contract Status +

Every accepted POST is answered with a Contract Status (Section 11), a Facilitator-signed object carrying the state and the trace so far. Nothing in the figure moves value, and no arrow in it is named for a movement of value. What a terms profile does @@ -2650,16 +2602,12 @@

The figure omits three arrows that the table carries: a FAIL Verdict recorded in DELIVERED or in WINDOW_OPEN also leads to - AWAITING_CHILDREN; under the no-window flow DELIVERED leads - there directly; and a Verdict that is late (verdict-lapsed) + AWAITING_CHILDREN; under the no-window flow delivered leads there directly; and a Verdict that is late (verdict-lapsed) opens the window without one. FINAL, SETTLED and ABANDONED are - terminal and each produces exactly one Outcome Record. The -01 - revision named one of these states for a movement of value; no state - here is.

+ terminal and each produces exactly one Outcome Record. The -01 revision had a state, RELEASING, named for a movement of value; it is gone. FUNDED remains, named for the event the settlement binding reports (Section 4.2), and nothing here says what that report means.

The state named PROPOSED in earlier revisions is gone. Between the parties' signatures and the Facilitator's acceptance a contract exists - only on the parties' side, so no Facilitator could observe that state - and the reference implementation never reported it.

+ only on the parties' side, so no Facilitator could observe that state, and the -01 reference implementation never reported it, although the -01 Section 12.1 example printed it in a 201 response.

@@ -2685,8 +2633,7 @@

Event - Recorded in; then - Members and condition + Recorded in; then. Members and condition @@ -2694,134 +2641,113 @@

accepted - none; then ACCEPTED - - object is vtc_hash. The contract passed Section 13.1. + none; then ACCEPTED. object is vtc_hash. The contract passed Section 13.1. funded - ACCEPTED; then FUNDED - - ref (string, optional, in the form the settlement binding defines). Recorded when every account the named terms profile requires shows finality on the settlement binding named in price.settlement; how a Facilitator observes that is the binding's to say, and this is the only sentence in this document that mentions an account. + ACCEPTED; then FUNDED. ref (string, optional, in the form the settlement binding defines). Recorded when the settlement binding named in price.settlement reports that whatever the named terms profile requires before work starts is in place; how a Facilitator observes that is the binding's to say. Where the profile requires nothing, funded follows accepted in the same operation. deadline-passed - ACCEPTED or FUNDED; then AWAITING_CHILDREN - - task.deadline has passed with no delivered entry. + ACCEPTED or FUNDED; then AWAITING_CHILDREN. task.deadline has passed with no delivered entry. delivered - FUNDED; then DELIVERED - - object is the Delivery's digest. The Delivery passed Section 6. + FUNDED; then DELIVERED, or under no-window AWAITING_CHILDREN directly. object is the Delivery's digest. The Delivery passed Section 6. window-opened - DELIVERED; then WINDOW_OPEN - Under delivery-first, immediately after delivered; under verdict-first, immediately after a PASS verdict or after verdict-lapsed. closes_at (timestamp, required) is at plus challenge.window_seconds. + DELIVERED; then WINDOW_OPEN. Recorded in the same operation as the entry it follows, with the same at: under delivery-first the delivered entry; under verdict-first a PASS verdict or verdict-lapsed. closes_at (timestamp, required) is at plus challenge.window_seconds. verdict - DELIVERED, WINDOW_OPEN or DISPUTED; then see the condition - - object is the Verdict's digest; signer (the kid of its signature); outcome (PASS or FAIL); answers (digest of the Challenge, when the Verdict carries challenge_hash); supersedes (digest of the Verdict it replaces, when one stood). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending. + DELIVERED (verdict-first, no Verdict standing), WINDOW_OPEN (delivery-first, no Verdict standing) or DISPUTED (answering a pending Challenge); then as the condition says. object is the Verdict's digest; signer (the kid of its signature); outcome (PASS or FAIL); answers (digest of the Challenge, present exactly when the Verdict carries challenge_hash); supersedes (digest of the Verdict that stood, present exactly when one did). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes the state of nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending. verdict-lapsed - DELIVERED; then WINDOW_OPEN - Under verdict-first, verification.max_verdict_seconds have passed since delivered with no verdict. window-opened follows. + DELIVERED; unchanged. Under verdict-first, verification.max_verdict_seconds have passed since delivered with no verdict. window-opened follows in the same operation. challenge - WINDOW_OPEN or DISPUTED; then DISPUTED - - object is the Challenge's digest; signer (the kid of its signature); costs copied from the Challenge when present. The Challenge passed Section 7.3 before closes_at. + WINDOW_OPEN or DISPUTED; then DISPUTED. object is the Challenge's digest; signer (the kid of its signature); costs copied from the Challenge when present. The Challenge passed Section 7.3 before closes_at. dispute-lapsed - DISPUTED; then WINDOW_OPEN - - object is the Challenge's digest. challenge.max_dispute_seconds have passed since that challenge entry with no Verdict answering it. Leads to WINDOW_OPEN once no Challenge is pending; the earlier Verdict, if any, stands. + DISPUTED; then WINDOW_OPEN. object is the Challenge's digest. challenge.max_dispute_seconds have passed since that challenge entry with no Verdict answering it. Leads to WINDOW_OPEN once no Challenge is pending; the earlier Verdict, if any, stands. window-closed - WINDOW_OPEN; then AWAITING_CHILDREN - - closes_at has passed and no Challenge is pending. The window is never extended: a dispute that outlasts it delays this entry and does not move closes_at. + WINDOW_OPEN; then AWAITING_CHILDREN. closes_at has passed and no Challenge is pending. The window is never extended: a dispute that outlasts it delays this entry and does not move closes_at. child-registered - any non-terminal; unchanged - - object is the child contract's digest; facilitator (URI). Section 10.2. + any non-terminal; unchanged. object is the child contract's digest; facilitator (URI). Section 10.2. child-final - any non-terminal; unchanged - - object is the child's Outcome Record digest; child (the child contract's digest). + any non-terminal; unchanged. object is the child's Outcome Record digest; child (the child contract's digest). child-unresolved - any non-terminal; unchanged - - child (the child contract's digest). The child's latest finality instant (Section 10.3) has passed and no Outcome Record for it is held. + any non-terminal; unchanged. child (the child contract's digest). The child's latest finality instant (Section 10.3) has passed and no Outcome Record for it is held. children-final - AWAITING_CHILDREN; then terminal follows - Every registered child has a child-final or child-unresolved entry. A contract with no registered children records this entry on entering AWAITING_CHILDREN. + AWAITING_CHILDREN; then terminal follows. Every registered child has a child-final or child-unresolved entry. A contract with no registered children records this entry on entering AWAITING_CHILDREN. terminal - AWAITING_CHILDREN; then FINAL, SETTLED or ABANDONED - - state (the terminal state) and challenge_upheld (boolean). ABANDONED where deadline-passed was recorded; SETTLED where the standing Verdict is FAIL, with challenge_upheld true when that Verdict answers a Challenge; FINAL otherwise. + AWAITING_CHILDREN; then FINAL, SETTLED or ABANDONED. state (the terminal state) and challenge_upheld (boolean). ABANDONED where deadline-passed was recorded; SETTLED where the standing Verdict is FAIL, with challenge_upheld true when that Verdict answers a Challenge; FINAL otherwise.

The standing Verdict is the last verdict entry in the trace that no later entry supersedes. A Challenge is pending from its - challenge entry until a verdict entry answers it or - a dispute-lapsed entry names it.

+ challenge entry until a verdict entry answers it, a dispute-lapsed entry names it, or a terminal entry is recorded.

Every instant in the table is read from the Facilitator's clock, and an entry conditioned on an instant having passed is recorded at - the first opportunity after it, which need not be that instant. Two - Facilitators given the same posted records with the same clock - readings record the same trace; that is the determinism the + the first opportunity after it, which need not be that instant. Two Facilitators given the same posted records, the same clock readings and the same reports from the settlement binding record the same trace; that is the determinism the experiment in Section 1.4 tests, and the reason - every condition above is stated over the trace and the clock and - nothing else.

+ every condition above is stated over the trace, the clock and the binding's report, and nothing else.

+

An instant has passed when the Facilitator's clock reads that + instant or a later one, and a record received when the clock reads an + instant has arrived at it, so a Challenge received when the clock + reads closes_at is after the window. Before acting on a posted + record a Facilitator MUST first record every clock-driven entry that + is due, so that the record is judged in the state the clock produced; + entries due at the same instant are recorded in the order of the + table. The at of an entry MUST be no earlier than that of + the entry before it, and a Status's issued_at MUST be no + earlier than the at of its last entry.

@@ -2833,8 +2759,7 @@

A VTC is a JSON object, media type application/vnd.pact.contract+json, with the members in - Section 3.2. A VTC is valid only if every required member - is present, the parties are distinct, and both the Buyer and the Seller + Section 3.2. A VTC is valid only if every required member is present, the Buyer and the Seller are distinct after normalization (Section 14.2), any named Verifier satisfies Section 9.1, and both the Buyer and the Seller have contributed exactly one signature that verifies against a key bound to its identifier (Section 14.2). The Facilitator and any Verifier do not sign the VTC; their assent is expressed by acting on it, @@ -2846,63 +2771,74 @@

replayable against any facilitator, chain or token contract.

-
+
 ========== NOTE: '\' line wrapping per RFC 8792 ===========
 
 {
   "pact": "0.2",
   "type": "VerifiableTaskContract",
-  "id": "vtc_7f3a91",
+  "id": "vtc_9f2c11",
   "parties": {
-    "buyer":       "did:web:acme.example",
-    "seller":      "did:web:dataforge.example",
+    "buyer": "did:web:buyer.example:agents:procure-1",
+    "seller": "did:web:dataforge.example:agents:etl-3",
     "facilitator": "did:web:settle.example",
-    "verifier":    "did:web:audit.example"
+    "verifier": "did:web:audit.example"
   },
   "task": {
     "spec_hash": "sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef11\
    1d27ff6bee37f05531823b72",
-    "deadline":  "2026-11-14T00:00:00Z"
+    "spec_uri": "https://buyer.example/specs/taskspec.json",
+    "deadline": "2026-11-14T00:00:00Z"
   },
   "price": {
-    "amount":     "180.00",
-    "currency":   "USDC",
+    "amount": "180.00",
+    "currency": "USDC",
     "settlement": "https://settle.example/bindings/ledger-1",
-    "network":    "eip155:8453"
+    "network": "eip155:8453"
   },
   "verification": {
-    "tier":                "T0-reexec",
-    "profile":             "acceptance",
-    "criteria_hash":       "sha256:0bdde1ab6b081d2b4bda580c539375\
-   6ae95c10b8351c9c55eb9316416265fc1b",
+    "tier": "T0-reexec",
+    "profile": "acceptance",
+    "criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\
+   10b8351c9c55eb9316416265fc1b",
     "max_verdict_seconds": 86400
   },
   "flow": "verdict-first",
   "terms": {
-    "profile":
-      "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution",
-    "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\
-   c1147743326dabd45bda546dc62",
-    "parameters":   { "...": "the profile's; not read here" }
+    "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restit\
+   ution",
+    "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf\
+   6956e835df2653b99d71d363a68",
+    "parameters": {
+      "...": "the profile's; not read here"
+    }
   },
   "challenge": {
     "window_seconds": 3600,
     "max_dispute_seconds": 86400
   },
-  "signatures": [ { "protected": "...", "signature": "..." },
-                  { "protected": "...", "signature": "..." } ]
+  "signatures": [
+    {
+      "protected": "...",
+      "signature": "..."
+    },
+    {
+      "protected": "...",
+      "signature": "..."
+    }
+  ]
 }
 
Figure 3: -A Verifiable Task Contract, signatures abbreviated +A Verifiable Task Contract, signatures and parameters abbreviated
-

Digests are elided here; the reference repository's values are in - Section 15. The parameters object is shown - elided on purpose: nothing in this document depends on what is in - it.

+

The values are the reference repository's + (Section 15), with the signatures abbreviated. The + parameters object is shown elided on purpose: nothing in this + document depends on what is in it.

@@ -2913,7 +2849,7 @@

committed harness_uri as a string while leaving the bytes at that URI uncommitted, which permitted a Buyer to substitute the acceptance instrument after signature, run the substituted - instrument, and submit the failure as a valid fraud proof. The -01 + instrument, and submit the failure as a valid proof of nonconformance. The -01 revision stated the rule and its own reference TaskSpec broke it for three of four URIs; this revision's example carries all four sibling hashes, and the validator checks each.

@@ -2921,7 +2857,7 @@

single octet stream, the commitment MUST be computed as SHA-256(JCS(M)) where M is an object mapping each file's path, relative to the bundle root and expressed with "/" separators, - to SHA-256 of its bytes, over every file in the bundle. A + to the digest of its bytes in the string form of Section 2, over every file in the bundle. A file whose name, or any directory on whose path, begins with a dot is not part of a bundle. A manifest of per-file digests is specified rather than an archive digest because archive formats carry ordering, timestamp and permission metadata that is not stable across producers. The same @@ -2944,7 +2880,7 @@

tiers, a proof statement with its verifying key for proving tiers, or rubric_uri and rubric_hash for judgment tiers. An empty acceptance object MUST be rejected. The -00 revision's - schema permitted one, which made every fraud proof impossible.

+ schema permitted one, which made every proof of nonconformance impossible.

Thresholds MUST be stated so that they cannot be satisfied by returning almost nothing. A threshold expressed only as a rate over returned rows is satisfied by returning one correct row out of @@ -2977,11 +2913,7 @@

A Facilitator MUST refuse a contract whose terms.profile and terms.profile_hash do not match an entry in the terms_profiles array of its own capability document - (Section 8), so that no party signs terms the - Facilitator will not evaluate, and MUST refuse a contract whose - parameters do not validate against the named profile's - parameters.schema.json. It reads parameters for no - other purpose. The rule of Section 2 that an + (Section 8; terms-unsupported), so that no party signs terms the Facilitator will not evaluate, and MUST refuse a contract whose parameters do not validate against the named profile's parameters.schema.json (terms-parameters-invalid). This document reads parameters for no other purpose; the named profile's schedule and admission rule read them as the profile's. The rule of Section 2 that an undefined member is rejected does not apply inside parameters; the profile's schema governs there.

A profile usable with this document defines, in its prose, a @@ -2993,9 +2925,10 @@

profile author would rather not think about: a lapsed dispute, an unresolved child, a contract abandoned before it was funded. Deterministic means the result depends on the contract, the trace and - nothing else, so that any party holding those can recompute it. The - prose also names the accounts the schedule uses and how each one's - opening amount is computed from the contract. This document does not + nothing else, so that any party holding those can recompute it. The prose also names the accounts the schedule moves value between, and says which of them are internal, opened empty and required to close empty, and which are external. A profile MAY also state + an admission rule: a condition on the contract, evaluated once at + accepted, whose failure is reported with a problem type from + the profile's namespace (Section 13.3). This document does not register profiles and defines none normatively; Appendix A carries one for the experiment.

Everything the -01 revision said in its Section 5.3, and everything @@ -3051,20 +2984,26 @@

{ "pact": "0.2", "type": "Delivery", - "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ - 5fbbd4ebace4fb980f1c2", - "work_hash": "sha256:9c1f...", - "work_uri": "https://cdn.dataforge.example/o/9c1f", - "input_hash": "sha256:41ab...", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff49493\ + 04c2c855ca1d746ba6459", + "work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee2\ + 80a3170b6b9a5037036ef0", + "work_uri": "https://cdn.dataforge.example/o/a26d", + "input_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c9\ + 75a1d1ce519d582306338b1", "evidence": { - "profile": "acceptance", - "instrument_hash":"sha256:0bdde1ab6b081d2b4bda580c5393756ae95\ - c10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:7e02...", - "results_uri": "https://cdn.dataforge.example/o/7e02" + "profile": "acceptance", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\ + 5c10b8351c9c55eb9316416265fc1b", + "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf84\ + 8ebaca540214b4f6d2165688a51", + "results_uri": "https://cdn.dataforge.example/o/28d3" }, - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } }

@@ -3091,8 +3030,7 @@

7.1. Flows

The flow member selects one of three shapes for the state - machine of Section 4.1. A conformant Facilitator MUST - implement verdict-first; the others are OPTIONAL, and a + machine of Section 4.1. A conformant Facilitator MUST implement verdict-first and MUST list it in the flows member of its capability document (Section 3.9); the others are OPTIONAL, and a Facilitator MUST refuse a contract naming a flow it does not advertise (flow-unsupported).

@@ -3107,9 +3045,7 @@

delivery-first:
The window opens at - delivered. A Verdict MAY be recorded inside the window - without a Challenge; a FAIL ends the contract, a PASS changes - nothing. + delivered. One Verdict MAY be recorded inside the window without a Challenge, and a second only in answer to one; a FAIL ends the contract, a PASS changes nothing.
@@ -3120,11 +3056,10 @@

-

The -01 revision had four release modes, named for when value - moved. Two of them, on-window and optimistic, - produce the same trace and differed only in which event a profile - acts on, which is a profile parameter and not a protocol matter. The - mapping is in Appendix B.

+

The -01 revision had four release modes. Two of them produce the + same trace and differed only in which event a profile acts on, which + is a profile parameter and not a protocol matter. The mapping is in + Appendix B.

The window opens at the instant of the window-opened entry and closes at that instant plus challenge.window_seconds, carried in the entry as closes_at. A Facilitator MUST NOT @@ -3151,16 +3086,20 @@

{ "pact": "0.2", "type": "Verdict", - "vtc_id": "vtc_7f3a91", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ - f8aaaf2f186702504fba242ffb", - "outcome": "PASS", - "profile": "acceptance", + "vtc_id": "vtc_9f2c11", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45\ + 370cb3023331bc8bfbd924fcbe", + "outcome": "PASS", + "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\ 10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:7e02...", - "evaluated_at": "2026-11-10T09:14:22Z", - "signature": { "protected": "...", "signature": "..." } + "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf848e\ + baca540214b4f6d2165688a51", + "evaluated_at": "2026-11-10T09:14:22Z", + "signature": { + "protected": "...", + "signature": "..." + } } @@ -3178,13 +3117,11 @@

(no-recorded-delivery); one whose delivery_hash does not match that entry, or whose profile or instrument_hash does not match the contract - (verdict-nonconformant); one received in a state the table - in Section 4.2 does not list for it, or under the + (verdict-nonconformant); one received in a state, or under conditions, that the table in Section 4.2 does not list for it, or under the no-window flow (wrong-state); and one carrying challenge_hash that names no pending Challenge, or omitting it while the contract is DISPUTED (verdict-nonconformant). - A Verdict that answers a Challenge supersedes the Verdict that stood - before it, and both stay in the trace.

+ A Verdict recorded while one stands supersedes it, and both stay in the trace; since a Verdict is accepted while one stands only in DISPUTED, only a Verdict that answers a Challenge ever supersedes.

A Verdict commits to the instrument it ran and to the results it produced. Without instrument_hash a Verifier could run something other than the committed instrument and the contract would @@ -3207,8 +3144,7 @@

A Challenge is a JSON object, media type application/vnd.pact.challenge+json, with the members in - Section 3.6, by which a party submits a fraud - proof inside the window. A Facilitator MUST refuse a Challenge + Section 3.6, by which a party submits a proof of nonconformance (what optimistic systems call a fraud proof) inside the window. A Facilitator MUST refuse a Challenge received when the contract is not in WINDOW_OPEN or DISPUTED, or after closes_at (challenge-window-closed); one whose delivery_hash does not match the delivered @@ -3217,19 +3153,13 @@

one whose signer it cannot resolve (signature-invalid); and one signed by the contract's Seller (unexpected-signer), since a performer's statement against its own Delivery is not a - fraud proof and the -01 revision left the case open. A Facilitator + proof of nonconformance and the -01 revision left the case open. A Facilitator MUST NOT refuse a Challenge on the ground that its signer is the contract's Buyer.

A Challenge that is accepted is evaluated by a party satisfying - Section 9.1, whose finding is a Verdict carrying - challenge_hash; the Challenger's own assertion is not a - finding. The Challenger is the party identified by the kid of + Section 9.1, whose finding is a Verdict carrying challenge_hash; the Challenger's own assertion is not a finding, unless the Challenger is the verifier the contract names, whose Verdict is the finding by definition. A named Verifier that finds its own PASS wrong posts a Challenge and answers it. The Challenger is the party identified by the kid of the Challenge's signature.

-

A Facilitator MAY require that a Challenge be accompanied by a - deposit in the amount its capability document advertises as - challenge_deposit. How a deposit is posted is the settlement - binding's, what becomes of it is the terms profile's, and this - document says nothing further about it. Section 17.14 +

A capability document MAY advertise challenge_deposit. Whether anything must accompany a Challenge, how it is posted and what becomes of it are the terms profile's and the settlement binding's to say; this document carries the member and reads it for no purpose. Section 17.14 discusses what a deposit does and does not prevent.

@@ -3240,19 +3170,26 @@

{ "pact": "0.2", "type": "Challenge", - "vtc_id": "vtc_7f3a91", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ - f8aaaf2f186702504fba242ffb", + "vtc_id": "vtc_9f2c11", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45\ + 370cb3023331bc8bfbd924fcbe", "proof": { - "profile": "acceptance", + "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\ 5c10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:a91e...", - "results_uri": "https://watch.example/o/a91e", - "failing_checks": ["schema_valid_rate", "row_count_min"] + "results_hash": "sha256:7f05a60223e24ca9393e0c241d0448cb5f39e\ + 5edb71c117fb7d3d593d5861b40", + "results_uri": "https://watch.example/o/a91e", + "failing_checks": ["schema_valid_rate", "row_count_min"] }, - "costs": { "amount": "1.20", "currency": "USDC" }, - "signature": { "protected": "...", "signature": "..." } + "costs": { + "amount": "1.20", + "currency": "USDC" + }, + "signature": { + "protected": "...", + "signature": "..." + } }

@@ -3316,15 +3253,14 @@

Before a Buyer and Seller can co-sign a VTC they must agree on a Facilitator and know what it implements. This document registers one - well-known URI for that purpose, per [RFC8615].

+ well-known URI for that purpose, per [RFC8615]. A client SHOULD fetch the document again before it proposes, since nothing in it survives the Facilitator withdrawing a profile.

This is deliberately narrower than agent discovery, which is the subject of separate work and is not restated here. What is discovered is one service's capabilities, not an agent's identity, skills or endpoints.

A Facilitator SHOULD publish a JSON document, media type application/vnd.pact.facilitator+json, with the members in - Section 3.9, at the path - /.well-known/pact-facilitator of its origin. The document MUST + Section 3.9, at the path /.well-known/pact-facilitator of its origin: for an https: identifier that origin, and for a did:web identifier https:// followed by the host the method encodes. The document MUST be served over HTTPS. It MUST be signed, and the signature MUST verify against a key bound to the identifier in facilitator. An unsigned capability document is not usable for contract formation, @@ -3340,29 +3276,39 @@

"pact": "0.2", "type": "FacilitatorCapabilities", "facilitator": "did:web:settle.example", + "issued_at": "2026-11-01T09:00:00Z", "settlement_bindings": [ - { "id": "https://settle.example/bindings/ledger-1", + { + "id": "https://settle.example/bindings/ledger-1", "networks": ["eip155:8453"], - "assets": ["USDC"] } + "assets": ["USDC"] + } ], - "flows": ["verdict-first", "delivery-first"], - "verification_profiles": ["acceptance", "bisection"], + "flows": ["verdict-first", "delivery-first"], + "verification_profiles": ["acceptance"], "terms_profiles": [ - { "id": - "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f\ - 5fc1147743326dabd45bda546dc62" } + { + "id": "tag:laxsharma79@gmail.com,2026:pact:bonded-restituti\ + on", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124c\ + cf6956e835df2653b99d71d363a68" + } ], - "max_contract_value": { "amount": "50000.00", - "currency": "USDC" }, + "max_contract_value": { + "amount": "50000.00", + "currency": "USDC" + }, "endpoints": { - "contract": "https://settle.example/pact/v2/contracts", - "delivery": "https://settle.example/pact/v2/deliveries", - "verdict": "https://settle.example/pact/v2/verdicts", + "contract": "https://settle.example/pact/v2/contracts", + "delivery": "https://settle.example/pact/v2/deliveries", + "verdict": "https://settle.example/pact/v2/verdicts", "challenge": "https://settle.example/pact/v2/challenges", - "outcome": "https://settle.example/pact/v2/outcomes" + "outcome": "https://settle.example/pact/v2/outcomes" }, - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } } @@ -3371,11 +3317,7 @@

A client MUST NOT infer any capability from the absence of a member. - A Facilitator that does not publish a capability document can still be - named in a VTC by prior arrangement; discovery is a convenience, not a - precondition. A Facilitator MUST NOT list a terms profile whose - vectors (Section 12.1) its own implementation does not - reproduce.

+ A client may hold a Facilitator\'s capability document by prior arrangement rather than fetch it from the well-known path; the path is a convenience, the document is not, since a Facilitator refuses what its document does not advertise (Section 5.3, Section 13.1). A Facilitator lists only the terms profiles whose vectors (Section 12.1) its own implementation reproduces.

@@ -3395,15 +3337,8 @@

name does not determine how much checking a contract gets and the profile largely does.

Consider one task, a bulk data transformation, under two profiles at - the same nominal tier. Re-executing the whole computation and comparing - outputs costs approximately what performing it cost. Running a committed - acceptance instrument against the delivered artifact costs a small - fraction of a percent. Those two differ by more than two orders of - magnitude in what checking costs relative to the price. A terms profile - may make that ratio matter; this document requires only that a - verification profile state an order-of-magnitude estimate of its cost - relative to the work, since a figure nobody can estimate is a figure - nobody can use.

+ the same nominal tier. Re-executing the whole computation and comparing outputs costs about what performing it cost; running a committed acceptance instrument against the delivered artifact costs a small fraction of that. Those are the author's estimates, not measurements (the measurements Section 16 mentions are of the protocol, not the work), and the two can differ by orders of magnitude in what checking costs relative to the price. A terms profile + may make that ratio matter; this document requires of a verification profile the five statements listed after the profiles below, one of which is an order-of-magnitude estimate of its cost relative to the work, since a figure nobody can estimate is a figure nobody can use.

Implementations SHOULD select the cheapest profile that detects the failures they actually care about, rather than the strongest-sounding one. A committed acceptance instrument that is adequate is worth more @@ -3412,10 +3347,9 @@

acceptance:
Run the instrument committed by - criteria_hash against the Delivery. The fraud proof is a + criteria_hash against the Delivery. The proof of nonconformance is a failing evaluation. Deterministic by construction, since the - instrument is fixed before work begins. Cost: a small fraction of a - percent of the work for a data transformation. + instrument is fixed before work begins. Cost: a small fraction of the work for a data transformation, by estimate.
@@ -3435,15 +3369,21 @@

+

A verification profile usable with this document states five + things: what artefact is evaluated and against what; what constitutes a + valid proof of nonconformance, including whether absence of evidence + is one; that its proof can be evaluated by a party other than the + Seller; its cost relative to the work, to order of magnitude; and + whether it is deterministic and with what tolerance + (Section 17.10). acceptance states these below; the other two are sketches that a full profile document completes; a profile defined elsewhere states them in its own document.

9.1. Verifier Independence and Identifier Normalization

Independence is a relation between the party that signs a Verdict - and the parties to the contract. It MUST be derived by the evaluator - and MUST NOT be satisfied by a field in which a record declares - itself independent. A Facilitator MUST refuse a Verdict whose signer + and the parties to the contract. It MUST be derived by the Facilitator + and MUST NOT be satisfied by a field in which a record declares itself independent. Rules of this kind are stated for evaluation after the fact in [X402COMPLIANCE]; this document binds them at contract formation. A Facilitator MUST refuse a Verdict whose signer is, after normalization, the contract's Buyer, Seller or Facilitator, and MUST refuse a contract whose parties.verifier is any of those three (verifier-not-independent). The last case is the @@ -3481,7 +3421,7 @@

                 A (Buyer)
                     |
-                vtc_7f3a91      at did:web:settle.example
+                vtc_9f2c11      at did:web:settle.example
                     |
                 B (Seller)
                     |
@@ -3529,14 +3469,17 @@ 

10.2. Registration and Children Final

-

The parent's Facilitator learns of a child when the parent's Seller - registers it: a POST of the child's co-signed contract to the - parent's contract resource (Section 13). The - registering party is the child's Buyer, which is why it holds the - child's contract and why it is authorised: it is a party to both.

+

The parent's Facilitator learns of a child when the child's co-signed + contract is posted to the parent's children resource + (Section 13). Any holder of that contract may post it; + the registration is authenticated by the child's own signatures, and + the child's Buyer, which is the parent's Seller, is the party that + ordinarily holds it. A registered child is identified at the parent's + venue by its digest, so its id need not be unique there.

A Facilitator MUST refuse a registration, with the problem type - named, when: the body is not a valid contract - (Section 14.2); its parent.vtc_hash is not the + named, when: the body is not a valid contract (Section 14.2, the + rules on members and signatures; its terms and deadline are its own + Facilitator's to check); its parent.vtc_hash is not the parent's digest or its parent.facilitator is not this Facilitator (parent-unresolvable); its parties.buyer is not the parent's parties.seller @@ -3559,13 +3502,10 @@

check, so the attack cost one signature.

A child becomes final for its parent when the parent's Facilitator holds the child's Outcome Record. It may obtain that record itself, - by retrieving it from the child's Facilitator, or receive it from the - parent's Seller by a POST to the same resource + by retrieving it from the child's Facilitator, or receive it by a POST to the child's entry under that resource (Section 13). Either way the Facilitator MUST verify the record's Facilitator signature against a key bound to the - identifier the registration recorded, and MUST verify that its - vtc_hash is the registered child's digest, before recording - child-final. Where the child's latest finality instant passes + identifier the registration recorded, and MUST verify that its vtc_hash is the registered child's digest, refusing a record that fails either check (child-outcome-invalid), before recording child-final. Where the child's latest finality instant passes with no record held, the Facilitator records child-unresolved. children-final follows when every registered child has one entry or the other, and the parent's @@ -3661,35 +3601,54 @@

{ "pact": "0.2", "type": "ContractStatus", - "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ - 5fbbd4ebace4fb980f1c2", - "state": "WINDOW_OPEN", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff49493\ + 04c2c855ca1d746ba6459", + "state": "WINDOW_OPEN", "trace": [ - { "event": "accepted", "at": "2026-11-01T10:00:00Z", - "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ - 225fbbd4ebace4fb980f1c2" }, - { "event": "funded", "at": "2026-11-01T10:00:00Z" }, - { "event": "delivered", "at": "2026-11-10T08:30:12Z", - "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ - aaf2f186702504fba242ffb" }, - { "event": "verdict", "at": "2026-11-10T09:14:30Z", - "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ - e09452a74bac15e6752ca81", "outcome": "PASS" }, - { "event": "window-opened", "at": "2026-11-10T09:14:30Z", - "closes_at": "2026-11-10T10:14:30Z" } + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff494\ + 9304c2c855ca1d746ba6459" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370\ + cb3023331bc8bfbd924fcbe" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:1ac94d72dbdd1f51e523ecddb3a3b360703976215\ + b13324485db355c948adfc0", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + } ], "issued_at": "2026-11-10T09:14:30Z", - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } }

Figure 11: -A Contract Status after the Verdict of Figure 1 +A Contract Status after the Verdict of Figure 5

-

Two rules make a Status worth keeping. A Facilitator MUST issue a - Status for every request it accepts, carrying the entry that request +

Two rules make a Status worth keeping. A Facilitator MUST issue a Status for every POST it accepts, carrying the entry that request caused, so that the requester holds a signed receipt of what was recorded and when. And the trace in every Status a Facilitator issues for a contract MUST be a prefix of the trace in every later one; two @@ -3716,7 +3675,7 @@

A Facilitator MUST issue exactly one Outcome Record for every contract that reaches a terminal state, including SETTLED and ABANDONED, MUST sign it, and MUST NOT require the signature of any - other party on it. The -00 revision's record needed the signature of + other party on it. A Facilitator MUST serve the bytes of the record it signed rather than sign it again on retrieval; under a randomized signature scheme a second signing would produce a second record with a different digest. The -00 revision's record needed the signature of the party it recorded against, which made a reputation layer built on it structurally incapable of recording a loss. The Facilitator signature is what makes the record evidence: without it the record is @@ -3732,55 +3691,107 @@

{ "pact": "0.2", "type": "OutcomeRecord", - "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ - 5fbbd4ebace4fb980f1c2", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff49493\ + 04c2c855ca1d746ba6459", "parties": { - "buyer": "did:web:acme.example", - "seller": "did:web:dataforge.example", + "buyer": "did:web:buyer.example:agents:procure-1", + "seller": "did:web:dataforge.example:agents:etl-3", "facilitator": "did:web:settle.example", - "verifier": "did:web:audit.example" + "verifier": "did:web:audit.example" + }, + "outcome": { + "state": "SETTLED", + "challenge_upheld": true }, - "outcome": { "state": "SETTLED", "challenge_upheld": true }, - "work_hash": "sha256:9c1f...", + "work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee2\ + 80a3170b6b9a5037036ef0", "trace": [ - { "event": "accepted", "at": "...", - "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ - 225fbbd4ebace4fb980f1c2" }, - { "event": "funded", "at": "..." }, - { "event": "delivered", "at": "...", - "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ - aaf2f186702504fba242ffb" }, - { "event": "verdict", "at": "...", - "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ - e09452a74bac15e6752ca81", "outcome": "PASS" }, - { "event": "window-opened", "at": "...", "closes_at": "..." }, - { "event": "challenge", "at": "...", - "object": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df\ - 6cfef12d12d1340c15e5d85" }, - { "event": "verdict", "at": "...", - "object": "sha256:012bab194f444012f87ff8986419537f9de9f3dfe\ - 845c6623ac6a7488fb98b73", "outcome": "FAIL", - "answers": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4d\ - f6cfef12d12d1340c15e5d85", - "supersedes": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51\ - cee8e09452a74bac15e6752ca81" }, - { "event": "children-final", "at": "..." }, - { "event": "terminal", "at": "...", "state": "SETTLED", - "challenge_upheld": true } + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff494\ + 9304c2c855ca1d746ba6459" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370\ + cb3023331bc8bfbd924fcbe" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:1ac94d72dbdd1f51e523ecddb3a3b360703976215\ + b13324485db355c948adfc0", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + }, + { + "event": "challenge", + "at": "2026-11-10T09:40:00Z", + "object": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093e\ + c2c6795962e67a6e2ff55ff", + "signer": "did:web:watch.example#k1", + "costs": { + "amount": "1.20", + "currency": "USDC" + } + }, + { + "event": "verdict", + "at": "2026-11-10T09:58:05Z", + "object": "sha256:10d537e7b8face8bd7695541d36d32568394a5319\ + 7653280c404e2d84a63d46d", + "signer": "did:web:audit.example#k1", + "outcome": "FAIL", + "answers": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093\ + ec2c6795962e67a6e2ff55ff", + "supersedes": "sha256:1ac94d72dbdd1f51e523ecddb3a3b36070397\ + 6215b13324485db355c948adfc0" + }, + { + "event": "children-final", + "at": "2026-11-10T09:58:05Z" + }, + { + "event": "terminal", + "at": "2026-11-10T09:58:05Z", + "state": "SETTLED", + "challenge_upheld": true + } ], "terms_result": { - "profile": - "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ - c1147743326dabd45bda546dc62", - "currency": "USDC", + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restit\ + ution", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf\ + 6956e835df2653b99d71d363a68", + "currency": "USDC", "transfers": [ - { "event": 8, "from": "...", "to": "...", "amount": "...", - "code": "..." } + { + "event": 8, + "from": "...", + "to": "...", + "amount": "...", + "code": "..." + } ] }, - "signatures": [ { "protected": "...", "signature": "..." } ] + "signatures": [ + { + "protected": "...", + "signature": "..." + } + ] } @@ -3808,22 +3819,17 @@

code (a string the profile defines, naming the schedule line that produced the entry).

This document defines the form of the list and two arithmetic - facts about it, and nothing about what any entry means. Over the - accounts and opening amounts the profile declares for the contract - (Section 5.3): no entry takes from an account more than + facts about it, and nothing about what any entry means. Over the internal accounts the profile declares (Section 5.3), which open empty: no entry takes from an account more than that account holds at that point in the list; and after the last - entry every account the profile marks internal holds zero. A - Facilitator MUST NOT sign an Outcome Record whose list breaks either - fact, and MUST NOT sign one whose list differs from what the - profile's schedule produces for the record's own trace. Any party + entry every account the profile marks internal holds zero. The two facts are constraints on a profile, checked against its vectors before a Facilitator lists it (Section 8); a Facilitator MUST NOT sign an Outcome Record whose list breaks either, since such a list shows the profile it evaluates to be defective, and MUST NOT sign one whose list differs from what the profile's schedule produces for the record's own trace. Any party holding the contract, the trace and the profile's bundle can recompute the list; that is the property the experiment in Section 1.4 depends on.

vectors.json in a profile's bundle is an array of objects, each with name, contract (a VTC, or the - members of one the schedule reads), trace (a complete - trace), and transfers (the list the schedule produces for - it). A Facilitator MUST reproduce every vector of a profile before + members of one the schedule reads), and then either trace (a complete + trace) with transfers (the list the schedule produces for + it) and accounts (the profile's internal accounts, which open empty and must close empty), or admission (an object carrying either admitted, true, or refused, the problem type the admission rule answers with for that contract). A Facilitator MUST reproduce every vector of a profile before listing that profile in its capability document (Section 8), which is the only conformance requirement this document places on a profile implementation.

@@ -3845,18 +3851,14 @@

largest power of two smaller than n. The shape is therefore fixed by n alone, and two implementations that agree on D agree on the root.

-

The domain separation is not optional. Without distinct prefixes an - attacker can present an interior node as though it were a leaf, and so - claim an inclusion proof for a subtree that never existed.

+

The domain separation is not optional, because the prefixes are what make MTH the function [RFC9162] defines, and a second implementation must compute the same root. The second-preimage attack the prefixes guard against, a leaf input chosen to equal an interior node's input, needs a leaf of that input's length; the fixed 32-byte digests in D cannot supply one, so here the prefixes buy agreement with the RFC rather than a defence the construction would otherwise lack.

The member is present when at least one child is registered and absent otherwise; it MUST NOT be present with an empty or zero value, which would be indistinguishable from a tree whose children were withheld. Where every registered child is unresolved D is empty and the root is MTH of the empty list, SHA-256 of the empty string; the child-unresolved entries in the trace say which records the - root does not cover. The -01 revision computed leaves over records - with their signatures removed, which let a record be re-signed - without changing the root.

+ root does not cover. The -01 revision did not say whether a leaf covered the record's signatures; this revision says it does, so a record cannot be re-signed without changing the root.

@@ -3866,9 +3868,7 @@

13. Protocol Endpoints

-

This section specifies the operations a Facilitator exposes. Base - URIs are not fixed by this document; they are discovered from the - endpoints member of the capability document +

This section specifies the operations a Facilitator exposes. This document fixes no base URI; each is discovered from the endpoints member of the capability document (Section 8), so a Facilitator may mount them anywhere on its origin.

@@ -3890,8 +3890,7 @@

Supply a child's outcome:
POST - {contract}/{id}/children/{child_id}; body, the child's - Outcome Record; 200 with a Status. + {contract}/{id}/children/{child_hash}, where child_hash is the digest by which the registration identifies the child; body, the child's Outcome Record; 200 with a Status. This resource is keyed by digest, so the id rule of Section 13.2 does not apply to it.
Submit a Delivery:
@@ -3915,21 +3914,24 @@

-

All requests and responses use the media types defined in +

A request naming a contract the Facilitator does not hold is refused + as unknown-contract (404). A body larger than the Facilitator + accepts is refused as payload-too-large (413). A failure inside + the Facilitator is reported as internal-error (500), the one + problem type that names no rule. A GET of an Outcome Record before the + terminal entry is refused as wrong-state.

+

All requests and responses use the media types defined in Section 19. All requests MUST be made over HTTPS, following the recommendations of [RFC9325]. Status codes are as - defined in [RFC9110]. A Delivery and a Challenge are - answered 202 (Accepted) rather than 201 because - acceptance of the bytes is not acceptance of the work; what follows - depends on a Verdict the Facilitator does not itself produce.

-

A Facilitator authenticates the sender of a POST by the signature on + defined in [RFC9110]. A Delivery and a Challenge are answered 202 (Accepted) because, in the sense of [RFC9110] Section 15.3.3, their processing is not complete when the response is sent: what either record leads to may depend on a Verdict the Facilitator does not itself produce. A contract and a Verdict are answered 201 (Created); the contract is the resource the Location header names, and a Verdict, to which this document gives no resource of its own, is identified by the digest the Status in the response carries.

+

A Facilitator authenticates the sender of a POST by the signature on the body, and by nothing else in this document: it MUST reject a Delivery not signed by the contract's Seller, a Verdict not signed by a party admissible under Section 7.2, a Challenge whose signer it cannot resolve, and a child registration or child outcome whose body does not verify as Section 10.2 requires. A Facilitator MAY require an HTTP-layer authentication in - addition. Retrieval is discussed in Section 17.12.

+ addition. Retrieval is discussed in Section 17.12.

@@ -3940,10 +3942,10 @@

Section 14, Section 5.3 and Section 9.1 before creating the resource, MUST refuse a contract whose parties.facilitator is not itself or - whose price.settlement, network or asset it does not + whose price.settlement, network or currency it does not advertise (facilitator-mismatch, settlement-unsupported), and MUST refuse otherwise with the - problem type that names the rule.

+ problem type that names the rule. settlement-unsupported also covers a price stated in a currency other than max_contract_value's and a price above it; a verification profile the Facilitator does not list is refused as settlement-unsupported.

 POST /pact/v2/contracts HTTP/1.1
@@ -3951,17 +3953,17 @@ 

Content-Type: application/vnd.pact.contract+json { "pact": "0.2", "type": "VerifiableTaskContract", - "id": "vtc_7f3a91", ... } + "id": "vtc_9f2c11", ... }

 HTTP/1.1 201 Created
-Location: /pact/v2/contracts/vtc_7f3a91
+Location: /pact/v2/contracts/vtc_9f2c11
 Content-Type: application/vnd.pact.status+json
 
 { "pact": "0.2", "type": "ContractStatus",
-  "vtc_id": "vtc_7f3a91", "state": "ACCEPTED",
+  "vtc_id": "vtc_9f2c11", "state": "ACCEPTED",
   "trace": [ { "event": "accepted", ... } ], ... }
 
@@ -3983,8 +3985,7 @@

conflict.

Where a POST carries the same object id as an existing resource but a different digest, the Facilitator MUST respond - 409 (Conflict) (object-conflict). Retrying a - submission is therefore always safe, and altering one never is.

+ 409 (Conflict) (object-conflict). Retrying a submission is therefore safe when the same bytes are resent, and altering one never is. A record signed afresh is a different record with a different digest, not a retry: ECDSA signatures are randomized unless produced as [RFC6979] describes, so a client signing with ES256 or ES384 SHOULD sign deterministically or keep the bytes it sent and resend those.

@@ -4006,16 +4007,18 @@

failure of the specification.

+========== NOTE: '\' line wrapping per RFC 8792 ===========
+
 HTTP/1.1 422 Unprocessable Content
 Content-Type: application/problem+json
 
 {
-  "type":   "tag:laxsharma79@gmail.com,2026:pact:problem:
-             signatures-unordered",
+  "type":   "tag:laxsharma79@gmail.com,2026:pact:problem:signatur\
+   es-unordered",
   "title":  "Signature set not sorted",
   "status": 422,
-  "detail": "the second entry's kid sorts before the first's
-             after normalization.",
+  "detail": "the second entry's kid sorts before the first's afte\
+   r normalization.",
   "section": "14.1"
 }
 
@@ -4051,7 +4054,7 @@

Figure 13: -HTTP exchange for the flow in Figure 1 +HTTP exchange for the flow in Figure 1

@@ -4075,10 +4078,7 @@

14.1. Signatures

Every signature carried by a VTC, Delivery, Verdict, Challenge, - Status, Outcome Record or capability document is a JWS - [RFC7515] in the General JSON Serialization of - Section 7.2.1 of that document, with the payload detached as its - Appendix F describes. The payload is BASE64URL of the JCS-canonical + Status, Outcome Record or capability document has, for each signer, the form of one signature object of the JWS [RFC7515] General JSON Serialization, Section 7.2.1 of that document, with the payload detached as its Appendix F describes. The payload is BASE64URL of the JCS-canonical bytes of the object with the signing member removed, so the JWS Signing Input is ASCII(BASE64URL(UTF8(protected)) || "." || BASE64URL(JCS(object))) exactly as Section 5.1 of @@ -4091,10 +4091,7 @@

typ.
  • - alg MUST be ES256 or ES384 - [RFC7518], or EdDSA [RFC8037] - with an Ed25519 key; a verifier MAY also accept Ed448. A verifier - MUST reject any other value, and MUST reject none. Absent + alg MUST be Ed25519 [RFC9864] with an Ed25519 key, or ES256 or ES384 [RFC7518] with a P-256 or P-384 key. A verifier MUST reject any other value, including the polymorphic EdDSA identifier of [RFC8037] that [RFC9864] deprecates, and MUST reject none (algorithm-not-permitted). Absent an allowlist an attacker selects the algorithm, which permits both unsigned acceptance and confusion of a public key for a symmetric secret. @@ -4106,7 +4103,19 @@

    can publish a key document to re-attribute a genuine signature to itself.

  • -
  • +
  • The protected header MUST NOT carry jwk, jku, + x5c, x5u, x5t, x5t#S256 or + crit, and a signature entry MUST NOT carry an unprotected + header; a verifier MUST reject an entry carrying any of them + (signature-invalid). A key travels by reference and never + inline, so that the kid rule cannot be bypassed. +
  • +
  • The resolved key MUST be of the type and curve alg + requires: Ed25519 for Ed25519, P-256 for + ES256, P-384 for ES384. A mismatch is + signature-invalid. +
  • +
  • typ MUST be the full media type of the object signed, including the application/ prefix, so that a signature over one object type cannot be replayed as a signature over @@ -4114,27 +4123,27 @@

    omitting the prefix; this document requires the full form so that typ equals the registered media type character for character. Explicit typing follows Section 3.11 of - [RFC8725]. + [RFC8725].

  • -
  • A signatures array MUST be sorted by the normalized +
  • A signatures array MUST be sorted by the normalized kid of its entries (Section 9.1), ties broken by the unnormalized kid, both compared as sequences of Unicode code points; a verifier MUST reject an unsorted array (signatures-unordered). Two clients that each attach their own entry and exchange the object would otherwise produce two arrays, and since the digest covers the - array, two digests for one agreement. + array, two digests for one agreement.
  • -
  • An ECDSA signature MUST have its s value in the low +
  • An ECDSA signature MUST have its s value in the low half of the curve order, that is s at most n/2 for the order n of the curve [SP800-186], and a verifier MUST reject one that does not. [RFC7518] fixes the encoding and not which of the two valid s values is accepted; accepting both lets anyone holding a valid signature produce a second one over the same bytes without the key, and a - second signature is a second digest. EdDSA verification per + second signature is a second digest. Ed25519 verification per [RFC8032] already rejects a non-canonical - S, so the rule is stated for ECDSA only. + S, so the rule is stated for ECDSA only.
  • @@ -4164,8 +4173,7 @@

    establishes that the holder of that key signed; that the key belongs to the party is a property of the identity method, and this document does not add to it. An identity system for agents defined - elsewhere, such as [I-D.ietf-wimse-aims], is used by - naming its identifiers here and resolving them by its rules.

    + elsewhere, such as [I-D.ietf-wimse-aims], is used by naming its identifiers here in one of these two forms; a further form needs a resolution rule added to this list, which is the one change it would take.

    @@ -4189,8 +4197,7 @@

    identifier MUST be rejected.
  • - challenge.window_seconds MUST be greater than zero, - and task.deadline MUST be later than the instant of + challenge.window_seconds, challenge.max_dispute_seconds and verification.max_verdict_seconds MUST be greater than zero, and task.deadline MUST be later than the instant of acceptance (deadline-invalid).
  • Every URI member inside hash-committed content MUST have a @@ -4209,10 +4216,12 @@

    schema (terms-unsupported, terms-parameters-invalid).

  • -
  • Every amount MUST have the form in - Section 2 (amount-invalid), and every - object MUST validate against the schema published for its media - type (schema-invalid). +
  • Every object MUST validate against the schema published for its + media type (schema-invalid), which includes the form of + every amount (Section 2); an amount carrying more + decimal places than the settlement binding named in + price.settlement supports is refused + (amount-invalid).
  • @@ -4222,7 +4231,8 @@

    14.3. Test Vectors

    -

    Each rule above has an accepting and a rejecting form. A +

    Most rules above have an accepting and a rejecting form; the table + carries the ones a suite most often gets wrong. A conformance suite built from this section alone, with no reference to any implementation, should reach the same verdicts. Rejecting vectors name the rule they violate.

    @@ -4347,8 +4357,7 @@

    V-18 - object keys ordered by code point, with a - supplementary-plane key + object keys ordered by code point rather than UTF-16 unit, a supplementary-plane key beside one in U+E000 to U+FFFF digest mismatch @@ -4396,6 +4405,12 @@

    one digest mismatch + + V-26 + a protected header carrying a member Section 14.1 forbids: jwk, jku, x5c, x5u, x5t, x5t#S256 or crit + + reject + @@ -4409,8 +4424,7 @@

    half of the V-18 mistake: [RFC8785] prints numbers as ECMAScript does, so the float one is 1 and never 1.0. The -02 reference canonicalizer printed 1.0 - until this vector caught it, and every digest in - Section 15 changed when it was fixed.

    + until this vector caught it, and the spec_hash, vtc_hash and delivery_hash of Section 15 changed when it was fixed.

    @@ -4420,13 +4434,12 @@

    15. Worked Example

    -

    The tables and digests below are the reference repository's, at the +

    The digests below are the reference repository's, at the tag named in Section 16. The object figures in earlier - sections use short illustrative identifiers for page width; the - repository examples carry the full ones, and the digests here are - computed over those. The figures that the -01 revision printed here - about a bond and a required detection rate are now the profile's, and - Appendix A carries them.

    + sections are the repository's objects with their signatures + abbreviated and the contract's parameters elided; the digests + here are computed over the full objects. The figures that the -01 revision printed here + about a bond and a required detection rate are now the profile's; Appendix A carries the rule and the parameters.

    A buyer commissions a data transformation at a price of 180.00 USDC under the verdict-first flow, the acceptance verification profile, and the terms profile of @@ -4441,12 +4454,12 @@

    d27ff6bee37f05531823b72 criteria_hash sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b83\ 51c9c55eb9316416265fc1b - profile_hash sha256:00d71829f6f9192b43b929d0154a6eb409f5fc114\ - 7743326dabd45bda546dc62 - vtc_hash sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ - 225fbbd4ebace4fb980f1c2 - delivery_hash sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ - aaf2f186702504fba242ffb + profile_hash sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf6956\ + e835df2653b99d71d363a68 + vtc_hash sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff494\ + 9304c2c855ca1d746ba6459 + delivery_hash sha256:6bcbb831ea27a8754a0df9b44361be12411e45370\ + cb3023331bc8bfbd924fcbe

    criteria_hash is the manifest digest of @@ -4457,19 +4470,8 @@

    same construction over the profile bundle. vtc_hash is the digest of the signed contract, and delivery_hash of the signed Delivery, both per Section 2.

    -

    Every value above changed from the -01 revision, for four reasons - that are each recorded so that a reader comparing the two documents can - account for the difference: spec_hash because the TaskSpec - now carries the sibling hashes Section 5.1 always required; - vtc_hash because the contract's members changed - (Appendix B) and because spec_hash did; - delivery_hash because it now covers the Delivery's signature; - and profile_hash because it did not exist.

    -

    The trace the reference implementation records for this contract - on the path of Figure 1, and on the dispute path of - Figure 7, together with the transfer lists the - profile produces for each, are the vectors in the profile's bundle, - and Appendix A prints them.

    +

    The -01 revision printed three of the values above, spec_hash, criteria_hash and vtc_hash, and each differs from what it printed, for reasons recorded so that a reader comparing the two documents can account for the difference. spec_hash and vtc_hash differ because the -01 canonicalizer serialized numbers as the host language printed them (Section 14.3, V-25); spec_hash also because the TaskSpec now carries the sibling hashes Section 5.1 always required, and vtc_hash also because the contract's members changed (Appendix B). criteria_hash carries no number; it differs because the two files of the instrument bundle were edited to drop their mention of the withdrawn call-for-bids example. profile_hash is new, and delivery_hash, which the -01 figures showed only as a placeholder, now covers the Delivery's signature.

    +

    The traces the reference implementation records for this contract on the path of Figure 1 and on the dispute path of Figure 7 carry the event sequences of the first two vectors in the profile's bundle, and the transfer lists the profile produces for them are those vectors' lists; the vectors name their objects by placeholder digests, so the match is of sequence and lists, not of bytes. Appendix A prints both lists.

    @@ -4478,20 +4480,18 @@

    16. Implementation Status

    This section records the status of known implementations of this - document per [RFC7942], and is to be removed before - publication as an RFC.

    + document per [RFC7942]. The section and the reference to [RFC7942] are to be removed before publication as an RFC, and the listing of an implementation here implies no endorsement by the IETF.

    One implementation is known to the author, and the author wrote it: - https://github.com/pact-spec/spec, under the Revised BSD licence. At + https://github.com/pact-spec/spec, under the Apache License 2.0. At tag v0.2.0 it comprises the object schemas, the examples whose digests - Section 15 prints, a conformance validator that runs - 103 checks including every vector of + Section 15 prints, a conformance validator that runs 107 checks including every vector of Section 14.3, a Facilitator serving the endpoints of Section 13 with the profile of Appendix A, and clients for the other roles. Its previous tag, v0.1.0, implemented the -01 revision and is the source of the measurements the author has published about it. No second implementation exists, so nothing in Section 1.4 has - been tested, and this document claims no interoperability.

    + been tested, and this document claims no interoperability. It is an individual submission and the product of no working group; the implementation is a prototype, the information is current as of the tag named above, and the contact is the author.

    @@ -4532,7 +4532,7 @@

    Seller cannot deliver against a substituted instrument or input; - cannot judge its own Delivery; cannot re-sign a record without + is refused as Verifier when it signs under a party identifier; cannot re-sign a record without changing every digest over it its signature on the contract and the Delivery; the Verdicts and Challenges on its Delivery @@ -4580,9 +4580,7 @@

    client SHOULD retain every Status it receives, and a party that submitted a record and holds no Status for it has a claim it can make only outside this protocol. Making omission attributable needs - a witness the Facilitator does not control, such as a monitor with a - gossip path of the kind [RFC9162] assumes, and this - document specifies none.

    + a witness the Facilitator does not control, such as the client gossip that [RFC9162] Section 11.3 mentions and leaves undefined, and this document specifies none.

    Time is the Facilitator's. Every instant in a trace is read from its clock, and nothing in this document lets a party prove that a recorded instant is wrong. This document therefore states the @@ -4644,7 +4642,7 @@

    The -00 revision committed harness_uri as a string. The bytes at that URI were covered by nothing. A Buyer could therefore sign a contract, replace the acceptance instrument afterwards, run the - replacement, and submit its failure as a textbook-valid fraud proof. + replacement, and submit its failure as a textbook-valid proof of nonconformance. Cost of the attack: one file overwrite. The mirror attack works against a Seller that hosts the input sample. Section 5.1 requires a sibling hash over the dereferenced @@ -4746,12 +4744,11 @@

    17.10. Nondeterminism as Shield and as Weapon

    A re-execution profile that does not state what determinism it - assumes cuts both ways. An honest Seller doing model-assisted work is - convicted by a re-execution that differs for ordinary reasons. A - cheating Seller escapes any fraud proof by asserting nondeterminism, + assumes cuts both ways. An honest Seller doing model-assisted work is found wrong by a re-execution that differs for ordinary reasons. A + cheating Seller escapes any proof of nonconformance by asserting nondeterminism, unfalsifiably. A verification profile MUST state whether it is deterministic and what tolerance applies, and a contract naming one - that does not is not safely enforceable by anyone.

    + that does not cannot be judged safely by anyone.

    @@ -4775,12 +4772,10 @@

    17.12. Retrieval

    -

    A GET on a contract's Status or Outcome Record MUST be refused - unless the requester is a party named in the contract's +

    A GET on a contract's Status or Outcome Record MUST be refused (retrieval-restricted) unless the requester is a party named in the contract's parties, the identifier in the contract's parent.facilitator, or a party the Facilitator has chosen to - admit; a Facilitator MAY open retrieval more widely and SHOULD say so - in its capability document. How a requester proves which identifier + admit; a Facilitator MAY open retrieval more widely and SHOULD say so in its capability document (retrieval, Section 3.9). How a requester proves which identifier it is, on a GET with no body to sign, is an HTTP-layer matter this document leaves to the deployment. The -01 revision left retrieval unauthenticated by default, which published every contract graph a @@ -4796,10 +4791,7 @@

    signs contracts the party never agreed to. Rotation and revocation belong to the identity method behind the kid (Section 14.1.1), and this document does not restate them. - Two things it does require: a Facilitator MUST record, with each - record it accepts, the key material or its digest as resolved at the - time of acceptance, so that a later rotation does not make an earlier - signature unverifiable; and a Facilitator MUST NOT accept a record + Two things it does require: a Facilitator MUST retain, for as long as it retains a record it accepted, the key material or its digest as resolved at the time of acceptance, and SHOULD make it available to a party retrieving the record, so that a later rotation does not make an earlier signature unverifiable; and a Facilitator MUST NOT accept a record whose kid resolves to a key the identity method marks as revoked at the time of acceptance.

    @@ -4811,10 +4803,7 @@

    Every accepted Challenge costs an independent evaluation. Without a cost to the Challenger, a party can exhaust a Verifier's or a - Facilitator's capacity by challenging every Delivery. The deposit of - Section 7.3 is one defence, and it is a MAY because a - deposit also deters the honest challenger an open model relies on. A - Facilitator that requires no deposit SHOULD rate-limit Challenges per + Facilitator's capacity by challenging every Delivery. A deposit required by a terms profile, advertised as challenge_deposit (Section 7.3), is one defence, and this document requires none, since a deposit also deters the honest Challenger an open model relies on. A Facilitator whose profiles require no deposit SHOULD rate-limit Challenges per Challenger and per contract, and SHOULD publish that it does so.

    @@ -4862,14 +4851,14 @@

    with mechanisms specified elsewhere and is not specified here.

    -
    +

    18.3. Challenger Access

    An open challenge model requires that some party outside the contract can obtain the deliverable and the input in order to build a - fraud proof. That is in direct conflict with confidentiality of both. + proof of nonconformance. That is in direct conflict with confidentiality of both. The conflict is real and this document does not dissolve it. What it does is make the choice visible: a contract whose content cannot be disclosed to a Challenger will receive no Challenge from outside its @@ -4882,8 +4871,7 @@

    18.4. Retention

    -

    Retention duties stated for dispute purposes can conflict with - erasure rights asserted by a data subject. Contracts SHOULD state a +

    Retention periods stated for dispute purposes can conflict with erasure requests from a data subject. Contracts SHOULD state a retention period, and implementers should be aware that a hash commitment survives deletion of the content it commits to, which is usually the property they want and occasionally the one they must @@ -4949,9 +4937,7 @@

    Objects MUST be canonicalized per [RFC8785] before hashing or signing. Implementations that canonicalize by sorting object keys - on Unicode code point rather than UTF-16 code unit will produce - divergent digests for keys outside the Basic Multilingual - Plane. + on Unicode code point rather than UTF-16 code unit can produce a divergent digest when a key outside the Basic Multilingual Plane is compared with one whose first differing unit lies in U+E000 to U+FFFF.
    Published specification:
    @@ -5066,8 +5052,7 @@

    The -01 revision asked for these in the standards tree under the names pact-contract+json and so on. Registration in that tree from outside the IETF stream needs approval this document does - not have ([RFC6838], Section 3.1), and the vendor - tree is where an individual's specification belongs.

    + not have ([RFC6838], Section 3.1), and [RFC6838] Section 3.2 opens the vendor tree to anyone who interchanges files associated with a publicly available product.

    @@ -5117,12 +5102,7 @@

    permits without registration. Each is the identifier in the table appended to the prefix tag:laxsharma79@gmail.com,2026:pact:problem:, a tag URI - [RFC4151] under the author's control. A tag URI is - an identifier and is not dereferenceable, which is why it was - chosen over the -01 revision's prefix on a code-hosting site: an - identifier should not change when hosting does. Documentation for - every type is maintained in the repository named in - Section 16. Each entry carries the identifier, the + [RFC4151] under the author's control. A tag URI is an identifier and does not dereference. [RFC9457] Section 4 says a type URI SHOULD resolve to documentation; this document departs from that on purpose, so that an identifier does not change when hosting does, which the -01 revision's prefix on a code-hosting site could not promise, and the section named for each type is its documentation. The list of types, each with its status and the section that defines it, is printed by the reference implementation in the repository named in Section 16, and the section named for each type says what it means. Each entry carries the identifier, the HTTP status it accompanies, and the section stating the rule it reports. A terms profile that refuses a request defines its own types under its own prefix and reports them as @@ -5276,14 +5256,14 @@

    signature-invalid - 401 + 400 Section 14.1 signature-missing - 401 + 400 Section 14.2 @@ -5388,6 +5368,10 @@

    Liusvaara, I., "CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE)", RFC 8037, DOI 10.17487/RFC8037, , <https://www.rfc-editor.org/info/rfc8037>.
    +
    [RFC9864]
    +
    +Jones, M.B. and O. Steele, "Fully-Specified Algorithms for JSON Object Signing and Encryption (JOSE) and CBOR Object Signing and Encryption (COSE)", RFC 9864, DOI 10.17487/RFC9864, , <https://www.rfc-editor.org/info/rfc9864>.
    +
    [RFC8032]
    Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, DOI 10.17487/RFC8032, , <https://www.rfc-editor.org/info/rfc8032>.
    @@ -5436,17 +5420,17 @@

    Hinden, R. and B. Haberman, "Unique Local IPv6 Unicast Addresses", RFC 4193, DOI 10.17487/RFC4193, , <https://www.rfc-editor.org/info/rfc4193>.
    -
    [I-D.bhutton-json-schema]
    -
    -Wright, A., Andrews, H., Hutton, B., and G. Dennis, "JSON Schema: A Media Type for Describing JSON Documents", Work in Progress, Internet-Draft, draft-bhutton-json-schema-01, , <https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01>.
    -
    [DID-CORE]
    W3C, "Decentralized Identifiers (DIDs) v1.0", W3C Recommendation, , <https://www.w3.org/TR/2022/REC-did-core-20220719/>.
    [DID-WEB]
    +
    +W3C Credentials Community Group, "did:web Method Specification", Unofficial draft, undated; accessed 16 September 2026, <https://w3c-ccg.github.io/did-method-web/>.
    +
    +
    [I-D.bhutton-json-schema]
    -W3C Credentials Community Group, "did:web Method Specification", , <https://w3c-ccg.github.io/did-method-web/>.
    +Wright, A., Andrews, H., Hutton, B., and G. Dennis, "JSON Schema: A Media Type for Describing JSON Documents", Work in Progress, Internet-Draft, draft-bhutton-json-schema-01, , <https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01>.
    @@ -5459,6 +5443,10 @@

    Birkholz, H., Thaler, D., Richardson, M., Smith, N., and W. Pan, "Remote ATtestation procedureS (RATS) Architecture", RFC 9334, DOI 10.17487/RFC9334, , <https://www.rfc-editor.org/info/rfc9334>.
    +
    [RFC6979]
    +
    +Pornin, T., "Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA)", RFC 6979, DOI 10.17487/RFC6979, , <https://www.rfc-editor.org/info/rfc6979>.
    +
    [RFC9711]
    Lundblade, L., Mandyam, G., O'Donoghue, J., and C. Wallace, "The Entity Attestation Token (EAT)", RFC 9711, DOI 10.17487/RFC9711, , <https://www.rfc-editor.org/info/rfc9711>.
    @@ -5483,6 +5471,14 @@

    Watsen, K., Auerswald, E., Farrel, A., and Q. Wu, "Handling Long Lines in Content of Internet-Drafts and RFCs", RFC 8792, DOI 10.17487/RFC8792, , <https://www.rfc-editor.org/info/rfc8792>.
    +
    [CAIP-2]
    +
    +Chain Agnostic Standards Alliance, "CAIP-2: Blockchain ID Specification", Status: Final, , <https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md>.
    +
    +
    [X402COMPLIANCE]
    +
    +wowlegend (Tersign), pull request author, "Extension: compliance-fields", Open pull request 2853 to x402-foundation/x402, specs/extensions/compliance_fields.md, unmerged as of September 2026, , <https://github.com/x402-foundation/x402/pull/2853>.
    +
    [RFC8555]
    Barnes, R., Hoffman-Andrews, J., McCarney, D., and J. Kasten, "Automatic Certificate Management Environment (ACME)", RFC 8555, DOI 10.17487/RFC8555, , <https://www.rfc-editor.org/info/rfc8555>.
    @@ -5517,15 +5513,15 @@

    [I-D.stone-vcap-ap2-binding]
    -Stone, B. E. N. S. S. T. O. N., "VCAP-AP2 Binding: Verified Delivery Settlement for the Agent Payments Protocol", Work in Progress, Internet-Draft, draft-stone-vcap-ap2-binding-01, , <https://datatracker.ietf.org/doc/html/draft-stone-vcap-ap2-binding-01>.
    +Stone, B., "VCAP-AP2 Binding: Verified Delivery Settlement for the Agent Payments Protocol", Work in Progress, Internet-Draft, draft-stone-vcap-ap2-binding-01, , <https://datatracker.ietf.org/doc/html/draft-stone-vcap-ap2-binding-01>.
    [I-D.sahu-agent-action-receipts]
    -sahu, N., "Signed, Hash-Chained Action Receipts for AI Agents", Work in Progress, Internet-Draft, draft-sahu-agent-action-receipts-00, , <https://datatracker.ietf.org/doc/html/draft-sahu-agent-action-receipts-00>.
    +Sahu, N., "Signed, Hash-Chained Action Receipts for AI Agents", Work in Progress, Internet-Draft, draft-sahu-agent-action-receipts-00, , <https://datatracker.ietf.org/doc/html/draft-sahu-agent-action-receipts-00>.
    [I-D.mih-sato-agent-accountability-composition]
    -Mih, S., Sato, Schrock, I., Bu, S., and A. Sokolov, "Agent Accountability: Composition and Conformance", Work in Progress, Internet-Draft, draft-mih-sato-agent-accountability-composition-01, , <https://datatracker.ietf.org/doc/html/draft-mih-sato-agent-accountability-composition-01>.
    +Mih, S., Sato, T., Schrock, I., Bu, S., and A. Sokolov, "Agent Accountability: Composition and Conformance", Work in Progress, Internet-Draft, draft-mih-sato-agent-accountability-composition-01, , <https://datatracker.ietf.org/doc/html/draft-mih-sato-agent-accountability-composition-01>.
    [I-D.asor-wimse-agent-delegation-chain]
    @@ -5539,6 +5535,10 @@

    Sharma, L., "PACT: Liability and Settlement for Autonomous Agent Contracts", Internet-Draft, draft-laxsharma-pact-01, superseded by this document, , <https://www.ietf.org/archive/id/draft-laxsharma-pact-01.html>.
    +
    [I-D.laxsharma-pact-00]
    +
    +Sharma, L., "PACT: A Contract Layer for Autonomous Agent Commerce", Internet-Draft, draft-laxsharma-pact-00, superseded, , <https://www.ietf.org/archive/id/draft-laxsharma-pact-00.html>.
    +
    [ASOKAN98]
    Asokan, N., Shoup, V., and M. Waidner, "Asynchronous Protocols for Optimistic Fair Exchange", Proceedings of the IEEE Symposium on Security and Privacy, , <https://doi.org/10.1109/secpri.1998.674826>.
    @@ -5569,13 +5569,11 @@

    in Section 1.4 has something to run against and the vectors in the reference repository have something to reproduce. It is the -01 revision's settlement content written as a schedule over - the events of Section 4.2, with the choices the -01 - revision left open now made, and it is offered as an example of the + the events of Section 4.2, with the choices the -01 revision left open now made and two of its own choices changed where the arithmetic or its text required (Appendix A.5), and it is offered as an example of the form a profile takes, not as a recommendation of these terms. What the figures below mean between the parties to a contract that names this profile is a question this document does not answer and its author is - not qualified to answer; a profile meant for use needs an owner who - is.

    + not qualified to answer; a profile meant for use needs an owner who is. Until such a profile exists, this one is also the only profile a Facilitator can list, since terms_profiles must have an entry; that is a fact about the present and not a rule of this document.

    @@ -5585,7 +5583,7 @@

    tag:laxsharma79@gmail.com,2026:pact:bonded-restitution. The bundle in the reference repository, under profiles/bonded-restitution/, contains - README.md (this text), parameters.schema.json and + README.md (the prose of this appendix, in Markdown), parameters.schema.json and vectors.json; profile_hash is the manifest digest over those three files and Section 15 prints it. Problem types this profile reports are under the prefix @@ -5606,14 +5604,12 @@

    verification_fund:
    -
    amount, required. What - the Buyer posts to pay for checking. +
    amount, required. What the Seller posts to pay for checking; the -01 prose never said who posts it and its figure drew it from the Seller, which this profile follows.
    cap:
    -
    amount, required. The most that leaves - the Seller's accounts under this contract. +
    amount, required. The most that leaves the bond under this contract; it bounds ranks 3 to 5 together, and what the bond holds beyond it returns to the Seller.
    @@ -5644,13 +5640,20 @@

    assurance:
    object, required. mode - (certain, committed-sample or open) and - q_min (a number greater than zero and at most one). + (certain, committed-sample or open), + q_min (a number greater than zero and at most one) and, + under committed-sample, sample_rate (a number + greater than zero and at most one: the declared fraction of + deliveries verified; the draw MUST derive from a seed the Buyer committed before the Delivery was submitted, combined with the Delivery's digest; how the seed is committed is outside the + profile).
    -

    The -01 revision's four release modes map onto flow and - principal_on as Appendix B shows.

    +

    This profile defines no Challenge deposit; a Facilitator that + advertises challenge_deposit does not do so under this + profile.

    +

    The -01 revision's four release modes map onto flow and + principal_on as Appendix B shows.

    @@ -5684,12 +5687,14 @@

    not hold, or when assurance.mode is open alone. The inequality is the classical deterrence bound ([POLINSKY99]; [BELENKIY08] Theorem 1 - for outsourced computation), with E the one term the -01 revision - added: value that moved before a Verdict cannot be recovered by the - schedule, so it raises what the Seller must post one for one. A + for outsourced computation), with E the one term the -01 revision added: principal that moves before any Verdict is outside what the Verifier's check can withhold, so it raises what the Seller must post one for one. The bound deters nonconformance against that check and says nothing about what a later Challenge recovers; after a PASS is overturned the restitution of the schedule is bounded by the bond and the cap, whatever principal_on was. A contract whose seller_bond or verification_fund exceeds cap is reported as parameters-inconsistent.

    +

    The rule is falsified, and this profile with it, if the constraint + proves unworkable at the prices and verification costs real + deployments exhibit. That was the -01 revision's own failure + condition, restated here where the rule now lives.

    @@ -5706,7 +5711,7 @@

    funded:
    buyer to escrow, P, lock; - seller to bond, B, bond; buyer to fund, + seller to bond, B, bond; seller to fund, verification_fund, fund.
    @@ -5737,7 +5742,7 @@

    terminal, FINAL:
    escrow to seller, the escrow balance, principal; bond to seller, the bond balance, - return; fund to buyer, the fund balance, + return; fund to seller, the fund balance, fund-return.
    @@ -5745,11 +5750,8 @@

    terminal, ABANDONED:
    escrow to buyer, the escrow balance, reverse; bond to seller, the bond balance, - return; fund to buyer, the fund balance, - fund-return. The -01 revision said the bond was slashed - "to the extent of" the basis here and never said by how much; with - the price reversed the Buyer's loss is zero under either basis, so - nothing is slashed. + return; fund to seller, the fund balance, + fund-return. The -01 revision said the bond was slashed "to the extent of" the basis here, and its Section 5.3 defined the basis as an amount, the value already released or the full price, without relating either to a loss; with the price reversed the Buyer's loss is zero under either basis, so this profile slashes nothing here, which under basis price is a departure.
    @@ -5758,21 +5760,14 @@

    only what remains. (1) escrow to buyer, the escrow balance, reverse. (2) if challenge_upheld: fund to the Challenger whose Challenge the standing Verdict answers, the lesser - of that Challenge's costs and the fund balance, - costs. (3) bond to buyer, the lesser of the bond balance, - cap, and the Buyer's loss, restitution; the loss + of that Challenge's costs when stated in the contract's currency (otherwise nothing) and the fund balance, + costs. (3) bond to buyer, the lesser of the bond balance, the cap room and the Buyer's loss, restitution; the loss is "released" under basis released and P minus the rank-1 - entry under basis price, which differ only when the price - moved in part. (4) if challenge_upheld: bond to that - Challenger, the bond balance, bounty. (5) bond to buyer or - sink per remainder_to, the bond balance, - remainder. Then fund to buyer, the fund balance, - fund-return. + entry under basis price, which coincide under this schedule, since every principal entry moves the whole escrow balance; the parameter is kept for a profile that adds partial release. (4) if challenge_upheld: bond to that Challenger, the lesser of the bond balance and the cap room, bounty. (5) bond to buyer or sink per remainder_to, the lesser of the bond balance and the cap room, remainder. Then bond to seller, the bond balance, return, which is what the cap kept; then fund to seller, the fund balance, fund-return. The cap room at each rank is cap less what the entries so far have moved out of the bond.

    -

    Ranks 2 and 4 pay one Challenger, the one whose Challenge the - standing Verdict answers. A Challenge that was not answered by the +

    Ranks 2 and 4 pay one Challenger, the one whose Challenge the standing Verdict answers. The -01 revision required the reward to be non-exclusive, paying every independent discoverer in full; one bond cannot fund that for two discoverers, so this profile pays one and records the departure here. A Challenge that was not answered by the standing Verdict, whether lapsed, rejected or superseded, receives nothing. Rank 4 gives the whole remaining bond, because the -01 revision forbade capping it at a fraction chosen for tidiness and @@ -5791,10 +5786,8 @@

    with q 1.0, under the verdict-first flow, and a Challenge claiming costs of 1.20. Amounts are in USDC. Trace indexes count from zero. The lists below are what vectors.json carries - for the two paths in the figures of this document; the repository's - file also carries the SETTLED-by-Verifier and ABANDONED paths and - the price basis.

    -
    + for the two paths in the figures of this document; the repository's file also carries the SETTLED-by-Verifier, ABANDONED, verdict-lapsed and delivery-first paths, the price basis, and two admission vectors, one refused and one admitted at the boundary of the constraint.

    +
    @@ -5805,14 +5798,14 @@ 

    event from to amount code 1 buyer escrow 180.00 lock 1 seller bond 18.00 bond - 1 buyer fund 0.50 fund + 1 seller fund 0.50 fund 3 escrow seller 180.00 principal 7 bond seller 18.00 return - 7 fund buyer 0.50 fund-return + 7 fund seller 0.50 fund-return

    Figure 14: -FINAL: the path of Figure 1 +FINAL: the path of Figure 1
    @@ -5827,14 +5820,14 @@

    event from to amount code 1 buyer escrow 180.00 lock 1 seller bond 18.00 bond - 1 buyer fund 0.50 fund + 1 seller fund 0.50 fund 3 escrow seller 180.00 principal 8 fund challenger:<kid> 0.50 costs 8 bond buyer 18.00 restitution

    Figure 15: -SETTLED on an upheld Challenge: the path of Figure 5 +SETTLED on an upheld Challenge: the path of Figure 7

    In the second vector rank 1 emits nothing because the escrow is @@ -5854,9 +5847,7 @@

    This revision separates the protocol from the meaning of its terms. The -01 revision, in its title, abstract, Section 1.2 and throughout, - made who owed whom the subject of the document; two readers on the - IETF dispatch list observed in September 2026 that this placed it - outside what the IETF is placed to evaluate, and they were right. What + made who owed whom the subject of the document; one reader on the IETF dispatch list, Rich Salz, read it in September 2026 as a legal framework with a protocol attached, and another, John C Klensin, wrote that its framing was tied closely enough to legal terminology that the IETF was the wrong place to evaluate it; both were right. What follows is the list of what changed, with the wire consequences first.

      @@ -5927,8 +5918,7 @@

      child outcome supply, child-unresolved, a finite latest finality instant per contract and the rule L(child) before L(parent); the depth and cycle rules are withdrawn with the reason - (Section 10). The -01 Section 10.2 is one sentence in - Section 10.3. + (Section 10). The -01 Section 10.2, which had liability cascade upward as recovery and not downward as discharge, is withdrawn to the profile; Section 10.3 says only that this document does not state what a child's outcome means for its parent.
    • Section 3 is a data dictionary and a role table (Section 3); no sentence in it requires anything of @@ -5947,6 +5937,36 @@

    • The experiment is restated over protocol observables (Section 1.4). +
    • +
    • A contract carries exactly one signature per party and no other; + the -01 revision accepted further signers (Section 14.2). +
    • +
    • + verification.arbiter is withdrawn; nothing read it. +
    • +
    • The capability document gains issued_at and + retrieval, and its flows must list verdict-first (Section 7.1). The + well-known URI is registered provisionally, with the author as change + controller. +
    • +
    • A Verdict is accepted while one stands only in answer to a + Challenge (Section 4.2). +
    • +
    • The -01 rule that confidential content MUST NOT declare open + assurance is the profile's now; Appendix A.4 + refuses open assurance alone. +
    • +
    • + signature-invalid and signature-missing are 400 + and not 401, since no HTTP authentication scheme is involved; V-26 + names the protected-header members a verifier rejects + (Section 14.1). +
    • +
    • + Section 16 names the repository licence, Apache License 2.0; the -01 said Revised BSD, which was wrong. +
    • +
    • + alg names are the fully specified ones of [RFC9864], Ed25519 for an Ed25519 key; the polymorphic EdDSA identifier the -01 used is refused.
    @@ -5956,11 +5976,12 @@

    Acknowledgements

    -

    Rich Salz and John C Klensin, on the IETF dispatch list in - September 2026, read the -01 revision as a document about who owes - whom with a protocol attached, and said so; this revision's split - between records and terms is the consequence, and the author is - grateful for the reading. The UTF-16 key-ordering vector that exposed +

    On the IETF dispatch list in September 2026, Rich Salz read the -01 + revision as a legal framework with a protocol attached and said so, and + John C Klensin wrote that the framing of its terms was tied closely enough + to legal terminology that the IETF was the wrong place to evaluate it. + Both were right; this revision's split between records and terms is + the consequence, and the author is grateful for the reading. The UTF-16 key-ordering vector that exposed a latent canonicalization defect in the reference validator, and the formulation of verifier independence as a relation the evaluator derives rather than a field the record declares, came from Tersign diff --git a/draft/draft-laxsharma-pact-02.txt b/draft/draft-laxsharma-pact-02.txt index de549ff..d40e2a8 100644 --- a/draft/draft-laxsharma-pact-02.txt +++ b/draft/draft-laxsharma-pact-02.txt @@ -15,22 +15,19 @@ Expires: 20 March 2027 Abstract Autonomous agents can already prove who they are, show whose - authority they act under, find one another, call one another, and - pay. What they cannot do with any existing specification is agree on - a task in a form a third party can check, deliver against it, have - the delivery judged by someone other than the performer, and carry - away a record of the outcome that a stranger can verify. This - document specifies PACT, a set of signed JSON records that closes - that gap. + authority they act under, find and call one another, and pay. What + no existing specification lets them do is agree on a task in a form a + third party can check, deliver against it, have the delivery judged + by someone other than the performer, and carry away a record of the + outcome that a stranger can verify. This document specifies PACT, a + set of signed JSON records that closes that gap. PACT defines four things: a co-signed task contract whose digest - covers its signature set, so the commitment proves who agreed and not - only what was written; a Verdict record bound by digest to the - Delivery record it judges; a Facilitator-signed event trace and - Outcome Record for every contract, so what happened is recorded once, - in one order, by a party that is not the performer; and a Merkle - commitment from a parent contract's Outcome Record to the Outcome - Records of its subcontracts. + covers its signature set; a Verdict record bound by digest to the + Delivery it judges; a Facilitator-signed event trace and Outcome + Record for every contract, recorded once, in one order, by a party + other than the performer; and a Merkle commitment from a parent's + Outcome Record to its subcontracts' Outcome Records. Settlement terms are carried by reference to a profile defined outside this document. This document specifies no escrow, custody or @@ -47,9 +44,12 @@ Status of This Memo working documents as Internet-Drafts. The list of current Internet- Drafts is at https://datatracker.ietf.org/drafts/current/. + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." - - + This Internet-Draft will expire on 20 March 2027. @@ -58,13 +58,6 @@ Sharma Expires 20 March 2027 [Page 1] Internet-Draft PACT September 2026 - Internet-Drafts are draft documents valid for a maximum of six months - and may be updated, replaced, or obsoleted by other documents at any - time. It is inappropriate to use Internet-Drafts as reference - material or to cite them other than as "work in progress." - - This Internet-Draft will expire on 20 March 2027. - Copyright Notice Copyright (c) 2026 IETF Trust and the persons identified as the @@ -83,8 +76,8 @@ Table of Contents 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4 1.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . 4 - 1.2. What This Document Specifies, and What It Does Not . . . 5 - 1.3. Relationship to Existing Work . . . . . . . . . . . . . . 6 + 1.2. What This Document Specifies, and What It Does Not . . . 4 + 1.3. Relationship to Existing Work . . . . . . . . . . . . . . 5 1.4. The Experiment . . . . . . . . . . . . . . . . . . . . . 7 2. Conventions and Definitions . . . . . . . . . . . . . . . . . 7 2.1. Terminology . . . . . . . . . . . . . . . . . . . . . . . 9 @@ -98,14 +91,21 @@ Table of Contents 3.7. Contract Status Members . . . . . . . . . . . . . . . . . 14 3.8. Outcome Record Members . . . . . . . . . . . . . . . . . 14 3.9. Capability Document Members . . . . . . . . . . . . . . . 15 - 3.10. Roles . . . . . . . . . . . . . . . . . . . . . . . . . . 15 - 4. Protocol Overview . . . . . . . . . . . . . . . . . . . . . . 16 + 3.10. Roles . . . . . . . . . . . . . . . . . . . . . . . . . . 16 + 4. Protocol Overview . . . . . . . . . . . . . . . . . . . . . . 17 4.1. States . . . . . . . . . . . . . . . . . . . . . . . . . 17 4.2. Events . . . . . . . . . . . . . . . . . . . . . . . . . 18 5. The Verifiable Task Contract . . . . . . . . . . . . . . . . 21 5.1. Hash Commitments and Content Conveyance . . . . . . . . . 23 - 5.2. The Task Specification . . . . . . . . . . . . . . . . . 23 + 5.2. The Task Specification . . . . . . . . . . . . . . . . . 24 5.3. Terms . . . . . . . . . . . . . . . . . . . . . . . . . . 24 + 6. The Delivery Record . . . . . . . . . . . . . . . . . . . . . 25 + 7. Verdicts, Challenges and the Window . . . . . . . . . . . . . 26 + 7.1. Flows . . . . . . . . . . . . . . . . . . . . . . . . . . 27 + 7.2. Verdicts . . . . . . . . . . . . . . . . . . . . . . . . 27 + 7.3. Challenges . . . . . . . . . . . . . . . . . . . . . . . 29 + 7.4. Disputes and Lapses . . . . . . . . . . . . . . . . . . . 30 + 8. Facilitator Capability Discovery . . . . . . . . . . . . . . 31 @@ -114,54 +114,54 @@ Sharma Expires 20 March 2027 [Page 2] Internet-Draft PACT September 2026 - 6. The Delivery Record . . . . . . . . . . . . . . . . . . . . . 25 - 7. Verdicts, Challenges and the Window . . . . . . . . . . . . . 26 - 7.1. Flows . . . . . . . . . . . . . . . . . . . . . . . . . . 26 - 7.2. Verdicts . . . . . . . . . . . . . . . . . . . . . . . . 27 - 7.3. Challenges . . . . . . . . . . . . . . . . . . . . . . . 28 - 7.4. Disputes and Lapses . . . . . . . . . . . . . . . . . . . 29 - 8. Facilitator Capability Discovery . . . . . . . . . . . . . . 30 - 9. Verification Profiles . . . . . . . . . . . . . . . . . . . . 31 - 9.1. Verifier Independence and Identifier Normalization . . . 32 - 10. Contract Trees . . . . . . . . . . . . . . . . . . . . . . . 33 - 10.1. Binding a Child to Its Parent . . . . . . . . . . . . . 33 - 10.2. Registration and Children Final . . . . . . . . . . . . 34 - 10.3. Finality Is Bottom-Up . . . . . . . . . . . . . . . . . 35 - 11. The Contract Status . . . . . . . . . . . . . . . . . . . . . 36 - 12. Outcome Records . . . . . . . . . . . . . . . . . . . . . . . 38 - 12.1. The Terms Result . . . . . . . . . . . . . . . . . . . . 39 - 12.2. The Children Merkle Root . . . . . . . . . . . . . . . . 40 - 13. Protocol Endpoints . . . . . . . . . . . . . . . . . . . . . 41 - 13.1. Proposing a Contract . . . . . . . . . . . . . . . . . . 42 - 13.2. Idempotency . . . . . . . . . . . . . . . . . . . . . . 42 - 13.3. Error Responses . . . . . . . . . . . . . . . . . . . . 43 - 13.4. Exchange . . . . . . . . . . . . . . . . . . . . . . . . 43 - 14. Conformance . . . . . . . . . . . . . . . . . . . . . . . . . 44 - 14.1. Signatures . . . . . . . . . . . . . . . . . . . . . . . 44 - 14.1.1. Key Resolution . . . . . . . . . . . . . . . . . . . 45 - 14.2. Rules Not Expressible in a Schema . . . . . . . . . . . 45 - 14.3. Test Vectors . . . . . . . . . . . . . . . . . . . . . . 46 - 15. Worked Example . . . . . . . . . . . . . . . . . . . . . . . 48 - 16. Implementation Status . . . . . . . . . . . . . . . . . . . . 49 - 17. Security Considerations . . . . . . . . . . . . . . . . . . . 49 - 17.1. Trust in the Facilitator . . . . . . . . . . . . . . . . 51 - 17.2. Verifier Capture . . . . . . . . . . . . . . . . . . . . 51 - 17.3. Algorithm, Key and Encoding Confusion . . . . . . . . . 52 - 17.4. Substitution of Committed Content . . . . . . . . . . . 52 - 17.5. Fetching Committed Content . . . . . . . . . . . . . . . 52 - 17.6. Children: Attachment and Omission . . . . . . . . . . . 53 - 17.7. Buying Silence from a Challenger . . . . . . . . . . . . 53 - 17.8. Non-Delivery . . . . . . . . . . . . . . . . . . . . . . 53 - 17.9. Cross-Venue Replay . . . . . . . . . . . . . . . . . . . 53 - 17.10. Nondeterminism as Shield and as Weapon . . . . . . . . . 54 - 17.11. Fabricated History . . . . . . . . . . . . . . . . . . . 54 - 17.12. Retrieval . . . . . . . . . . . . . . . . . . . . . . . 54 - 17.13. Key Compromise and Rotation . . . . . . . . . . . . . . 54 - 17.14. Denial of Service by Challenge . . . . . . . . . . . . . 55 - 18. Privacy Considerations . . . . . . . . . . . . . . . . . . . 55 - 18.1. Input Disclosure Before Contract Formation . . . . . . . 55 - 18.2. The Contract Graph . . . . . . . . . . . . . . . . . . . 55 - 18.3. Challenger Access . . . . . . . . . . . . . . . . . . . 56 + 9. Verification Profiles . . . . . . . . . . . . . . . . . . . . 33 + 9.1. Verifier Independence and Identifier Normalization . . . 34 + 10. Contract Trees . . . . . . . . . . . . . . . . . . . . . . . 35 + 10.1. Binding a Child to Its Parent . . . . . . . . . . . . . 35 + 10.2. Registration and Children Final . . . . . . . . . . . . 36 + 10.3. Finality Is Bottom-Up . . . . . . . . . . . . . . . . . 37 + 11. The Contract Status . . . . . . . . . . . . . . . . . . . . . 38 + 12. Outcome Records . . . . . . . . . . . . . . . . . . . . . . . 40 + 12.1. The Terms Result . . . . . . . . . . . . . . . . . . . . 43 + 12.2. The Children Merkle Root . . . . . . . . . . . . . . . . 44 + 13. Protocol Endpoints . . . . . . . . . . . . . . . . . . . . . 44 + 13.1. Proposing a Contract . . . . . . . . . . . . . . . . . . 46 + 13.2. Idempotency . . . . . . . . . . . . . . . . . . . . . . 46 + 13.3. Error Responses . . . . . . . . . . . . . . . . . . . . 47 + 13.4. Exchange . . . . . . . . . . . . . . . . . . . . . . . . 47 + 14. Conformance . . . . . . . . . . . . . . . . . . . . . . . . . 48 + 14.1. Signatures . . . . . . . . . . . . . . . . . . . . . . . 48 + 14.1.1. Key Resolution . . . . . . . . . . . . . . . . . . . 49 + 14.2. Rules Not Expressible in a Schema . . . . . . . . . . . 50 + 14.3. Test Vectors . . . . . . . . . . . . . . . . . . . . . . 50 + 15. Worked Example . . . . . . . . . . . . . . . . . . . . . . . 52 + 16. Implementation Status . . . . . . . . . . . . . . . . . . . . 54 + 17. Security Considerations . . . . . . . . . . . . . . . . . . . 54 + 17.1. Trust in the Facilitator . . . . . . . . . . . . . . . . 56 + 17.2. Verifier Capture . . . . . . . . . . . . . . . . . . . . 56 + 17.3. Algorithm, Key and Encoding Confusion . . . . . . . . . 57 + 17.4. Substitution of Committed Content . . . . . . . . . . . 57 + 17.5. Fetching Committed Content . . . . . . . . . . . . . . . 57 + 17.6. Children: Attachment and Omission . . . . . . . . . . . 58 + 17.7. Buying Silence from a Challenger . . . . . . . . . . . . 58 + 17.8. Non-Delivery . . . . . . . . . . . . . . . . . . . . . . 58 + 17.9. Cross-Venue Replay . . . . . . . . . . . . . . . . . . . 58 + 17.10. Nondeterminism as Shield and as Weapon . . . . . . . . . 59 + 17.11. Fabricated History . . . . . . . . . . . . . . . . . . . 59 + 17.12. Retrieval . . . . . . . . . . . . . . . . . . . . . . . 59 + 17.13. Key Compromise and Rotation . . . . . . . . . . . . . . 59 + 17.14. Denial of Service by Challenge . . . . . . . . . . . . . 60 + 18. Privacy Considerations . . . . . . . . . . . . . . . . . . . 60 + 18.1. Input Disclosure Before Contract Formation . . . . . . . 60 + 18.2. The Contract Graph . . . . . . . . . . . . . . . . . . . 60 + 18.3. Challenger Access . . . . . . . . . . . . . . . . . . . 61 + 18.4. Retention . . . . . . . . . . . . . . . . . . . . . . . 61 + 19. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 61 + 19.1. Media Types . . . . . . . . . . . . . . . . . . . . . . 61 + 19.2. Well-Known URI . . . . . . . . . . . . . . . . . . . . . 63 + 19.3. Problem Types . . . . . . . . . . . . . . . . . . . . . 64 + 20. Normative References . . . . . . . . . . . . . . . . . . . . 65 + 21. Informative References . . . . . . . . . . . . . . . . . . . 68 @@ -170,23 +170,16 @@ Sharma Expires 20 March 2027 [Page 3] Internet-Draft PACT September 2026 - 18.4. Retention . . . . . . . . . . . . . . . . . . . . . . . 56 - 19. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 56 - 19.1. Media Types . . . . . . . . . . . . . . . . . . . . . . 56 - 19.2. Well-Known URI . . . . . . . . . . . . . . . . . . . . . 58 - 19.3. Problem Types . . . . . . . . . . . . . . . . . . . . . 59 - 20. Normative References . . . . . . . . . . . . . . . . . . . . 60 - 21. Informative References . . . . . . . . . . . . . . . . . . . 62 - Appendix A. An Example Terms Profile: bonded-restitution . . . . 66 - A.1. Identity and Bundle . . . . . . . . . . . . . . . . . . . 66 - A.2. Parameters . . . . . . . . . . . . . . . . . . . . . . . 66 - A.3. Accounts . . . . . . . . . . . . . . . . . . . . . . . . 67 - A.4. Admission . . . . . . . . . . . . . . . . . . . . . . . . 67 - A.5. Schedule . . . . . . . . . . . . . . . . . . . . . . . . 67 - A.6. Vectors . . . . . . . . . . . . . . . . . . . . . . . . . 69 - Appendix B. Changes from -01 . . . . . . . . . . . . . . . . . . 70 - Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 71 - Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 72 + Appendix A. An Example Terms Profile: bonded-restitution . . . . 72 + A.1. Identity and Bundle . . . . . . . . . . . . . . . . . . . 72 + A.2. Parameters . . . . . . . . . . . . . . . . . . . . . . . 72 + A.3. Accounts . . . . . . . . . . . . . . . . . . . . . . . . 73 + A.4. Admission . . . . . . . . . . . . . . . . . . . . . . . . 73 + A.5. Schedule . . . . . . . . . . . . . . . . . . . . . . . . 74 + A.6. Vectors . . . . . . . . . . . . . . . . . . . . . . . . . 75 + Appendix B. Changes from -01 . . . . . . . . . . . . . . . . . . 76 + Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 79 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 79 1. Introduction @@ -204,17 +197,24 @@ Internet-Draft PACT September 2026 to have that result judged by a third implementation against criteria fixed before the work began, and no record of the outcome that a fourth implementation can verify without trusting any of the first - three. Receipts record that an action occurred. Audit records - establish whether behaviour matched intent. Payment schemes move + three. Receipts record that an action occurred, audit records + establish whether behaviour matched intent, and payment schemes move value on the payer's instruction. None of them says what was agreed, what was delivered, or whether the one met the other. - That gap is not an oversight in those documents; it is outside their + Those documents leave that gap on purpose, since it is outside their scope, and correctly so. It is the gap this document addresses, and only that gap. +1.2. What This Document Specifies, and What It Does Not - + PACT specifies exactly four things: a co-signed contract record whose + digest covers its signature set (Section 5); a Delivery record and + the Verdict record bound to it by digest (Section 6, Section 7.2); an + event trace, signed by a Facilitator, from which one Outcome Record + per contract is produced (Section 11, Section 12); and a Merkle + commitment from a parent's Outcome Record to its children's + (Section 12.2). @@ -226,23 +226,16 @@ Sharma Expires 20 March 2027 [Page 4] Internet-Draft PACT September 2026 -1.2. What This Document Specifies, and What It Does Not - - PACT specifies exactly four things: a co-signed contract record whose - digest covers its signature set (Section 5); a Delivery record and - the Verdict record bound to it by digest (Section 6, Section 7.2); an - event trace, signed by a Facilitator, from which one Outcome Record - per contract is produced (Section 11, Section 12); and a Merkle - commitment from a parent's Outcome Record to its children's - (Section 10). - A contract names its settlement terms by reference: a profile identifier, a digest over the profile's bytes, and a parameter object that this document does not read (Section 5.3). What those terms mean, and everything about who holds or moves value under them, is the profile's to say. This document specifies the records, their digests, who signs each one, the order in which a Facilitator records - events, and a commitment across records. That is the whole of it. + events, and a commitment across records. That is the whole of it. A + contract carries a price and names a settlement binding, since a task + contract without them is not one; what happens to the price is the + profile's, and what the binding reports is the binding's. A deployment relies on other specifications, agreements or arrangements for: the meaning of the terms a contract names; agent @@ -254,23 +247,30 @@ Internet-Draft PACT September 2026 do not settle. Carrying terms by reference is an old pattern in this series. ACME - [RFC8555] carries a terms-of-service URL and requires a client to - assert agreement to it before an account is created, without defining - a single term. A certificate carries its policy as an identifier - whose rules live outside the IETF ([RFC5280], Section 4.2.1.4), and - the framework for writing those rules [RFC3647] says it does not aim - to provide legal advice. The Internet Open Trading Protocol - [RFC2801] specified the messages of a trade and left the trade's - terms to the parties. PACT follows that line. - - Two mechanisms present in the -00 revision remain withdrawn: contract - channels, and the sealed-bid award procedure. The reasons are - recorded in [I-D.laxsharma-pact-01] and are not repeated. The change - from -01 to this revision is listed in Appendix B. - - + [RFC8555] carries a terms-of-service URL and, where a server chooses + to require it, has the client assert agreement to those terms before + an account is created, without defining a single term. A certificate + carries its policy as an identifier whose rules live outside the IETF + ([RFC5280], Section 4.2.1.4), and the framework for writing those + rules [RFC3647] says it does not aim to provide legal advice. The + Internet Open Trading Protocol [RFC2801] specified the messages of a + trade and left the trade's terms to the parties. PACT follows that + line. + + Two mechanisms present in the -00 revision [I-D.laxsharma-pact-00] + remain withdrawn: contract channels, and the sealed-bid award + procedure. The reasons are recorded in [I-D.laxsharma-pact-01] and + are not repeated. The change from -01 to this revision is listed in + Appendix B. +1.3. Relationship to Existing Work + PACT's agree, perform, verify, record loop is an instance of + optimistic fair exchange [ASOKAN98], in which a third party is + contacted only when the exchange fails. What that literature + establishes is what a third party must be able to observe for an + exchange to be fair; the records in this document are that + observation, written down in a form two implementations can compare. @@ -282,21 +282,12 @@ Sharma Expires 20 March 2027 [Page 5] Internet-Draft PACT September 2026 -1.3. Relationship to Existing Work - - PACT's agree, perform, verify, record loop is an instance of - optimistic fair exchange [ASOKAN98], in which a third party is - contacted only when the exchange fails. What that literature - establishes is what a third party must be able to observe for an - exchange to be fair; the records in this document are that - observation, written down in a form two implementations can compare. - Two adjacent Internet-Drafts address agent commerce settlement directly. [I-D.hood-agtp-commerce] carries Work Completion Records and an audit-verified settlement timing; [I-D.stone-vcap-ap2-binding] binds verified commerce settlement to the Agent Payments Protocol. - Neither carries a co-signed contract whose digest covers its - signatures, and PACT is designed to be usable alongside either. + This document binds to neither and is designed to be usable alongside + either. Five bodies of IETF work touch the same records, and the relationship to each is stated here so that it is not left to the reader. @@ -329,6 +320,15 @@ Internet-Draft PACT September 2026 (Section 13.2). Error reporting follows [RFC9457]. WIMSE. [I-D.ietf-wimse-aims] gives workload and agent identity a + home. PACT does not define an identity format; a kid resolves as + Section 14.1.1 says. An identity system defined elsewhere is used + by naming its identifiers in one of the two forms that section + resolves; a further form needs one resolution rule added there, + and nothing else in this document changes. + + SATP. [I-D.ietf-satp-core] transfers a digital asset between two + + @@ -338,22 +338,15 @@ Sharma Expires 20 March 2027 [Page 6] Internet-Draft PACT September 2026 - home. PACT does not define an identity format; a kid resolves as - Section 14.1.1 says, and that section is written so that an - identity system defined elsewhere can be named without changing - this document. - - SATP. [I-D.ietf-satp-core] transfers a digital asset between two gateways with evidence a third party can verify. An Outcome - Record is not an asset transfer and does not move one; it is a - signed statement that certain records were received in a certain - order, and what any of that means for an asset is the terms - profile's to say. - - Verification evidence formats for hardware-attested tiers are - specified in [RFC9334] and [RFC9711]. Signed, hash-chained action - receipts [I-D.sahu-agent-action-receipts], composition of - accountability records + Record moves no asset. It is a signed statement that certain + records were received in a certain order, and what any of that + means for an asset is the terms profile's to say. + + Verification evidence for hardware-attested tiers follows the + architecture of [RFC9334] and the EAT format of [RFC9711]. Signed, + hash-chained action receipts [I-D.sahu-agent-action-receipts], + composition of accountability records [I-D.mih-sato-agent-accountability-composition], delegation chains [I-D.asor-wimse-agent-delegation-chain], and contestability bindings [I-D.pinto-agent-authz-contestability] are each specified elsewhere, @@ -361,12 +354,15 @@ Internet-Draft PACT September 2026 1.4. The Experiment - This document is Experimental. The question it tests is stated over - protocol observables only. Given the same sequence of posted records - and the same clock readings, two independent Facilitator - implementations should produce the same event trace (Section 11). - Given the same trace and the same terms profile, they should produce - the same Outcome Record body (Section 12), byte for byte after + This document is Experimental, and an individual submission with no + formal standing in the standards process: no working group has + adopted it and the IETF has not endorsed it. The question it tests + is stated over protocol observables only. Given the same sequence of + posted records, the same clock readings and the same reports from the + settlement binding, two independent Facilitator implementations + should produce the same event trace (Section 4.2). Given the same + trace and the same terms profile, they should produce the same + Outcome Record body (Section 12), byte for byte after canonicalization. The experiment succeeds if two independent Facilitators, serving Buyers and Sellers built by different implementers, reach every terminal state in Figure 2 with Outcome @@ -386,6 +382,10 @@ Internet-Draft PACT September 2026 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. + Canonical form. Every JSON object defined here is canonicalized with + JCS [RFC8785] before hashing or signing. Implementations MUST order + object keys by UTF-16 code unit as [RFC8785] Section 3.2.3 requires. + Sorting by Unicode code point is a common substitution; it agrees @@ -394,14 +394,11 @@ Sharma Expires 20 March 2027 [Page 7] Internet-Draft PACT September 2026 - Canonical form. Every JSON object defined here is canonicalized with - JCS [RFC8785] before hashing or signing. Implementations MUST order - object keys by UTF-16 code unit as [RFC8785] Section 3.2.3 requires. - Sorting by Unicode code point is a common substitution; it agrees - with the required order throughout the Basic Multilingual Plane and - diverges above it. Numbers MUST be serialized as [RFC8785] - Section 3.2.2.3 requires, which is how ECMAScript prints them: the - number one is 1, whatever type held it, and never 1.0. + with the required order until a key outside the Basic Multilingual + Plane is compared with one whose first differing unit lies in U+E000 + to U+FFFF, where the two orders disagree. Numbers MUST be serialized + as [RFC8785] Section 3.2.2.3 requires, which is how ECMAScript prints + them: the number one is 1, whatever type held it, and never 1.0. Figures. A figure line that would exceed the page width is folded with the single backslash strategy of [RFC8792], and a figure that @@ -414,9 +411,10 @@ Internet-Draft PACT September 2026 of the whole object, including every signature member it carries. Every hash member in this document that names another object (vtc_hash, delivery_hash, challenge_hash, the object member of a - trace entry, and the leaves of Section 12.2) is that object's digest. - A digest that excluded signatures would prove what was written and - not who agreed to it; the -00 revision had that defect and the -01 + trace entry) is that object's digest, and an element of the list D in + Section 12.2 is the 32 bytes that digest's hexadecimal encodes. A + digest that excluded signatures would prove what was written and not + who agreed to it; the -00 revision had that defect and the -01 revision fixed it for the contract only. This revision applies one construction everywhere. @@ -432,10 +430,12 @@ Internet-Draft PACT September 2026 recognise is inside the commitment and cannot be ignored safely. An implementation MUST reject an object whose pact version it does not implement, and MUST reject an object carrying a member this document - does not define for it, with one exception: the contents of - terms.parameters (Section 5.3) are defined by the named profile and - this document reads none of them. Extension is by a new version, not - by adding members. + does not define for it, with two exceptions: the contents of + terms.parameters (Section 5.3), which the named profile defines and + this document does not read; and the members of a Delivery's + evidence, a Challenge's proof and a TaskSpec's constraints beyond + those Section 3 names, which the verification profile defines. + Extension is by a new version, not by adding members. @@ -454,19 +454,19 @@ Internet-Draft PACT September 2026 the "Z" designator. The Facilitator's clock governs every deadline and window in this document: the instant at which the Facilitator records an event is the instant that counts, that instant is what the - trace carries, and parties should allow for skew when acting near a + trace carries, and parties SHOULD allow for skew when acting near a boundary. Section 17.1 says what that clock can and cannot prove. - Amounts. An amount is a decimal string with no exponent and a - fractional part of two to eighteen digits; comparisons are exact and - no rounding is implied. A currency is an asset identifier whose + Amounts. An amount is a decimal string with no sign, no exponent and + a fractional part of two to eighteen digits; comparisons are exact + and no rounding is implied. A currency is an asset identifier whose namespace is defined by the settlement binding named in price.settlement, and need not be an ISO 4217 code. A network is a - ledger identifier in the form the same binding defines. This - document carries amounts; it does not say what any amount is for. - Where a record produced under this document lists amounts, as - terms_result does (Section 12.1), the meaning of every entry is the - named profile's. + ledger identifier in the form the same binding defines; the examples + use [CAIP-2] chain identifiers. This document carries amounts; it + does not say what any amount is for. Where a record produced under + this document lists amounts, as terms_result does (Section 12.1), the + meaning of every entry is the named profile's. Identifiers. A party identifier is a URI. Two identifiers name the same party when they are equal after the normalization in @@ -475,7 +475,7 @@ Internet-Draft PACT September 2026 2.1. Terminology - Four words in this document have meanings elsewhere that are close + Five words in this document have meanings elsewhere that are close enough to mislead, and are defined here once. Contract: Used in this document for a co-signed JSON object of the @@ -493,11 +493,11 @@ Internet-Draft PACT September 2026 Evidence: The evidence member of a Delivery is the set of artefacts a Verifier evaluates, produced by the Seller. It is not Evidence - in the sense of [RFC9334]. The member name is kept from -01 - because renaming it would change every committed digest for no - gain in clarity that this note does not provide. + in the sense of [RFC9334]. The member name is kept because it is + the ordinary word for what the member holds; the RATS term names a + role in an attestation architecture, and this note is the + disambiguation. - Facilitator: The party that runs the state machine of Section 4 for @@ -506,6 +506,7 @@ Sharma Expires 20 March 2027 [Page 9] Internet-Draft PACT September 2026 + Facilitator: The party that runs the state machine of Section 4 for a contract: it accepts or refuses the records posted to it, records events in one order on its own clock, and signs the trace and the Outcome Record. Nothing in this document says that a @@ -522,11 +523,11 @@ Internet-Draft PACT September 2026 object, and what it commits to. It is a dictionary and not a rulebook: the rule that a record omitting a required member, or carrying one this document does not define for it, does not conform - is stated once in Section 2; the rules a Facilitator applies when it - accepts or refuses a record are in Section 14 and in the section that - defines the record. No sentence in this section requires anything of - any party. Where a member's meaning is the named terms profile's, - the entry says so and says nothing more. + is stated once, in Section 14.2; the rules a Facilitator applies when + it accepts or refuses a record are in Section 14 and in the section + that defines the record. No sentence in this section requires + anything of any party. Where a member's meaning is the named terms + profile's, the entry says so and says nothing more. Types are JSON types. A digest is a string of the form in Section 2. An amount is a string of the form in Section 2. A URI is a string. @@ -556,7 +557,6 @@ Internet-Draft PACT September 2026 - Sharma Expires 20 March 2027 [Page 10] Internet-Draft PACT September 2026 @@ -578,8 +578,8 @@ Internet-Draft PACT September 2026 task: object, required. spec_hash (digest, required) commits to a TaskSpec (Section 5.2); spec_uri (URI, optional) says where its bytes may be fetched; deadline (timestamp, required) is the - instant after which the deadline-passed event may be recorded - (Section 4.2). + instant at or after which the deadline-passed event may be + recorded (Section 4.2). price: object, required. amount (amount, required), currency (string, required), settlement (URI, required, naming a settlement @@ -590,10 +590,10 @@ Internet-Draft PACT September 2026 verification: object, required. tier (string, required), profile (string or URI, required; Section 9), criteria_hash (digest, required; the manifest digest of the acceptance instrument per - Section 5.1), max_verdict_seconds (integer, required; the longest - interval after delivered within which a first Verdict is recorded - before verdict-lapsed may be), arbiter (URI, optional). Commits - to how a Delivery is judged and by what. + Section 5.1), max_verdict_seconds (integer, required, greater than + zero; the longest interval after delivered within which a first + Verdict is recorded before verdict-lapsed may be). Commits to how + a Delivery is judged and by what. flow: string, required. One of verdict-first, delivery-first, no- window (Section 7.1). Selects the shape of the state machine for @@ -619,9 +619,9 @@ Internet-Draft PACT September 2026 greater than zero) is the duration of the challenge window; - max_dispute_seconds (integer, required) is the longest interval - after a challenge event within which a Verdict on that Challenge - is recorded before dispute-lapsed may be. + max_dispute_seconds (integer, required, greater than zero) is the + longest interval after a challenge event within which a Verdict on + that Challenge is recorded before dispute-lapsed may be. parent: object, optional; present only in a subcontract (Section 10). vtc_id (string, required), vtc_hash (digest, @@ -677,14 +677,14 @@ Internet-Draft PACT September 2026 work_uri: URI, optional. Where the bytes may be fetched, subject to Section 17.5. - input_hash: digest, required for tiers whose fraud proof re- - executes. Commits to the production input actually consumed. + input_hash: digest, required for tiers whose proof of nonconformance + re-executes. Commits to the production input actually consumed. evidence: object, required. Members profiled by verification.tier and verification.profile; for the acceptance profile, profile, - instrument_hash, results_hash and results_uri. Conformance to the - profile is a validity condition of the Delivery, not a judgement - on the work. + instrument_hash and results_hash (required) and results_uri + (optional). Conformance to the profile is a validity condition of + the Delivery, not a judgement on the work. 3.5. Verdict Members @@ -705,8 +705,9 @@ Internet-Draft PACT September 2026 verification profile applied and the digest of the instrument actually run, which equals the contract's criteria_hash. - results_hash: digest, required. Commits to the Verifier's own - results. + results_hash: digest, required. The digest of the results document + the verification profile defines; for acceptance, the bytes of the + results file the instrument wrote. evaluated_at: timestamp, required. The Verifier's own clock; informational, since the trace carries the Facilitator's. @@ -720,8 +721,7 @@ Internet-Draft PACT September 2026 contract and commit to the Delivery challenged. proof: object, required. Members profiled by verification.profile; - for the acceptance profile, profile, instrument_hash, - results_hash, results_uri and failing_checks (array of strings). + @@ -730,16 +730,19 @@ Sharma Expires 20 March 2027 [Page 13] Internet-Draft PACT September 2026 - costs: object, optional. amount and currency: a figure the - Challenger asserts for producing the proof. This document records - it in the trace and reads it for nothing; its meaning is the named - terms profile's. + for the acceptance profile, profile, instrument_hash and + results_hash (required), results_uri and failing_checks (array of + strings; optional). + + costs: object, optional. amount and currency: an amount the + Challenger states. This document records it in the trace and + reads it for nothing; its meaning is the named terms profile's. 3.7. Contract Status Members Carried in the Contract Status (Section 11), media type application/ vnd.pact.status+json, the Facilitator's signed response to every - accepted request. + accepted POST. vtc_id, vtc_hash: string and digest, required. @@ -748,7 +751,8 @@ Internet-Draft PACT September 2026 trace: array of objects, required. The event trace so far, in the order recorded (Section 4.2). Each entry carries event (string, required), at (timestamp, required), object (digest, required - where the event was caused by a posted record), and the event- + where the event was caused by a posted record, and on dispute- + lapsed, where it names the Challenge that lapsed), and the event- specific members listed in Section 4.2. issued_at: timestamp, required. When this status was signed. @@ -777,21 +781,18 @@ Internet-Draft PACT September 2026 - - - - Sharma Expires 20 March 2027 [Page 14] Internet-Draft PACT September 2026 profile_hash (copied from the contract), currency (string), and - transfers (array of objects), each with from (string), to - (string), amount (amount) and code (string). The entries are the - named profile's output for the trace; this document defines their - form and two arithmetic invariants over them, and nothing about - their meaning. + transfers (array of objects), each with event (integer, the zero- + based index of the trace entry the transfer follows), from + (string), to (string), amount (amount) and code (string). The + entries are the named profile's output for the trace; this + document defines their form and two arithmetic invariants over + them, and nothing about their meaning. children_merkle_root: digest, required where the contract has registered children and absent otherwise (Section 12.2). @@ -804,36 +805,35 @@ Internet-Draft PACT September 2026 facilitator: URI, required. The identifier that appears in parties.facilitator. + issued_at: timestamp, required. When the document was signed. + Nothing in a document survives its Facilitator withdrawing a + profile; Section 8 says when to fetch it again. + settlement_bindings: array of objects, required. Each with id - (URI), networks and assets (arrays of strings). + (URI), networks and assets (arrays of strings), all required. flows: array of strings, required. The flows of Section 7.1 the - Facilitator implements. + Facilitator implements; Section 7.1 requires verdict-first among + them. - verification_profiles: array of strings, required. + verification_profiles: array of strings, required. A contract + naming a profile not listed is refused (Section 13.1). terms_profiles: array of objects, required, with at least one entry. Each with id (URI) and profile_hash (digest): the terms profiles, at the revisions named, whose schedules this Facilitator evaluates. - max_contract_value: object, optional. amount and currency. + max_contract_value: object, optional. amount and currency; a + contract whose price, stated in the same currency, exceeds it is + refused, as is one whose price is stated in another currency + (Section 13.1). challenge_deposit: object, optional. amount and currency; see Section 7.3. - endpoints: object, required. Maps each endpoint name in Section 13 - to an absolute URI. - -3.10. Roles - - A role is defined by where its identifier appears, what it signs, and - what it receives. Nothing else about a role is defined here. - - - - - + retrieval: string, optional. parties, the default of Section 17.12, + or open. @@ -842,54 +842,54 @@ Sharma Expires 20 March 2027 [Page 15] Internet-Draft PACT September 2026 - +=============+======================+=================+============+ - | Role | Identifier appears | Signs | Receives | - | | in | | | - +=============+======================+=================+============+ - | Buyer | parties.buyer | the contract; | Contract | - | | | a child | Status, | - | | | registration | Outcome | - | | | (Section | Record | - | | | 10.2) | | - +-------------+----------------------+-----------------+------------+ - | Seller | parties.seller | the contract; | Contract | - | | | the Delivery | Status, | - | | | | Outcome | - | | | | Record | - +-------------+----------------------+-----------------+------------+ - | Facilitator | parties.facilitator, | Contract | every | - | | parent.facilitator, | Status, | posted | - | | the capability | Outcome | record | - | | document | Record, the | | - | | | capability | | - | | | document | | - +-------------+----------------------+-----------------+------------+ - | Verifier | parties.verifier, or | the Verdict | the | - | | the kid of a Verdict | | Delivery | - | | | | and, on a | - | | | | Challenge, | - | | | | the | - | | | | Challenge | - +-------------+----------------------+-----------------+------------+ - | Challenger | the kid of a | the Challenge | Contract | - | | Challenge | | Status | - +-------------+----------------------+-----------------+------------+ - - Table 1: Roles, by what each signs and receives + endpoints: object, required. Maps each endpoint name in Section 13 + to an absolute URI. + +3.10. Roles - One identifier may play more than one role across contracts, and - Section 9.1 says which combinations within one contract a Facilitator - refuses. + A role is defined by where its identifier appears, what it signs, and + what it receives. Nothing else about a role is defined here. -4. Protocol Overview + +=============+======================+================+============+ + | Role | Identifier appears | Signs | Receives | + | | in | | | + +=============+======================+================+============+ + | Buyer | parties.buyer | the contract | Contract | + | | | | Status, | + | | | | Outcome | + | | | | Record | + +-------------+----------------------+----------------+------------+ + | Seller | parties.seller | the contract; | Contract | + | | | the Delivery; | Status, | + | | | as the Buyer | Outcome | + | | | of a child, | Record | + | | | that child's | | + | | | contract | | + | | | (Section 10.2) | | + +-------------+----------------------+----------------+------------+ + | Facilitator | parties.facilitator, | Contract | every | + | | parent.facilitator, | Status, | posted | + | | the capability | Outcome | record | + | | document | Record, the | | + | | | capability | | + | | | document | | + +-------------+----------------------+----------------+------------+ + | Verifier | parties.verifier, or | the Verdict | the | + | | the kid of a Verdict | | Delivery | + | | | | and, on a | + | | | | Challenge, | + | | | | the | + | | | | Challenge | + +-------------+----------------------+----------------+------------+ + | Challenger | the kid of a | the Challenge | Contract | + | | Challenge | | Status | + +-------------+----------------------+----------------+------------+ + + Table 1: Roles, by what each signs and receives - A contract passes through four phases. Propose establishes the - record. Agree co-signs it and a Facilitator accepts it. Complete - produces a Delivery and a Verdict on it. Record produces an Outcome - Record. Every step after Agree is an event the Facilitator records - on its own clock, in one order, and the sequence of those events is - the contract's trace. The trace is the protocol's central object: - the state machine is defined over it, every response a Facilitator + One identifier may play more than one role across contracts, and + Section 9.1 and Section 14.2 say which combinations within one + contract a Facilitator refuses. @@ -898,8 +898,17 @@ Sharma Expires 20 March 2027 [Page 16] Internet-Draft PACT September 2026 - gives carries the prefix recorded so far, and the Outcome Record - carries the whole of it. +4. Protocol Overview + + A contract passes through four phases: Propose establishes the + record, Agree co-signs it and a Facilitator accepts it, Complete + produces a Delivery and a Verdict on it, and Record produces an + Outcome Record. Every step after Agree is an event the Facilitator + records on its own clock, in one order, and the sequence of those + events is the contract's trace. The trace is the protocol's central + object: the state machine is defined over it, every response a + Facilitator gives carries the prefix recorded so far, and the Outcome + Record carries the whole of it. Buyer Facilitator Seller Verifier | | | | @@ -925,13 +934,12 @@ Internet-Draft PACT September 2026 Figure 1: Message flow under the verdict-first flow, without a Challenge - Every accepted request is answered with a Contract Status - (Section 11), a Facilitator-signed object carrying the state and the - trace so far. Nothing in the figure moves value, and no arrow in it - is named for a movement of value. What a terms profile does at each - bracketed event is the profile's, and it is reported once, in the - Outcome Record, as a list the profile produced and the Facilitator - signed. + Every accepted POST is answered with a Contract Status (Section 11), + a Facilitator-signed object carrying the state and the trace so far. + Nothing in the figure moves value, and no arrow in it is named for a + movement of value. What a terms profile does at each bracketed event + is the profile's, and it is reported once, in the Outcome Record, as + a list the profile produced and the Facilitator signed. 4.1. States @@ -941,14 +949,6 @@ Internet-Draft PACT September 2026 - - - - - - - - Sharma Expires 20 March 2027 [Page 17] Internet-Draft PACT September 2026 @@ -978,16 +978,19 @@ Internet-Draft PACT September 2026 The figure omits three arrows that the table carries: a FAIL Verdict recorded in DELIVERED or in WINDOW_OPEN also leads to - AWAITING_CHILDREN; under the no-window flow DELIVERED leads there + AWAITING_CHILDREN; under the no-window flow delivered leads there directly; and a Verdict that is late (verdict-lapsed) opens the window without one. FINAL, SETTLED and ABANDONED are terminal and - each produces exactly one Outcome Record. The -01 revision named one - of these states for a movement of value; no state here is. + each produces exactly one Outcome Record. The -01 revision had a + state, RELEASING, named for a movement of value; it is gone. FUNDED + remains, named for the event the settlement binding reports + (Section 4.2), and nothing here says what that report means. The state named PROPOSED in earlier revisions is gone. Between the parties' signatures and the Facilitator's acceptance a contract exists only on the parties' side, so no Facilitator could observe - that state and the reference implementation never reported it. + that state, and the -01 reference implementation never reported it, + although the -01 Section 12.1 example printed it in a 201 response. 4.2. Events @@ -1002,62 +1005,59 @@ Internet-Draft PACT September 2026 - - - Sharma Expires 20 March 2027 [Page 18] Internet-Draft PACT September 2026 - +==========+====================+==================================+ - |Event | Recorded in; then | Members and condition | - +==========+====================+==================================+ - |accepted | none; then | object is vtc_hash. The | - | | ACCEPTED | contract passed Section 13.1. | - +----------+--------------------+----------------------------------+ - |funded | ACCEPTED; then | ref (string, optional, in the | - | | FUNDED | form the settlement binding | - | | | defines). Recorded when every | - | | | account the named terms profile | - | | | requires shows finality on the | - | | | settlement binding named in | - | | | price.settlement; how a | - | | | Facilitator observes that is the | - | | | binding's to say, and this is | - | | | the only sentence in this | - | | | document that mentions an | - | | | account. | - +----------+--------------------+----------------------------------+ - |deadline- | ACCEPTED or | task.deadline has passed with no | - |passed | FUNDED; then | delivered entry. | - | | AWAITING_CHILDREN | | - +----------+--------------------+----------------------------------+ - |delivered | FUNDED; then | object is the Delivery's digest. | - | | DELIVERED | The Delivery passed Section 6. | - +----------+--------------------+----------------------------------+ - |window- | DELIVERED; then | Under delivery-first, | - |opened | WINDOW_OPEN | immediately after delivered; | - | | | under verdict-first, immediately | - | | | after a PASS verdict or after | - | | | verdict-lapsed. closes_at | - | | | (timestamp, required) is at plus | - | | | challenge.window_seconds. | - +----------+--------------------+----------------------------------+ - |verdict | DELIVERED, | object is the Verdict's digest; | - | | WINDOW_OPEN or | signer (the kid of its | - | | DISPUTED; then see | signature); outcome (PASS or | - | | the condition | FAIL); answers (digest of the | - | | | Challenge, when the Verdict | - | | | carries challenge_hash); | - | | | supersedes (digest of the | - | | | Verdict it replaces, when one | - | | | stood). Then: FAIL leads to | - | | | AWAITING_CHILDREN; PASS in | - | | | DELIVERED leads to window- | - | | | opened; PASS in WINDOW_OPEN | - | | | changes nothing; PASS in | - | | | DISPUTED leads to WINDOW_OPEN | + +==================+===============================================+ + | Event | Recorded in; then. Members and condition | + +==================+===============================================+ + | accepted | none; then ACCEPTED. object is vtc_hash. The | + | | contract passed Section 13.1. | + +------------------+-----------------------------------------------+ + | funded | ACCEPTED; then FUNDED. ref (string, optional, | + | | in the form the settlement binding defines). | + | | Recorded when the settlement binding named in | + | | price.settlement reports that whatever the | + | | named terms profile requires before work | + | | starts is in place; how a Facilitator | + | | observes that is the binding's to say. Where | + | | the profile requires nothing, funded follows | + | | accepted in the same operation. | + +------------------+-----------------------------------------------+ + | deadline-passed | ACCEPTED or FUNDED; then AWAITING_CHILDREN. | + | | task.deadline has passed with no delivered | + | | entry. | + +------------------+-----------------------------------------------+ + | delivered | FUNDED; then DELIVERED, or under no-window | + | | AWAITING_CHILDREN directly. object is the | + | | Delivery's digest. The Delivery passed | + | | Section 6. | + +------------------+-----------------------------------------------+ + | window-opened | DELIVERED; then WINDOW_OPEN. Recorded in the | + | | same operation as the entry it follows, with | + | | the same at: under delivery-first the | + | | delivered entry; under verdict-first a PASS | + | | verdict or verdict-lapsed. closes_at | + | | (timestamp, required) is at plus | + | | challenge.window_seconds. | + +------------------+-----------------------------------------------+ + | verdict | DELIVERED (verdict-first, no Verdict | + | | standing), WINDOW_OPEN (delivery-first, no | + | | Verdict standing) or DISPUTED (answering a | + | | pending Challenge); then as the condition | + | | says. object is the Verdict's digest; signer | + | | (the kid of its signature); outcome (PASS or | + | | FAIL); answers (digest of the Challenge, | + | | present exactly when the Verdict carries | + | | challenge_hash); supersedes (digest of the | + | | Verdict that stood, present exactly when one | + | | did). Then: FAIL leads to AWAITING_CHILDREN; | + | | PASS in DELIVERED leads to window-opened; | + | | PASS in WINDOW_OPEN changes the state of | + | | nothing; PASS in DISPUTED leads to | + | | WINDOW_OPEN once no Challenge is pending. | @@ -1066,54 +1066,54 @@ Sharma Expires 20 March 2027 [Page 19] Internet-Draft PACT September 2026 - | | | once no Challenge is pending. | - +----------+--------------------+----------------------------------+ - |verdict- | DELIVERED; then | Under verdict-first, | - |lapsed | WINDOW_OPEN | verification.max_verdict_seconds | - | | | have passed since delivered with | - | | | no verdict. window-opened | - | | | follows. | - +----------+--------------------+----------------------------------+ - |challenge | WINDOW_OPEN or | object is the Challenge's | - | | DISPUTED; then | digest; signer (the kid of its | - | | DISPUTED | signature); costs copied from | - | | | the Challenge when present. The | - | | | Challenge passed Section 7.3 | - | | | before closes_at. | - +----------+--------------------+----------------------------------+ - |dispute- | DISPUTED; then | object is the Challenge's | - |lapsed | WINDOW_OPEN | digest. | - | | | challenge.max_dispute_seconds | - | | | have passed since that challenge | - | | | entry with no Verdict answering | - | | | it. Leads to WINDOW_OPEN once | - | | | no Challenge is pending; the | - | | | earlier Verdict, if any, stands. | - +----------+--------------------+----------------------------------+ - |window- | WINDOW_OPEN; then | closes_at has passed and no | - |closed | AWAITING_CHILDREN | Challenge is pending. The | - | | | window is never extended: a | - | | | dispute that outlasts it delays | - | | | this entry and does not move | - | | | closes_at. | - +----------+--------------------+----------------------------------+ - |child- | any non-terminal; | object is the child contract's | - |registered| unchanged | digest; facilitator (URI). | - | | | Section 10.2. | - +----------+--------------------+----------------------------------+ - |child- | any non-terminal; | object is the child's Outcome | - |final | unchanged | Record digest; child (the child | - | | | contract's digest). | - +----------+--------------------+----------------------------------+ - |child- | any non-terminal; | child (the child contract's | - |unresolved| unchanged | digest). The child's latest | - | | | finality instant (Section 10.3) | - | | | has passed and no Outcome Record | - | | | for it is held. | - +----------+--------------------+----------------------------------+ - |children- | AWAITING_CHILDREN; | Every registered child has a | - |final | then terminal | child-final or child-unresolved | - | | follows | entry. A contract with no | + +------------------+-----------------------------------------------+ + | verdict-lapsed | DELIVERED; unchanged. Under verdict-first, | + | | verification.max_verdict_seconds have passed | + | | since delivered with no verdict. window- | + | | opened follows in the same operation. | + +------------------+-----------------------------------------------+ + | challenge | WINDOW_OPEN or DISPUTED; then DISPUTED. | + | | object is the Challenge's digest; signer (the | + | | kid of its signature); costs copied from the | + | | Challenge when present. The Challenge passed | + | | Section 7.3 before closes_at. | + +------------------+-----------------------------------------------+ + | dispute-lapsed | DISPUTED; then WINDOW_OPEN. object is the | + | | Challenge's digest. | + | | challenge.max_dispute_seconds have passed | + | | since that challenge entry with no Verdict | + | | answering it. Leads to WINDOW_OPEN once no | + | | Challenge is pending; the earlier Verdict, if | + | | any, stands. | + +------------------+-----------------------------------------------+ + | window-closed | WINDOW_OPEN; then AWAITING_CHILDREN. | + | | closes_at has passed and no Challenge is | + | | pending. The window is never extended: a | + | | dispute that outlasts it delays this entry | + | | and does not move closes_at. | + +------------------+-----------------------------------------------+ + | child-registered | any non-terminal; unchanged. object is the | + | | child contract's digest; facilitator (URI). | + | | Section 10.2. | + +------------------+-----------------------------------------------+ + | child-final | any non-terminal; unchanged. object is the | + | | child's Outcome Record digest; child (the | + | | child contract's digest). | + +------------------+-----------------------------------------------+ + | child-unresolved | any non-terminal; unchanged. child (the child | + | | contract's digest). The child's latest | + | | finality instant (Section 10.3) has passed | + | | and no Outcome Record for it is held. | + +------------------+-----------------------------------------------+ + | children-final | AWAITING_CHILDREN; then terminal follows. | + | | Every registered child has a child-final or | + | | child-unresolved entry. A contract with no | + | | registered children records this entry on | + | | entering AWAITING_CHILDREN. | + +------------------+-----------------------------------------------+ + | terminal | AWAITING_CHILDREN; then FINAL, SETTLED or | + | | ABANDONED. state (the terminal state) and | + | | challenge_upheld (boolean). ABANDONED where | @@ -1122,52 +1122,52 @@ Sharma Expires 20 March 2027 [Page 20] Internet-Draft PACT September 2026 - | | | registered children records this | - | | | entry on entering | - | | | AWAITING_CHILDREN. | - +----------+--------------------+----------------------------------+ - |terminal | AWAITING_CHILDREN; | state (the terminal state) and | - | | then FINAL, | challenge_upheld (boolean). | - | | SETTLED or | ABANDONED where deadline-passed | - | | ABANDONED | was recorded; SETTLED where the | - | | | standing Verdict is FAIL, with | - | | | challenge_upheld true when that | - | | | Verdict answers a Challenge; | - | | | FINAL otherwise. | - +----------+--------------------+----------------------------------+ + | | deadline-passed was recorded; SETTLED where | + | | the standing Verdict is FAIL, with | + | | challenge_upheld true when that Verdict | + | | answers a Challenge; FINAL otherwise. | + +------------------+-----------------------------------------------+ Table 2: Events: the state each is recorded in, the state that follows, and what the entry carries The standing Verdict is the last verdict entry in the trace that no later entry supersedes. A Challenge is pending from its challenge - entry until a verdict entry answers it or a dispute-lapsed entry - names it. + entry until a verdict entry answers it, a dispute-lapsed entry names + it, or a terminal entry is recorded. Every instant in the table is read from the Facilitator's clock, and an entry conditioned on an instant having passed is recorded at the first opportunity after it, which need not be that instant. Two - Facilitators given the same posted records with the same clock - readings record the same trace; that is the determinism the - experiment in Section 1.4 tests, and the reason every condition above - is stated over the trace and the clock and nothing else. + Facilitators given the same posted records, the same clock readings + and the same reports from the settlement binding record the same + trace; that is the determinism the experiment in Section 1.4 tests, + and the reason every condition above is stated over the trace, the + clock and the binding's report, and nothing else. + + An instant has passed when the Facilitator's clock reads that instant + or a later one, and a record received when the clock reads an instant + has arrived at it, so a Challenge received when the clock reads + closes_at is after the window. Before acting on a posted record a + Facilitator MUST first record every clock-driven entry that is due, + so that the record is judged in the state the clock produced; entries + due at the same instant are recorded in the order of the table. The + at of an entry MUST be no earlier than that of the entry before it, + and a Status's issued_at MUST be no earlier than the at of its last + entry. 5. The Verifiable Task Contract A VTC is a JSON object, media type application/ vnd.pact.contract+json, with the members in Section 3.2. A VTC is - valid only if every required member is present, the parties are - distinct, and both the Buyer and the Seller have contributed exactly - one signature that verifies against a key bound to its identifier - (Section 14.2). The Facilitator and any Verifier do not sign the - VTC; their assent is expressed by acting on it, and a Facilitator - that will not act on a contract refuses it at Section 13.1. - - The settlement identifier, the network and the asset are all carried - inside price so that a co-signed VTC is bound to one venue. The -00 - revision omitted them, which made a signed contract replayable - against any facilitator, chain or token contract. - + valid only if every required member is present, the Buyer and the + Seller are distinct after normalization (Section 14.2), any named + Verifier satisfies Section 9.1, and both the Buyer and the Seller + have contributed exactly one signature that verifies against a key + bound to its identifier (Section 14.2). The Facilitator and any + Verifier do not sign the VTC; their assent is expressed by acting on + it, and a Facilitator that will not act on a contract refuses it at + Section 13.1. @@ -1178,54 +1178,54 @@ Sharma Expires 20 March 2027 [Page 21] Internet-Draft PACT September 2026 + The settlement identifier, the network and the asset are all carried + inside price so that a co-signed VTC is bound to one venue. The -00 + revision omitted them, which made a signed contract replayable + against any facilitator, chain or token contract. + ========== NOTE: '\' line wrapping per RFC 8792 =========== { "pact": "0.2", "type": "VerifiableTaskContract", - "id": "vtc_7f3a91", + "id": "vtc_9f2c11", "parties": { - "buyer": "did:web:acme.example", - "seller": "did:web:dataforge.example", + "buyer": "did:web:buyer.example:agents:procure-1", + "seller": "did:web:dataforge.example:agents:etl-3", "facilitator": "did:web:settle.example", - "verifier": "did:web:audit.example" + "verifier": "did:web:audit.example" }, "task": { "spec_hash": "sha256:9491d28ac7a3fcd3f0bf279f78e793547cd4ef11\ 1d27ff6bee37f05531823b72", - "deadline": "2026-11-14T00:00:00Z" + "spec_uri": "https://buyer.example/specs/taskspec.json", + "deadline": "2026-11-14T00:00:00Z" }, "price": { - "amount": "180.00", - "currency": "USDC", + "amount": "180.00", + "currency": "USDC", "settlement": "https://settle.example/bindings/ledger-1", - "network": "eip155:8453" + "network": "eip155:8453" }, "verification": { - "tier": "T0-reexec", - "profile": "acceptance", - "criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c539375\ - 6ae95c10b8351c9c55eb9316416265fc1b", + "tier": "T0-reexec", + "profile": "acceptance", + "criteria_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\ + 10b8351c9c55eb9316416265fc1b", "max_verdict_seconds": 86400 }, "flow": "verdict-first", "terms": { - "profile": - "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ - c1147743326dabd45bda546dc62", - "parameters": { "...": "the profile's; not read here" } + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restit\ + ution", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf\ + 6956e835df2653b99d71d363a68", + "parameters": { + "...": "the profile's; not read here" + } }, "challenge": { "window_seconds": 3600, - "max_dispute_seconds": 86400 - }, - "signatures": [ { "protected": "...", "signature": "..." }, - { "protected": "...", "signature": "..." } ] - } - - Figure 3: A Verifiable Task Contract, signatures abbreviated - @@ -1234,9 +1234,26 @@ Sharma Expires 20 March 2027 [Page 22] Internet-Draft PACT September 2026 - Digests are elided here; the reference repository's values are in - Section 15. The parameters object is shown elided on purpose: - nothing in this document depends on what is in it. + "max_dispute_seconds": 86400 + }, + "signatures": [ + { + "protected": "...", + "signature": "..." + }, + { + "protected": "...", + "signature": "..." + } + ] + } + + Figure 3: A Verifiable Task Contract, signatures and parameters + abbreviated + + The values are the reference repository's (Section 15), with the + signatures abbreviated. The parameters object is shown elided on + purpose: nothing in this document depends on what is in it. 5.1. Hash Commitments and Content Conveyance @@ -1245,21 +1262,34 @@ Internet-Draft PACT September 2026 committed harness_uri as a string while leaving the bytes at that URI uncommitted, which permitted a Buyer to substitute the acceptance instrument after signature, run the substituted instrument, and - submit the failure as a valid fraud proof. The -01 revision stated - the rule and its own reference TaskSpec broke it for three of four - URIs; this revision's example carries all four sibling hashes, and - the validator checks each. + submit the failure as a valid proof of nonconformance. The -01 + revision stated the rule and its own reference TaskSpec broke it for + three of four URIs; this revision's example carries all four sibling + hashes, and the validator checks each. Where the committed content is a bundle of files rather than a single octet stream, the commitment MUST be computed as SHA-256(JCS(M)) where M is an object mapping each file's path, relative to the bundle - root and expressed with "/" separators, to SHA-256 of its bytes, over - every file in the bundle. A manifest of per-file digests is - specified rather than an archive digest because archive formats carry - ordering, timestamp and permission metadata that is not stable across + root and expressed with "/" separators, to the digest of its bytes in + the string form of Section 2, over every file in the bundle. A file + whose name, or any directory on whose path, begins with a dot is not + part of a bundle. A manifest of per-file digests is specified rather + than an archive digest because archive formats carry ordering, + timestamp and permission metadata that is not stable across producers. The same construction commits to a terms profile (Section 5.3). + + + + + + +Sharma Expires 20 March 2027 [Page 23] + +Internet-Draft PACT September 2026 + + 5.2. The Task Specification The content committed by spec_hash is a TaskSpec: a JSON object with @@ -1273,7 +1303,7 @@ Internet-Draft PACT September 2026 with its verifying key for proving tiers, or rubric_uri and rubric_hash for judgment tiers. An empty acceptance object MUST be rejected. The -00 revision's schema permitted one, which made every - fraud proof impossible. + proof of nonconformance impossible. Thresholds MUST be stated so that they cannot be satisfied by returning almost nothing. A threshold expressed only as a rate over @@ -1282,14 +1312,6 @@ Internet-Draft PACT September 2026 therefore required wherever the deliverable is a transformation of that input. - - - -Sharma Expires 20 March 2027 [Page 23] - -Internet-Draft PACT September 2026 - - 5.3. Terms The terms member names the settlement terms both parties signed, by @@ -1311,12 +1333,22 @@ Internet-Draft PACT September 2026 A Facilitator MUST refuse a contract whose terms.profile and terms.profile_hash do not match an entry in the terms_profiles array - of its own capability document (Section 8), so that no party signs - terms the Facilitator will not evaluate, and MUST refuse a contract - whose parameters do not validate against the named profile's - parameters.schema.json. It reads parameters for no other purpose. - The rule of Section 2 that an undefined member is rejected does not - apply inside parameters; the profile's schema governs there. + of its own capability document (Section 8; terms-unsupported), so + that no party signs terms the Facilitator will not evaluate, and MUST + refuse a contract whose parameters do not validate against the named + profile's parameters.schema.json (terms-parameters-invalid). This + document reads parameters for no other purpose; the named profile's + + + +Sharma Expires 20 March 2027 [Page 24] + +Internet-Draft PACT September 2026 + + + schedule and admission rule read them as the profile's. The rule of + Section 2 that an undefined member is rejected does not apply inside + parameters; the profile's schema governs there. A profile usable with this document defines, in its prose, a schedule: a total, deterministic function from a contract and a trace @@ -1327,10 +1359,13 @@ Internet-Draft PACT September 2026 unresolved child, a contract abandoned before it was funded. Deterministic means the result depends on the contract, the trace and nothing else, so that any party holding those can recompute it. The - prose also names the accounts the schedule uses and how each one's - opening amount is computed from the contract. This document does not - register profiles and defines none normatively; Appendix A carries - one for the experiment. + prose also names the accounts the schedule moves value between, and + says which of them are internal, opened empty and required to close + empty, and which are external. A profile MAY also state an admission + rule: a condition on the contract, evaluated once at accepted, whose + failure is reported with a problem type from the profile's namespace + (Section 13.3). This document does not register profiles and defines + none normatively; Appendix A carries one for the experiment. Everything the -01 revision said in its Section 5.3, and everything it said in its Section 7 about what is posted, released, returned or @@ -1339,13 +1374,6 @@ Internet-Draft PACT September 2026 profile needs are in parameters, and the -01 figures in particular are the parameters of the profile in Appendix A. - - -Sharma Expires 20 March 2027 [Page 24] - -Internet-Draft PACT September 2026 - - 6. The Delivery Record The Delivery is the record a contract is judged against. It is a @@ -1367,6 +1395,13 @@ Internet-Draft PACT September 2026 deadline with nothing conformant recorded is now the deadline-passed event, and what that event costs anyone is the profile's. + + +Sharma Expires 20 March 2027 [Page 25] + +Internet-Draft PACT September 2026 + + Conformance of evidence is a check on shape, not on substance: the Facilitator confirms that the members the profile requires are present and well formed, and nothing about whether the work is any @@ -1375,52 +1410,31 @@ Internet-Draft PACT September 2026 entry, the Facilitator records deadline-passed (Section 4.2). No window opens, because there is nothing to challenge. - - - - - - - - - - - - - - - - - - - - - - -Sharma Expires 20 March 2027 [Page 25] - -Internet-Draft PACT September 2026 - - ========== NOTE: '\' line wrapping per RFC 8792 =========== { "pact": "0.2", "type": "Delivery", - "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ - 5fbbd4ebace4fb980f1c2", - "work_hash": "sha256:9c1f...", - "work_uri": "https://cdn.dataforge.example/o/9c1f", - "input_hash": "sha256:41ab...", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff49493\ + 04c2c855ca1d746ba6459", + "work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee2\ + 80a3170b6b9a5037036ef0", + "work_uri": "https://cdn.dataforge.example/o/a26d", + "input_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c9\ + 75a1d1ce519d582306338b1", "evidence": { - "profile": "acceptance", - "instrument_hash":"sha256:0bdde1ab6b081d2b4bda580c5393756ae95\ - c10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:7e02...", - "results_uri": "https://cdn.dataforge.example/o/7e02" + "profile": "acceptance", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\ + 5c10b8351c9c55eb9316416265fc1b", + "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf84\ + 8ebaca540214b4f6d2165688a51", + "results_uri": "https://cdn.dataforge.example/o/28d3" }, - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } } Figure 4: A Delivery for a T0-reexec contract, acceptance profile @@ -1434,22 +1448,8 @@ Internet-Draft PACT September 2026 7. Verdicts, Challenges and the Window -7.1. Flows - - The flow member selects one of three shapes for the state machine of - Section 4.1. A conformant Facilitator MUST implement verdict-first; - the others are OPTIONAL, and a Facilitator MUST refuse a contract - naming a flow it does not advertise (flow-unsupported). - - verdict-first: A Verdict is recorded before the window opens. The - window opens on a PASS Verdict or on verdict-lapsed; a FAIL - Verdict ends the contract without a window. - delivery-first: The window opens at delivered. A Verdict MAY be - recorded inside the window without a Challenge; a FAIL ends the - contract, a PASS changes nothing. - no-window: No window opens and no Verdict is accepted; delivered is @@ -1458,12 +1458,30 @@ Sharma Expires 20 March 2027 [Page 26] Internet-Draft PACT September 2026 +7.1. Flows + + The flow member selects one of three shapes for the state machine of + Section 4.1. A conformant Facilitator MUST implement verdict-first + and MUST list it in the flows member of its capability document + (Section 3.9); the others are OPTIONAL, and a Facilitator MUST refuse + a contract naming a flow it does not advertise (flow-unsupported). + + verdict-first: A Verdict is recorded before the window opens. The + window opens on a PASS Verdict or on verdict-lapsed; a FAIL + Verdict ends the contract without a window. + + delivery-first: The window opens at delivered. One Verdict MAY be + recorded inside the window without a Challenge, and a second only + in answer to one; a FAIL ends the contract, a PASS changes + nothing. + + no-window: No window opens and no Verdict is accepted; delivered is followed by the terminal path. - The -01 revision had four release modes, named for when value moved. - Two of them, on-window and optimistic, produce the same trace and - differed only in which event a profile acts on, which is a profile - parameter and not a protocol matter. The mapping is in Appendix B. + The -01 revision had four release modes. Two of them produce the + same trace and differed only in which event a profile acts on, which + is a profile parameter and not a protocol matter. The mapping is in + Appendix B. The window opens at the instant of the window-opened entry and closes at that instant plus challenge.window_seconds, carried in the entry @@ -1478,21 +1496,43 @@ Internet-Draft PACT September 2026 object, media type application/vnd.pact.verdict+json, with the members in Section 3.5, signed once. + + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 27] + +Internet-Draft PACT September 2026 + + ========== NOTE: '\' line wrapping per RFC 8792 =========== { "pact": "0.2", "type": "Verdict", - "vtc_id": "vtc_7f3a91", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ - f8aaaf2f186702504fba242ffb", - "outcome": "PASS", - "profile": "acceptance", + "vtc_id": "vtc_9f2c11", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45\ + 370cb3023331bc8bfbd924fcbe", + "outcome": "PASS", + "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\ 10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:7e02...", - "evaluated_at": "2026-11-10T09:14:22Z", - "signature": { "protected": "...", "signature": "..." } + "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf848e\ + baca540214b4f6d2165688a51", + "evaluated_at": "2026-11-10T09:14:22Z", + "signature": { + "protected": "...", + "signature": "..." + } } Figure 5: A Verdict @@ -1504,21 +1544,14 @@ Internet-Draft PACT September 2026 not-independent). It MUST refuse a Verdict for a contract with no delivered entry (no-recorded-delivery); one whose delivery_hash does not match that entry, or whose profile or instrument_hash does not - match the contract (verdict-nonconformant); one received in a state - the table in Section 4.2 does not list for it, or under the no-window - - - -Sharma Expires 20 March 2027 [Page 27] - -Internet-Draft PACT September 2026 - - - flow (wrong-state); and one carrying challenge_hash that names no - pending Challenge, or omitting it while the contract is DISPUTED - (verdict-nonconformant). A Verdict that answers a Challenge - supersedes the Verdict that stood before it, and both stay in the - trace. + match the contract (verdict-nonconformant); one received in a state, + or under conditions, that the table in Section 4.2 does not list for + it, or under the no-window flow (wrong-state); and one carrying + challenge_hash that names no pending Challenge, or omitting it while + the contract is DISPUTED (verdict-nonconformant). A Verdict recorded + while one stands supersedes it, and both stay in the trace; since a + Verdict is accepted while one stands only in DISPUTED, only a Verdict + that answers a Challenge ever supersedes. A Verdict commits to the instrument it ran and to the results it produced. Without instrument_hash a Verifier could run something @@ -1529,43 +1562,66 @@ Internet-Draft PACT September 2026 Under verdict-first a Verifier that never answers would leave a contract in DELIVERED forever, and the -01 revision had no rule for it. verification.max_verdict_seconds bounds the wait: when it passes - with no Verdict, the Facilitator records verdict-lapsed and opens the - window, so that the contract can still be challenged and can still - end. What a lapsed Verdict costs anyone is the profile's. -7.3. Challenges + + +Sharma Expires 20 March 2027 [Page 28] + +Internet-Draft PACT September 2026 + + + with no Verdict, the Facilitator records verdict-lapsed and opens the + window, so that the contract can still be challenged and can still + end. What a lapsed Verdict costs anyone is the profile's. + +7.3. Challenges A Challenge is a JSON object, media type application/ vnd.pact.challenge+json, with the members in Section 3.6, by which a - party submits a fraud proof inside the window. A Facilitator MUST - refuse a Challenge received when the contract is not in WINDOW_OPEN - or DISPUTED, or after closes_at (challenge-window-closed); one whose + party submits a proof of nonconformance (what optimistic systems call + a fraud proof) inside the window. A Facilitator MUST refuse a + Challenge received when the contract is not in WINDOW_OPEN or + DISPUTED, or after closes_at (challenge-window-closed); one whose delivery_hash does not match the delivered entry (object-conflict); one whose proof does not conform to the verification profile (proof- nonconformant); one whose signer it cannot resolve (signature- invalid); and one signed by the contract's Seller (unexpected- signer), since a performer's statement against its own Delivery is - not a fraud proof and the -01 revision left the case open. A - Facilitator MUST NOT refuse a Challenge on the ground that its signer - is the contract's Buyer. + not a proof of nonconformance and the -01 revision left the case + open. A Facilitator MUST NOT refuse a Challenge on the ground that + its signer is the contract's Buyer. A Challenge that is accepted is evaluated by a party satisfying Section 9.1, whose finding is a Verdict carrying challenge_hash; the - Challenger's own assertion is not a finding. The Challenger is the - party identified by the kid of the Challenge's signature. + Challenger's own assertion is not a finding, unless the Challenger is + the verifier the contract names, whose Verdict is the finding by + definition. A named Verifier that finds its own PASS wrong posts a + Challenge and answers it. The Challenger is the party identified by + the kid of the Challenge's signature. - A Facilitator MAY require that a Challenge be accompanied by a - deposit in the amount its capability document advertises as - challenge_deposit. How a deposit is posted is the settlement - binding's, what becomes of it is the terms profile's, and this - document says nothing further about it. Section 17.14 discusses what - a deposit does and does not prevent. + A capability document MAY advertise challenge_deposit. Whether + anything must accompany a Challenge, how it is posted and what + becomes of it are the terms profile's and the settlement binding's to + say; this document carries the member and reads it for no purpose. + Section 17.14 discusses what a deposit does and does not prevent. -Sharma Expires 20 March 2027 [Page 28] + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 29] Internet-Draft PACT September 2026 @@ -1575,19 +1631,26 @@ Internet-Draft PACT September 2026 { "pact": "0.2", "type": "Challenge", - "vtc_id": "vtc_7f3a91", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ - f8aaaf2f186702504fba242ffb", + "vtc_id": "vtc_9f2c11", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45\ + 370cb3023331bc8bfbd924fcbe", "proof": { - "profile": "acceptance", + "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\ 5c10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:a91e...", - "results_uri": "https://watch.example/o/a91e", - "failing_checks": ["schema_valid_rate", "row_count_min"] + "results_hash": "sha256:7f05a60223e24ca9393e0c241d0448cb5f39e\ + 5edb71c117fb7d3d593d5861b40", + "results_uri": "https://watch.example/o/a91e", + "failing_checks": ["schema_valid_rate", "row_count_min"] + }, + "costs": { + "amount": "1.20", + "currency": "USDC" }, - "costs": { "amount": "1.20", "currency": "USDC" }, - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } } Figure 6: A Challenge under the acceptance profile @@ -1614,14 +1677,7 @@ Internet-Draft PACT September 2026 - - - - - - - -Sharma Expires 20 March 2027 [Page 29] +Sharma Expires 20 March 2027 [Page 30] Internet-Draft PACT September 2026 @@ -1654,7 +1710,9 @@ Internet-Draft PACT September 2026 Before a Buyer and Seller can co-sign a VTC they must agree on a Facilitator and know what it implements. This document registers one - well-known URI for that purpose, per [RFC8615]. + well-known URI for that purpose, per [RFC8615]. A client SHOULD + fetch the document again before it proposes, since nothing in it + survives the Facilitator withdrawing a profile. This is deliberately narrower than agent discovery, which is the subject of separate work and is not restated here. What is @@ -1663,21 +1721,19 @@ Internet-Draft PACT September 2026 A Facilitator SHOULD publish a JSON document, media type application/ vnd.pact.facilitator+json, with the members in Section 3.9, at the - path /.well-known/pact-facilitator of its origin. The document MUST - be served over HTTPS. It MUST be signed, and the signature MUST - verify against a key bound to the identifier in facilitator. An - unsigned capability document is not usable for contract formation, - because terms_profiles determines which terms a party can name and - expect to be evaluated. - - - + path /.well-known/pact-facilitator of its origin: for an https: + identifier that origin, and for a did:web identifier https:// + followed by the host the method encodes. The document MUST be served + over HTTPS. It MUST be signed, and the signature MUST verify against + a key bound to the identifier in facilitator. An unsigned capability + document is not usable for contract formation, because terms_profiles + determines which terms a party can name and expect to be evaluated. -Sharma Expires 20 March 2027 [Page 30] +Sharma Expires 20 March 2027 [Page 31] Internet-Draft PACT September 2026 @@ -1688,39 +1744,63 @@ Internet-Draft PACT September 2026 "pact": "0.2", "type": "FacilitatorCapabilities", "facilitator": "did:web:settle.example", + "issued_at": "2026-11-01T09:00:00Z", "settlement_bindings": [ - { "id": "https://settle.example/bindings/ledger-1", + { + "id": "https://settle.example/bindings/ledger-1", "networks": ["eip155:8453"], - "assets": ["USDC"] } + "assets": ["USDC"] + } ], - "flows": ["verdict-first", "delivery-first"], - "verification_profiles": ["acceptance", "bisection"], + "flows": ["verdict-first", "delivery-first"], + "verification_profiles": ["acceptance"], "terms_profiles": [ - { "id": - "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f\ - 5fc1147743326dabd45bda546dc62" } + { + "id": "tag:laxsharma79@gmail.com,2026:pact:bonded-restituti\ + on", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124c\ + cf6956e835df2653b99d71d363a68" + } ], - "max_contract_value": { "amount": "50000.00", - "currency": "USDC" }, + "max_contract_value": { + "amount": "50000.00", + "currency": "USDC" + }, "endpoints": { - "contract": "https://settle.example/pact/v2/contracts", - "delivery": "https://settle.example/pact/v2/deliveries", - "verdict": "https://settle.example/pact/v2/verdicts", + "contract": "https://settle.example/pact/v2/contracts", + "delivery": "https://settle.example/pact/v2/deliveries", + "verdict": "https://settle.example/pact/v2/verdicts", "challenge": "https://settle.example/pact/v2/challenges", - "outcome": "https://settle.example/pact/v2/outcomes" + "outcome": "https://settle.example/pact/v2/outcomes" }, - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } } Figure 8: https://settle.example/.well-known/pact-facilitator + + + + + + + + +Sharma Expires 20 March 2027 [Page 32] + +Internet-Draft PACT September 2026 + + A client MUST NOT infer any capability from the absence of a member. - A Facilitator that does not publish a capability document can still - be named in a VTC by prior arrangement; discovery is a convenience, - not a precondition. A Facilitator MUST NOT list a terms profile - whose vectors (Section 12.1) its own implementation does not - reproduce. + A client may hold a Facilitator\'s capability document by prior + arrangement rather than fetch it from the well-known path; the path + is a convenience, the document is not, since a Facilitator refuses + what its document does not advertise (Section 5.3, Section 13.1). A + Facilitator lists only the terms profiles whose vectors + (Section 12.1) its own implementation reproduces. 9. Verification Profiles @@ -1730,14 +1810,6 @@ Internet-Draft PACT September 2026 deterministic re-execution; T1-tee, hardware attestation per [RFC9334]; T2-zkml, a proof of inference; and T3-jury, staked arbitration. Tiers are a vocabulary. Three profiles are defined - - - -Sharma Expires 20 March 2027 [Page 31] - -Internet-Draft PACT September 2026 - - below by name; any other is identified by a URI under its definer's control, and this document creates no registry for them. The distinction matters because the tier name does not determine how much @@ -1745,14 +1817,17 @@ Internet-Draft PACT September 2026 Consider one task, a bulk data transformation, under two profiles at the same nominal tier. Re-executing the whole computation and - comparing outputs costs approximately what performing it cost. - Running a committed acceptance instrument against the delivered - artifact costs a small fraction of a percent. Those two differ by - more than two orders of magnitude in what checking costs relative to - the price. A terms profile may make that ratio matter; this document - requires only that a verification profile state an order-of-magnitude - estimate of its cost relative to the work, since a figure nobody can - estimate is a figure nobody can use. + comparing outputs costs about what performing it cost; running a + committed acceptance instrument against the delivered artifact costs + a small fraction of that. Those are the author's estimates, not + measurements (the measurements Section 16 mentions are of the + protocol, not the work), and the two can differ by orders of + magnitude in what checking costs relative to the price. A terms + profile may make that ratio matter; this document requires of a + verification profile the five statements listed after the profiles + below, one of which is an order-of-magnitude estimate of its cost + relative to the work, since a figure nobody can estimate is a figure + nobody can use. Implementations SHOULD select the cheapest profile that detects the failures they actually care about, rather than the strongest-sounding @@ -1760,12 +1835,21 @@ Internet-Draft PACT September 2026 more than a re-execution profile that nobody can afford to run. acceptance: Run the instrument committed by criteria_hash against - the Delivery. The fraud proof is a failing evaluation. - Deterministic by construction, since the instrument is fixed - before work begins. Cost: a small fraction of a percent of the - work for a data transformation. + the Delivery. The proof of nonconformance is a failing + evaluation. Deterministic by construction, since the instrument + is fixed before work begins. Cost: a small fraction of the work + for a data transformation, by estimate. bisection: Interactive narrowing to a single disputed step, which is + + + + +Sharma Expires 20 March 2027 [Page 33] + +Internet-Draft PACT September 2026 + + then checked directly. Cost grows logarithmically in the size of the computation rather than linearly. @@ -1773,27 +1857,30 @@ Internet-Draft PACT September 2026 the computation is deterministic and the environment is pinned; see Section 17.10. Cost: approximately the work. + A verification profile usable with this document states five things: + what artefact is evaluated and against what; what constitutes a valid + proof of nonconformance, including whether absence of evidence is + one; that its proof can be evaluated by a party other than the + Seller; its cost relative to the work, to order of magnitude; and + whether it is deterministic and with what tolerance (Section 17.10). + acceptance states these below; the other two are sketches that a full + profile document completes; a profile defined elsewhere states them + in its own document. + 9.1. Verifier Independence and Identifier Normalization Independence is a relation between the party that signs a Verdict and - the parties to the contract. It MUST be derived by the evaluator and - MUST NOT be satisfied by a field in which a record declares itself - independent. A Facilitator MUST refuse a Verdict whose signer is, - after normalization, the contract's Buyer, Seller or Facilitator, and - MUST refuse a contract whose parties.verifier is any of those three - (verifier-not-independent). The last case is the rule the -01 + the parties to the contract. It MUST be derived by the Facilitator + and MUST NOT be satisfied by a field in which a record declares + itself independent. Rules of this kind are stated for evaluation + after the fact in [X402COMPLIANCE]; this document binds them at + contract formation. A Facilitator MUST refuse a Verdict whose signer + is, after normalization, the contract's Buyer, Seller or Facilitator, + and MUST refuse a contract whose parties.verifier is any of those + three (verifier-not-independent). The last case is the rule the -01 revision stated as a prohibition on the Facilitator's conduct; it is an identifier comparison and is stated as one. - - - - -Sharma Expires 20 March 2027 [Page 32] - -Internet-Draft PACT September 2026 - - Party identifiers MUST be normalized before comparison, and the normalization MUST fold toward identifying the same party: strip leading and trailing whitespace; lower-case the scheme and, for @@ -1806,6 +1893,19 @@ Internet-Draft PACT September 2026 parties. An independence claim reaches exactly as far as the record's own commitments. + + + + + + + + +Sharma Expires 20 March 2027 [Page 34] + +Internet-Draft PACT September 2026 + + 10. Contract Trees An agent that accepts work may subcontract part of it. The @@ -1817,7 +1917,7 @@ Internet-Draft PACT September 2026 A (Buyer) | - vtc_7f3a91 at did:web:settle.example + vtc_9f2c11 at did:web:settle.example | B (Seller) | @@ -1838,18 +1938,6 @@ Internet-Draft PACT September 2026 carried this member inside the member it has since removed; it is structural and is now where structure is. - - - - - - - -Sharma Expires 20 March 2027 [Page 33] - -Internet-Draft PACT September 2026 - - The child's Facilitator need not resolve the parent, and across Facilitators it often cannot. It MUST record parent as signed, and MUST allow the identifier in parent.facilitator to retrieve the @@ -1866,23 +1954,34 @@ Internet-Draft PACT September 2026 Facilitator is willing to register, and no Facilitator sees more than one level. + + + +Sharma Expires 20 March 2027 [Page 35] + +Internet-Draft PACT September 2026 + + 10.2. Registration and Children Final - The parent's Facilitator learns of a child when the parent's Seller - registers it: a POST of the child's co-signed contract to the - parent's contract resource (Section 13). The registering party is - the child's Buyer, which is why it holds the child's contract and why - it is authorised: it is a party to both. + The parent's Facilitator learns of a child when the child's co-signed + contract is posted to the parent's children resource (Section 13). + Any holder of that contract may post it; the registration is + authenticated by the child's own signatures, and the child's Buyer, + which is the parent's Seller, is the party that ordinarily holds it. + A registered child is identified at the parent's venue by its digest, + so its id need not be unique there. A Facilitator MUST refuse a registration, with the problem type - named, when: the body is not a valid contract (Section 14.2); its - parent.vtc_hash is not the parent's digest or its parent.facilitator - is not this Facilitator (parent-unresolvable); its parties.buyer is - not the parent's parties.seller after normalization (parent- - unresolvable); its latest finality instant is not earlier than the - parent's (Section 10.3, finality-ordering-violation); or the parent - is terminal (wrong-state). An accepted registration is recorded as - child-registered. + named, when: the body is not a valid contract (Section 14.2, the + rules on members and signatures; its terms and deadline are its own + Facilitator's to check); its parent.vtc_hash is not the parent's + digest or its parent.facilitator is not this Facilitator (parent- + unresolvable); its parties.buyer is not the parent's parties.seller + after normalization (parent-unresolvable); its latest finality + instant is not earlier than the parent's (Section 10.3, finality- + ordering-violation); or the parent is terminal (wrong-state). An + accepted registration is recorded as child-registered. child.parties.buyer == parent.parties.seller child.parent.vtc_hash == digest(parent) @@ -1896,19 +1995,12 @@ Internet-Draft PACT September 2026 A child becomes final for its parent when the parent's Facilitator holds the child's Outcome Record. It may obtain that record itself, - by retrieving it from the child's Facilitator, or receive it from the - parent's Seller by a POST to the same resource (Section 13). Either - - - -Sharma Expires 20 March 2027 [Page 34] - -Internet-Draft PACT September 2026 - - + by retrieving it from the child's Facilitator, or receive it by a + POST to the child's entry under that resource (Section 13). Either way the Facilitator MUST verify the record's Facilitator signature against a key bound to the identifier the registration recorded, and MUST verify that its vtc_hash is the registered child's digest, + refusing a record that fails either check (child-outcome-invalid), before recording child-final. Where the child's latest finality instant passes with no record held, the Facilitator records child- unresolved. children-final follows when every registered child has @@ -1918,6 +2010,14 @@ Internet-Draft PACT September 2026 Nothing in this document compels a parent's Seller to register a child, and Section 17.6 says what that means. + + + +Sharma Expires 20 March 2027 [Page 36] + +Internet-Draft PACT September 2026 + + 10.3. Finality Is Bottom-Up A parent's Outcome Record MUST carry children_merkle_root over the @@ -1954,14 +2054,6 @@ Internet-Draft PACT September 2026 the waiting state is what makes the rule honest about the case where a child is late anyway. - - - -Sharma Expires 20 March 2027 [Page 35] - -Internet-Draft PACT September 2026 - - parent |== work ==|= verdict =|= window =|= dispute =| ^ L(parent) child |== work ==|= vrd =|= win =|= dsp =| @@ -1974,6 +2066,14 @@ Internet-Draft PACT September 2026 What a child's outcome means for its parent is not stated here. No entry in a parent's schedule depends on any child's outcome unless the named terms profile says so; what this document guarantees is + + + +Sharma Expires 20 March 2027 [Page 37] + +Internet-Draft PACT September 2026 + + that the parent's Outcome Record commits to whichever child records exist when it is issued and names, in its trace, every child that does not. @@ -2013,7 +2113,19 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 36] + + + + + + + + + + + + +Sharma Expires 20 March 2027 [Page 38] Internet-Draft PACT September 2026 @@ -2023,32 +2135,59 @@ Internet-Draft PACT September 2026 { "pact": "0.2", "type": "ContractStatus", - "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ - 5fbbd4ebace4fb980f1c2", - "state": "WINDOW_OPEN", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff49493\ + 04c2c855ca1d746ba6459", + "state": "WINDOW_OPEN", "trace": [ - { "event": "accepted", "at": "2026-11-01T10:00:00Z", - "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ - 225fbbd4ebace4fb980f1c2" }, - { "event": "funded", "at": "2026-11-01T10:00:00Z" }, - { "event": "delivered", "at": "2026-11-10T08:30:12Z", - "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ - aaf2f186702504fba242ffb" }, - { "event": "verdict", "at": "2026-11-10T09:14:30Z", - "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ - e09452a74bac15e6752ca81", "outcome": "PASS" }, - { "event": "window-opened", "at": "2026-11-10T09:14:30Z", - "closes_at": "2026-11-10T10:14:30Z" } + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff494\ + 9304c2c855ca1d746ba6459" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370\ + cb3023331bc8bfbd924fcbe" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:1ac94d72dbdd1f51e523ecddb3a3b360703976215\ + b13324485db355c948adfc0", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + } ], "issued_at": "2026-11-10T09:14:30Z", - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } } - Figure 11: A Contract Status after the Verdict of Figure 1 + Figure 11: A Contract Status after the Verdict of Figure 5 + + + +Sharma Expires 20 March 2027 [Page 39] + +Internet-Draft PACT September 2026 + Two rules make a Status worth keeping. A Facilitator MUST issue a - Status for every request it accepts, carrying the entry that request + Status for every POST it accepts, carrying the entry that request caused, so that the requester holds a signed receipt of what was recorded and when. And the trace in every Status a Facilitator issues for a contract MUST be a prefix of the trace in every later @@ -2061,19 +2200,6 @@ Internet-Draft PACT September 2026 replaces that: the posted object is not echoed, and everything in the response is inside the Facilitator's signature. - - - - - - - - -Sharma Expires 20 March 2027 [Page 37] - -Internet-Draft PACT September 2026 - - 12. Outcome Records An Outcome Record records what a contract did. It is a JSON object, @@ -2085,9 +2211,12 @@ Internet-Draft PACT September 2026 A Facilitator MUST issue exactly one Outcome Record for every contract that reaches a terminal state, including SETTLED and ABANDONED, MUST sign it, and MUST NOT require the signature of any - other party on it. The -00 revision's record needed the signature of - the party it recorded against, which made a reputation layer built on - it structurally incapable of recording a loss. The Facilitator + other party on it. A Facilitator MUST serve the bytes of the record + it signed rather than sign it again on retrieval; under a randomized + signature scheme a second signing would produce a second record with + a different digest. The -00 revision's record needed the signature + of the party it recorded against, which made a reputation layer built + on it structurally incapable of recording a loss. The Facilitator signature is what makes the record evidence: without it the record is a claim by interested parties about themselves, and with it a fabricated history requires a Facilitator's key rather than two @@ -2098,65 +2227,133 @@ Internet-Draft PACT September 2026 { "pact": "0.2", "type": "OutcomeRecord", - "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ - 5fbbd4ebace4fb980f1c2", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff49493\ + 04c2c855ca1d746ba6459", "parties": { - "buyer": "did:web:acme.example", - "seller": "did:web:dataforge.example", + "buyer": "did:web:buyer.example:agents:procure-1", + "seller": "did:web:dataforge.example:agents:etl-3", "facilitator": "did:web:settle.example", - "verifier": "did:web:audit.example" + + + +Sharma Expires 20 March 2027 [Page 40] + +Internet-Draft PACT September 2026 + + + "verifier": "did:web:audit.example" + }, + "outcome": { + "state": "SETTLED", + "challenge_upheld": true }, - "outcome": { "state": "SETTLED", "challenge_upheld": true }, - "work_hash": "sha256:9c1f...", + "work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee2\ + 80a3170b6b9a5037036ef0", "trace": [ - { "event": "accepted", "at": "...", - "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ - 225fbbd4ebace4fb980f1c2" }, - { "event": "funded", "at": "..." }, - { "event": "delivered", "at": "...", - "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ - aaf2f186702504fba242ffb" }, - { "event": "verdict", "at": "...", - "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ - e09452a74bac15e6752ca81", "outcome": "PASS" }, - { "event": "window-opened", "at": "...", "closes_at": "..." }, - { "event": "challenge", "at": "...", + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff494\ + 9304c2c855ca1d746ba6459" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370\ + cb3023331bc8bfbd924fcbe" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:1ac94d72dbdd1f51e523ecddb3a3b360703976215\ + b13324485db355c948adfc0", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + }, + { + "event": "challenge", + "at": "2026-11-10T09:40:00Z", + "object": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093e\ + c2c6795962e67a6e2ff55ff", + "signer": "did:web:watch.example#k1", + "costs": { + "amount": "1.20", + "currency": "USDC" + } -Sharma Expires 20 March 2027 [Page 38] +Sharma Expires 20 March 2027 [Page 41] Internet-Draft PACT September 2026 - "object": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df\ - 6cfef12d12d1340c15e5d85" }, - { "event": "verdict", "at": "...", - "object": "sha256:012bab194f444012f87ff8986419537f9de9f3dfe\ - 845c6623ac6a7488fb98b73", "outcome": "FAIL", - "answers": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4d\ - f6cfef12d12d1340c15e5d85", - "supersedes": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51\ - cee8e09452a74bac15e6752ca81" }, - { "event": "children-final", "at": "..." }, - { "event": "terminal", "at": "...", "state": "SETTLED", - "challenge_upheld": true } + }, + { + "event": "verdict", + "at": "2026-11-10T09:58:05Z", + "object": "sha256:10d537e7b8face8bd7695541d36d32568394a5319\ + 7653280c404e2d84a63d46d", + "signer": "did:web:audit.example#k1", + "outcome": "FAIL", + "answers": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093\ + ec2c6795962e67a6e2ff55ff", + "supersedes": "sha256:1ac94d72dbdd1f51e523ecddb3a3b36070397\ + 6215b13324485db355c948adfc0" + }, + { + "event": "children-final", + "at": "2026-11-10T09:58:05Z" + }, + { + "event": "terminal", + "at": "2026-11-10T09:58:05Z", + "state": "SETTLED", + "challenge_upheld": true + } ], "terms_result": { - "profile": - "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ - c1147743326dabd45bda546dc62", - "currency": "USDC", + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restit\ + ution", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf\ + 6956e835df2653b99d71d363a68", + "currency": "USDC", "transfers": [ - { "event": 8, "from": "...", "to": "...", "amount": "...", - "code": "..." } + { + "event": 8, + "from": "...", + "to": "...", + "amount": "...", + "code": "..." + } ] }, - "signatures": [ { "protected": "...", "signature": "..." } ] + "signatures": [ + { + "protected": "...", + "signature": "..." + } + ] } + + + +Sharma Expires 20 March 2027 [Page 42] + +Internet-Draft PACT September 2026 + + Figure 12: An Outcome Record for a contract that reached SETTLED on an upheld Challenge @@ -2176,35 +2373,42 @@ Internet-Draft PACT September 2026 code (a string the profile defines, naming the schedule line that produced the entry). + This document defines the form of the list and two arithmetic facts + about it, and nothing about what any entry means. Over the internal + accounts the profile declares (Section 5.3), which open empty: no + entry takes from an account more than that account holds at that + point in the list; and after the last entry every account the profile + marks internal holds zero. The two facts are constraints on a + profile, checked against its vectors before a Facilitator lists it + (Section 8); a Facilitator MUST NOT sign an Outcome Record whose list + breaks either, since such a list shows the profile it evaluates to be + defective, and MUST NOT sign one whose list differs from what the + profile's schedule produces for the record's own trace. Any party + holding the contract, the trace and the profile's bundle can + recompute the list; that is the property the experiment in + Section 1.4 depends on. + vectors.json in a profile's bundle is an array of objects, each with + name, contract (a VTC, or the members of one the schedule reads), and + then either trace (a complete trace) with transfers (the list the + schedule produces for it) and accounts (the profile's internal + accounts, which open empty and must close empty), or admission (an + object carrying either admitted, true, or refused, the problem type + the admission rule answers with for that contract). A Facilitator + MUST reproduce every vector of a profile before listing that profile + in its capability document (Section 8), which is the only conformance + requirement this document places on a profile implementation. -Sharma Expires 20 March 2027 [Page 39] - -Internet-Draft PACT September 2026 - This document defines the form of the list and two arithmetic facts - about it, and nothing about what any entry means. Over the accounts - and opening amounts the profile declares for the contract - (Section 5.3): no entry takes from an account more than that account - holds at that point in the list; and after the last entry every - account the profile marks internal holds zero. A Facilitator MUST - NOT sign an Outcome Record whose list breaks either fact, and MUST - NOT sign one whose list differs from what the profile's schedule - produces for the record's own trace. Any party holding the contract, - the trace and the profile's bundle can recompute the list; that is - the property the experiment in Section 1.4 depends on. - vectors.json in a profile's bundle is an array of objects, each with - name, contract (a VTC, or the members of one the schedule reads), - trace (a complete trace), and transfers (the list the schedule - produces for it). A Facilitator MUST reproduce every vector of a - profile before listing that profile in its capability document - (Section 8), which is the only conformance requirement this document - places on a profile implementation. +Sharma Expires 20 March 2027 [Page 43] + +Internet-Draft PACT September 2026 + 12.2. The Children Merkle Root @@ -2219,9 +2423,13 @@ Internet-Draft PACT September 2026 therefore fixed by n alone, and two implementations that agree on D agree on the root. - The domain separation is not optional. Without distinct prefixes an - attacker can present an interior node as though it were a leaf, and - so claim an inclusion proof for a subtree that never existed. + The domain separation is not optional, because the prefixes are what + make MTH the function [RFC9162] defines, and a second implementation + must compute the same root. The second-preimage attack the prefixes + guard against, a leaf input chosen to equal an interior node's input, + needs a leaf of that input's length; the fixed 32-byte digests in D + cannot supply one, so here the prefixes buy agreement with the RFC + rather than a defence the construction would otherwise lack. The member is present when at least one child is registered and absent otherwise; it MUST NOT be present with an empty or zero value, @@ -2229,25 +2437,16 @@ Internet-Draft PACT September 2026 withheld. Where every registered child is unresolved D is empty and the root is MTH of the empty list, SHA-256 of the empty string; the child-unresolved entries in the trace say which records the root does - not cover. The -01 revision computed leaves over records with their - signatures removed, which let a record be re-signed without changing - the root. - - - - - -Sharma Expires 20 March 2027 [Page 40] - -Internet-Draft PACT September 2026 - + not cover. The -01 revision did not say whether a leaf covered the + record's signatures; this revision says it does, so a record cannot + be re-signed without changing the root. 13. Protocol Endpoints - This section specifies the operations a Facilitator exposes. Base - URIs are not fixed by this document; they are discovered from the - endpoints member of the capability document (Section 8), so a - Facilitator may mount them anywhere on its origin. + This section specifies the operations a Facilitator exposes. This + document fixes no base URI; each is discovered from the endpoints + member of the capability document (Section 8), so a Facilitator may + mount them anywhere on its origin. Propose a contract: POST {contract}; body, a contract; 201 with a Status. @@ -2258,8 +2457,19 @@ Internet-Draft PACT September 2026 Register a child: POST {contract}/{id}/children; body, the child's contract; 201 with a Status. - Supply a child's outcome: POST {contract}/{id}/children/{child_id}; - body, the child's Outcome Record; 200 with a Status. + Supply a child's outcome: POST + + + +Sharma Expires 20 March 2027 [Page 44] + +Internet-Draft PACT September 2026 + + + {contract}/{id}/children/{child_hash}, where child_hash is the + digest by which the registration identifies the child; body, the + child's Outcome Record; 200 with a Status. This resource is keyed + by digest, so the id rule of Section 13.2 does not apply to it. Submit a Delivery: POST {delivery}; body, a Delivery; 202 with a Status. @@ -2273,12 +2483,24 @@ Internet-Draft PACT September 2026 Retrieve an Outcome Record: GET {outcome}/{id}; 200 with the Outcome Record. + A request naming a contract the Facilitator does not hold is refused + as unknown-contract (404). A body larger than the Facilitator + accepts is refused as payload-too-large (413). A failure inside the + Facilitator is reported as internal-error (500), the one problem type + that names no rule. A GET of an Outcome Record before the terminal + entry is refused as wrong-state. + All requests and responses use the media types defined in Section 19. All requests MUST be made over HTTPS, following the recommendations of [RFC9325]. Status codes are as defined in [RFC9110]. A Delivery - and a Challenge are answered 202 (Accepted) rather than 201 because - acceptance of the bytes is not acceptance of the work; what follows - depends on a Verdict the Facilitator does not itself produce. + and a Challenge are answered 202 (Accepted) because, in the sense of + [RFC9110] Section 15.3.3, their processing is not complete when the + response is sent: what either record leads to may depend on a Verdict + the Facilitator does not itself produce. A contract and a Verdict + are answered 201 (Created); the contract is the resource the Location + header names, and a Verdict, to which this document gives no resource + of its own, is identified by the digest the Status in the response + carries. A Facilitator authenticates the sender of a POST by the signature on the body, and by nothing else in this document: it MUST reject a @@ -2293,7 +2515,9 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 41] + + +Sharma Expires 20 March 2027 [Page 45] Internet-Draft PACT September 2026 @@ -2304,23 +2528,26 @@ Internet-Draft PACT September 2026 required to sign it. A Facilitator MUST perform the checks in Section 14, Section 5.3 and Section 9.1 before creating the resource, MUST refuse a contract whose parties.facilitator is not itself or - whose price.settlement, network or asset it does not advertise + whose price.settlement, network or currency it does not advertise (facilitator-mismatch, settlement-unsupported), and MUST refuse - otherwise with the problem type that names the rule. + otherwise with the problem type that names the rule. settlement- + unsupported also covers a price stated in a currency other than + max_contract_value's and a price above it; a verification profile the + Facilitator does not list is refused as settlement-unsupported. POST /pact/v2/contracts HTTP/1.1 Host: settle.example Content-Type: application/vnd.pact.contract+json { "pact": "0.2", "type": "VerifiableTaskContract", - "id": "vtc_7f3a91", ... } + "id": "vtc_9f2c11", ... } HTTP/1.1 201 Created - Location: /pact/v2/contracts/vtc_7f3a91 + Location: /pact/v2/contracts/vtc_9f2c11 Content-Type: application/vnd.pact.status+json { "pact": "0.2", "type": "ContractStatus", - "vtc_id": "vtc_7f3a91", "state": "ACCEPTED", + "vtc_id": "vtc_9f2c11", "state": "ACCEPTED", "trace": [ { "event": "accepted", ... } ], ... } 13.2. Idempotency @@ -2336,20 +2563,17 @@ Internet-Draft PACT September 2026 Where a POST carries the same object id as an existing resource but a different digest, the Facilitator MUST respond 409 (Conflict) - (object-conflict). Retrying a submission is therefore always safe, - and altering one never is. - - - - + (object-conflict). Retrying a submission is therefore safe when the + same bytes are resent, and altering one never is. A record signed + afresh is a different record with a different digest, not a retry: + ECDSA signatures are randomized unless produced as [RFC6979] + describes, so a client signing with ES256 or ES384 SHOULD sign + deterministically or keep the bytes it sent and resend those. - - - -Sharma Expires 20 March 2027 [Page 42] +Sharma Expires 20 March 2027 [Page 46] Internet-Draft PACT September 2026 @@ -2367,16 +2591,18 @@ Internet-Draft PACT September 2026 violated, because a conformance failure a caller cannot locate is a failure of the specification. + ========== NOTE: '\' line wrapping per RFC 8792 =========== + HTTP/1.1 422 Unprocessable Content Content-Type: application/problem+json { - "type": "tag:laxsharma79@gmail.com,2026:pact:problem: - signatures-unordered", + "type": "tag:laxsharma79@gmail.com,2026:pact:problem:signatur\ + es-unordered", "title": "Signature set not sorted", "status": 422, - "detail": "the second entry's kid sorts before the first's - after normalization.", + "detail": "the second entry's kid sorts before the first's afte\ + r normalization.", "section": "14.1" } @@ -2400,16 +2626,16 @@ Internet-Draft PACT September 2026 |-- GET {outcome}/id -->| | |<-- 200 Outcome -------| | - Figure 13: HTTP exchange for the flow in Figure 1 - -Sharma Expires 20 March 2027 [Page 43] +Sharma Expires 20 March 2027 [Page 47] Internet-Draft PACT September 2026 + Figure 13: HTTP exchange for the flow in Figure 1 + 14. Conformance Every rule a PACT conformance checker enforces is stated in this @@ -2423,11 +2649,12 @@ Internet-Draft PACT September 2026 14.1. Signatures Every signature carried by a VTC, Delivery, Verdict, Challenge, - Status, Outcome Record or capability document is a JWS [RFC7515] in - the General JSON Serialization of Section 7.2.1 of that document, - with the payload detached as its Appendix F describes. The payload - is BASE64URL of the JCS-canonical bytes of the object with the - signing member removed, so the JWS Signing Input is + Status, Outcome Record or capability document has, for each signer, + the form of one signature object of the JWS [RFC7515] General JSON + Serialization, Section 7.2.1 of that document, with the payload + detached as its Appendix F describes. The payload is BASE64URL of + the JCS-canonical bytes of the object with the signing member + removed, so the JWS Signing Input is ASCII(BASE64URL(UTF8(protected)) || "." || BASE64URL(JCS(object))) exactly as Section 5.1 of [RFC7515] defines it. The payload is never transmitted; a verifier reconstructs it from the object it holds, and @@ -2436,11 +2663,13 @@ Internet-Draft PACT September 2026 * The protected header MUST carry alg, kid and typ. - * alg MUST be ES256 or ES384 [RFC7518], or EdDSA [RFC8037] with an - Ed25519 key; a verifier MAY also accept Ed448. A verifier MUST - reject any other value, and MUST reject none. Absent an allowlist - an attacker selects the algorithm, which permits both unsigned - acceptance and confusion of a public key for a symmetric secret. + * alg MUST be Ed25519 [RFC9864] with an Ed25519 key, or ES256 or + ES384 [RFC7518] with a P-256 or P-384 key. A verifier MUST reject + any other value, including the polymorphic EdDSA identifier of + [RFC8037] that [RFC9864] deprecates, and MUST reject none + (algorithm-not-permitted). Absent an allowlist an attacker + selects the algorithm, which permits both unsigned acceptance and + confusion of a public key for a symmetric secret. * kid MUST appear inside the protected header and MUST NOT be carried as a sibling of it. A key identifier outside the signed @@ -2448,6 +2677,23 @@ Internet-Draft PACT September 2026 publish a key document to re-attribute a genuine signature to itself. + * The protected header MUST NOT carry jwk, jku, x5c, x5u, x5t, + x5t#S256 or crit, and a signature entry MUST NOT carry an + unprotected header; a verifier MUST reject an entry carrying any + of them (signature-invalid). A key travels by reference and never + inline, so that the kid rule cannot be bypassed. + + + +Sharma Expires 20 March 2027 [Page 48] + +Internet-Draft PACT September 2026 + + + * The resolved key MUST be of the type and curve alg requires: + Ed25519 for Ed25519, P-256 for ES256, P-384 for ES384. A mismatch + is signature-invalid. + * typ MUST be the full media type of the object signed, including the application/ prefix, so that a signature over one object type cannot be replayed as a signature over another. Section 4.1.9 of @@ -2456,16 +2702,6 @@ Internet-Draft PACT September 2026 character for character. Explicit typing follows Section 3.11 of [RFC8725]. - - - - - -Sharma Expires 20 March 2027 [Page 44] - -Internet-Draft PACT September 2026 - - * A signatures array MUST be sorted by the normalized kid of its entries (Section 9.1), ties broken by the unnormalized kid, both compared as sequences of Unicode code points; a verifier MUST @@ -2480,7 +2716,7 @@ Internet-Draft PACT September 2026 [RFC7518] fixes the encoding and not which of the two valid s values is accepted; accepting both lets anyone holding a valid signature produce a second one over the same bytes without the - key, and a second signature is a second digest. EdDSA + key, and a second signature is a second digest. Ed25519 verification per [RFC8032] already rejects a non-canonical S, so the rule is stated for ECDSA only. @@ -2502,25 +2738,24 @@ Internet-Draft PACT September 2026 normalization in Section 9.1, the party identifier the signature is attributed to. Verifying a signature establishes that the holder of that key signed; that the key belongs to the party is a property of - the identity method, and this document does not add to it. An - identity system for agents defined elsewhere, such as - [I-D.ietf-wimse-aims], is used by naming its identifiers here and - resolving them by its rules. - -14.2. Rules Not Expressible in a Schema - - * parties.buyer and parties.seller MUST be distinct after the - normalization in Section 9.1 (parties-not-distinct). +Sharma Expires 20 March 2027 [Page 49] + +Internet-Draft PACT September 2026 + the identity method, and this document does not add to it. An + identity system for agents defined elsewhere, such as + [I-D.ietf-wimse-aims], is used by naming its identifiers here in one + of these two forms; a further form needs a resolution rule added to + this list, which is the one change it would take. -Sharma Expires 20 March 2027 [Page 45] - -Internet-Draft PACT September 2026 +14.2. Rules Not Expressible in a Schema + * parties.buyer and parties.seller MUST be distinct after the + normalization in Section 9.1 (parties-not-distinct). * A contract MUST carry exactly one verifying signature whose kid covers parties.buyer, exactly one whose kid covers parties.seller, @@ -2528,7 +2763,8 @@ Internet-Draft PACT September 2026 signatures is not sufficient: two signatures covering one identifier MUST be rejected. - * challenge.window_seconds MUST be greater than zero, and + * challenge.window_seconds, challenge.max_dispute_seconds and + verification.max_verdict_seconds MUST be greater than zero, and task.deadline MUST be later than the instant of acceptance (deadline-invalid). @@ -2545,102 +2781,109 @@ Internet-Draft PACT September 2026 that profile's schema (terms-unsupported, terms-parameters- invalid). - * Every amount MUST have the form in Section 2 (amount-invalid), and - every object MUST validate against the schema published for its - media type (schema-invalid). + * Every object MUST validate against the schema published for its + media type (schema-invalid), which includes the form of every + amount (Section 2); an amount carrying more decimal places than + the settlement binding named in price.settlement supports is + refused (amount-invalid). 14.3. Test Vectors - Each rule above has an accepting and a rejecting form. A conformance - suite built from this section alone, with no reference to any + Most rules above have an accepting and a rejecting form; the table + carries the ones a suite most often gets wrong. A conformance suite + built from this section alone, with no reference to any implementation, should reach the same verdicts. Rejecting vectors name the rule they violate. - +======+=========================================+==========+ - | ID | Mutation from a valid object | Expect | - +======+=========================================+==========+ - | V-01 | unmodified valid VTC | accept | - +------+-----------------------------------------+----------+ - | V-02 | alg set to none | reject | - +------+-----------------------------------------+----------+ - | V-03 | alg set to HS256 | reject | - +------+-----------------------------------------+----------+ - | V-04 | kid moved outside the protected header | reject | - +------+-----------------------------------------+----------+ - | V-05 | typ of a Delivery on a VTC signature | reject | - +------+-----------------------------------------+----------+ - | V-06 | buyer and seller set to the same | reject | - -Sharma Expires 20 March 2027 [Page 46] +Sharma Expires 20 March 2027 [Page 50] Internet-Draft PACT September 2026 - | | identifier | | - +------+-----------------------------------------+----------+ - | V-07 | buyer and seller differing only by | reject | - | | trailing "/" | | - +------+-----------------------------------------+----------+ - | V-08 | two signatures, both from the buyer | reject | - +------+-----------------------------------------+----------+ - | V-09 | window_seconds of 0 | reject | - +------+-----------------------------------------+----------+ - | V-10 | acceptance as an empty object | reject | - +------+-----------------------------------------+----------+ - | V-11 | harness_uri with harness_hash removed | reject | - +------+-----------------------------------------+----------+ - | V-12 | terms.profile_hash not advertised by | reject | - | | the Facilitator | | - +------+-----------------------------------------+----------+ - | V-13 | terms.parameters failing the profile's | reject | - | | schema | | - +------+-----------------------------------------+----------+ - | V-14 | Delivery with evidence absent | reject, | - | | | no entry | - +------+-----------------------------------------+----------+ - | V-15 | child whose buyer is not the parent's | reject | - | | seller | | - +------+-----------------------------------------+----------+ - | V-16 | child with L(child) not earlier than | reject | - | | L(parent) | | - +------+-----------------------------------------+----------+ - | V-17 | Verdict signed by the seller | reject | - +------+-----------------------------------------+----------+ - | V-18 | object keys ordered by code point, with | digest | - | | a supplementary-plane key | mismatch | - +------+-----------------------------------------+----------+ - | V-19 | buyer and seller differing only in the | accept | - | | case of a did:web path | | - +------+-----------------------------------------+----------+ - | V-20 | object carrying a member this document | reject | - | | does not define for it | | - +------+-----------------------------------------+----------+ - | V-21 | signatures not sorted by normalized kid | reject | - +------+-----------------------------------------+----------+ - | V-22 | ECDSA signature with s above n/2 | reject | - +------+-----------------------------------------+----------+ - | V-23 | Verdict with delivery_hash computed | reject | - | | over the Delivery without its signature | | - +------+-----------------------------------------+----------+ - | V-24 | Outcome Record whose transfers overdraw | reject | - | | an account of the profile | | + +======+==========================================+==========+ + | ID | Mutation from a valid object | Expect | + +======+==========================================+==========+ + | V-01 | unmodified valid VTC | accept | + +------+------------------------------------------+----------+ + | V-02 | alg set to none | reject | + +------+------------------------------------------+----------+ + | V-03 | alg set to HS256 | reject | + +------+------------------------------------------+----------+ + | V-04 | kid moved outside the protected header | reject | + +------+------------------------------------------+----------+ + | V-05 | typ of a Delivery on a VTC signature | reject | + +------+------------------------------------------+----------+ + | V-06 | buyer and seller set to the same | reject | + | | identifier | | + +------+------------------------------------------+----------+ + | V-07 | buyer and seller differing only by | reject | + | | trailing "/" | | + +------+------------------------------------------+----------+ + | V-08 | two signatures, both from the buyer | reject | + +------+------------------------------------------+----------+ + | V-09 | window_seconds of 0 | reject | + +------+------------------------------------------+----------+ + | V-10 | acceptance as an empty object | reject | + +------+------------------------------------------+----------+ + | V-11 | harness_uri with harness_hash removed | reject | + +------+------------------------------------------+----------+ + | V-12 | terms.profile_hash not advertised by the | reject | + | | Facilitator | | + +------+------------------------------------------+----------+ + | V-13 | terms.parameters failing the profile's | reject | + | | schema | | + +------+------------------------------------------+----------+ + | V-14 | Delivery with evidence absent | reject, | + | | | no entry | + +------+------------------------------------------+----------+ + | V-15 | child whose buyer is not the parent's | reject | + | | seller | | + +------+------------------------------------------+----------+ + | V-16 | child with L(child) not earlier than | reject | + | | L(parent) | | + +------+------------------------------------------+----------+ + | V-17 | Verdict signed by the seller | reject | + +------+------------------------------------------+----------+ + | V-18 | object keys ordered by code point rather | digest | + | | than UTF-16 unit, a supplementary-plane | mismatch | + | | key beside one in U+E000 to U+FFFF | | + +------+------------------------------------------+----------+ -Sharma Expires 20 March 2027 [Page 47] +Sharma Expires 20 March 2027 [Page 51] Internet-Draft PACT September 2026 - +------+-----------------------------------------+----------+ - | V-25 | a number serialized by the host | digest | - | | language's default formatter, such as | mismatch | - | | 1.0 for the float one | | - +------+-----------------------------------------+----------+ - - Table 3: Conformance vectors + | V-19 | buyer and seller differing only in the | accept | + | | case of a did:web path | | + +------+------------------------------------------+----------+ + | V-20 | object carrying a member this document | reject | + | | does not define for it | | + +------+------------------------------------------+----------+ + | V-21 | signatures not sorted by normalized kid | reject | + +------+------------------------------------------+----------+ + | V-22 | ECDSA signature with s above n/2 | reject | + +------+------------------------------------------+----------+ + | V-23 | Verdict with delivery_hash computed over | reject | + | | the Delivery without its signature | | + +------+------------------------------------------+----------+ + | V-24 | Outcome Record whose transfers overdraw | reject | + | | an account of the profile | | + +------+------------------------------------------+----------+ + | V-25 | a number serialized by the host | digest | + | | language's default formatter, such as | mismatch | + | | 1.0 for the float one | | + +------+------------------------------------------+----------+ + | V-26 | a protected header carrying a member | reject | + | | Section 14.1 forbids: jwk, jku, x5c, | | + | | x5u, x5t, x5t#S256 or crit | | + +------+------------------------------------------+----------+ + + Table 3: Conformance vectors V-07 and V-18 are the two most often got wrong. V-07 fails wherever party comparison is a string equality on unnormalized identifiers. @@ -2651,18 +2894,25 @@ Internet-Draft PACT September 2026 and the -01 reference validator made it. V-25 is the number half of the V-18 mistake: [RFC8785] prints numbers as ECMAScript does, so the float one is 1 and never 1.0. The -02 reference canonicalizer - printed 1.0 until this vector caught it, and every digest in - Section 15 changed when it was fixed. + printed 1.0 until this vector caught it, and the spec_hash, vtc_hash + and delivery_hash of Section 15 changed when it was fixed. 15. Worked Example - The tables and digests below are the reference repository's, at the - tag named in Section 16. The object figures in earlier sections use - short illustrative identifiers for page width; the repository - examples carry the full ones, and the digests here are computed over - those. The figures that the -01 revision printed here about a bond - and a required detection rate are now the profile's, and Appendix A - carries them. + The digests below are the reference repository's, at the tag named in + Section 16. The object figures in earlier sections are the + repository's objects with their signatures abbreviated and the + contract's parameters elided; the digests here are computed over the + full objects. The figures that the -01 revision printed here about a + bond and a required detection rate are now the profile's; Appendix A + carries the rule and the parameters. + + + +Sharma Expires 20 March 2027 [Page 52] + +Internet-Draft PACT September 2026 + A buyer commissions a data transformation at a price of 180.00 USDC under the verdict-first flow, the acceptance verification profile, @@ -2676,19 +2926,12 @@ Internet-Draft PACT September 2026 d27ff6bee37f05531823b72 criteria_hash sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b83\ 51c9c55eb9316416265fc1b - profile_hash sha256:00d71829f6f9192b43b929d0154a6eb409f5fc114\ - 7743326dabd45bda546dc62 - vtc_hash sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ - 225fbbd4ebace4fb980f1c2 - delivery_hash sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ - aaf2f186702504fba242ffb - - - -Sharma Expires 20 March 2027 [Page 48] - -Internet-Draft PACT September 2026 - + profile_hash sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf6956\ + e835df2653b99d71d363a68 + vtc_hash sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff494\ + 9304c2c855ca1d746ba6459 + delivery_hash sha256:6bcbb831ea27a8754a0df9b44361be12411e45370\ + cb3023331bc8bfbd924fcbe criteria_hash is the manifest digest of Section 5.1 over the acceptance instrument bundle, and the same value appears as @@ -2698,36 +2941,55 @@ Internet-Draft PACT September 2026 bundle. vtc_hash is the digest of the signed contract, and delivery_hash of the signed Delivery, both per Section 2. - Every value above changed from the -01 revision, for four reasons - that are each recorded so that a reader comparing the two documents - can account for the difference: spec_hash because the TaskSpec now - carries the sibling hashes Section 5.1 always required; vtc_hash - because the contract's members changed (Appendix B) and because - spec_hash did; delivery_hash because it now covers the Delivery's - signature; and profile_hash because it did not exist. + The -01 revision printed three of the values above, spec_hash, + criteria_hash and vtc_hash, and each differs from what it printed, + for reasons recorded so that a reader comparing the two documents can + account for the difference. spec_hash and vtc_hash differ because the + -01 canonicalizer serialized numbers as the host language printed + them (Section 14.3, V-25); spec_hash also because the TaskSpec now + carries the sibling hashes Section 5.1 always required, and vtc_hash + also because the contract's members changed (Appendix B). + criteria_hash carries no number; it differs because the two files of + the instrument bundle were edited to drop their mention of the + withdrawn call-for-bids example. profile_hash is new, and + delivery_hash, which the -01 figures showed only as a placeholder, + now covers the Delivery's signature. + + The traces the reference implementation records for this contract on + the path of Figure 1 and on the dispute path of Figure 7 carry the + event sequences of the first two vectors in the profile's bundle, and + the transfer lists the profile produces for them are those vectors' + lists; the vectors name their objects by placeholder digests, so the + match is of sequence and lists, not of bytes. Appendix A prints both + lists. + + + +Sharma Expires 20 March 2027 [Page 53] + +Internet-Draft PACT September 2026 - The trace the reference implementation records for this contract on - the path of Figure 1, and on the dispute path of Figure 7, together - with the transfer lists the profile produces for each, are the - vectors in the profile's bundle, and Appendix A prints them. 16. Implementation Status This section records the status of known implementations of this - document per [RFC7942], and is to be removed before publication as an - RFC. + document per [RFC7942]. The section and the reference to [RFC7942] + are to be removed before publication as an RFC, and the listing of an + implementation here implies no endorsement by the IETF. One implementation is known to the author, and the author wrote it: - https://github.com/pact-spec/spec, under the Revised BSD licence. At + https://github.com/pact-spec/spec, under the Apache License 2.0. At tag v0.2.0 it comprises the object schemas, the examples whose - digests Section 15 prints, a conformance validator that runs 103 + digests Section 15 prints, a conformance validator that runs 107 checks including every vector of Section 14.3, a Facilitator serving the endpoints of Section 13 with the profile of Appendix A, and clients for the other roles. Its previous tag, v0.1.0, implemented the -01 revision and is the source of the measurements the author has published about it. No second implementation exists, so nothing in Section 1.4 has been tested, and this document claims no - interoperability. + interoperability. It is an individual submission and the product of + no working group; the implementation is a prototype, the information + is current as of the tag named above, and the contact is the author. 17. Security Considerations @@ -2741,55 +3003,8 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 49] - -Internet-Draft PACT September 2026 - +=============+=====================+==============+===============+ - | Party | Enforced against it | Recorded | Checkable by | - | | | about it | | - +=============+=====================+==============+===============+ - | Buyer | cannot alter the | its | anyone | - | | task, instrument or | signature on | holding the | - | | terms after | the | contract | - | | signing; cannot | contract; | | - | | attach a child to a | any | | - | | contract it is not | Challenge it | | - | | party to | signs | | - +-------------+---------------------+--------------+---------------+ - | Seller | cannot deliver | its | anyone | - | | against a | signature on | holding the | - | | substituted | the contract | contract and | - | | instrument or | and the | the Delivery | - | | input; cannot judge | Delivery; | | - | | its own Delivery; | the Verdicts | | - | | cannot re-sign a | and | | - | | record without | Challenges | | - | | changing every | on its | | - | | digest over it | Delivery | | - +-------------+---------------------+--------------+---------------+ - | Verifier | cannot be a party | its | anyone | - | | to the contract; | Verdicts, | holding the | - | | must commit to the | superseded | Delivery and | - | | instrument it ran | ones | the | - | | and its results | included | instrument | - +-------------+---------------------+--------------+---------------+ - | Facilitator | nothing | what it | any holder of | - | | | chose to | two of its | - | | | sign, in the | Statuses, for | - | | | order it | equivocation; | - | | | chose, on a | nobody, for | - | | | clock that | omission or | - | | | is its own | for time, | - | | | | without a | - | | | | witness | - | | | | outside this | - | | | | document | - +-------------+---------------------+--------------+---------------+ - - Table 4: What the protocol enforces, records and lets others - check, by party @@ -2797,7 +3012,72 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 50] + + + + + + + + + +Sharma Expires 20 March 2027 [Page 54] + +Internet-Draft PACT September 2026 + + + +=============+======================+==============+===============+ + | Party | Enforced against it | Recorded | Checkable by | + | | | about it | | + +=============+======================+==============+===============+ + | Buyer | cannot alter the | its | anyone | + | | task, instrument or | signature | holding the | + | | terms after signing; | on the | contract | + | | cannot attach a | contract; | | + | | child to a contract | any | | + | | it is not party to | Challenge | | + | | | it signs | | + +-------------+----------------------+--------------+---------------+ + | Seller | cannot deliver | its | anyone | + | | against a | signature | holding the | + | | substituted | on the | contract and | + | | instrument or input; | contract | the Delivery | + | | is refused as | and the | | + | | Verifier when it | Delivery; | | + | | signs under a party | the | | + | | identifier; cannot | Verdicts | | + | | re-sign a record | and | | + | | without changing | Challenges | | + | | every digest over it | on its | | + | | | Delivery | | + +-------------+----------------------+--------------+---------------+ + | Verifier | cannot be a party to | its | anyone | + | | the contract; must | Verdicts, | holding the | + | | commit to the | superseded | Delivery and | + | | instrument it ran | ones | the | + | | and its results | included | instrument | + +-------------+----------------------+--------------+---------------+ + | Facilitator | nothing | what it | any holder of | + | | | chose to | two of its | + | | | sign, in | Statuses, for | + | | | the order | equivocation; | + | | | it chose, | nobody, for | + | | | on a clock | omission or | + | | | that is | for time, | + | | | its own | without a | + | | | | witness | + | | | | outside this | + | | | | document | + +-------------+----------------------+--------------+---------------+ + + Table 4: What the protocol enforces, records and lets others + check, by party + + + + + +Sharma Expires 20 March 2027 [Page 55] Internet-Draft PACT September 2026 @@ -2824,8 +3104,9 @@ Internet-Draft PACT September 2026 SHOULD retain every Status it receives, and a party that submitted a record and holds no Status for it has a claim it can make only outside this protocol. Making omission attributable needs a witness - the Facilitator does not control, such as a monitor with a gossip - path of the kind [RFC9162] assumes, and this document specifies none. + the Facilitator does not control, such as the client gossip that + [RFC9162] Section 11.3 mentions and leaves undefined, and this + document specifies none. Time is the Facilitator's. Every instant in a trace is read from its clock, and nothing in this document lets a party prove that a @@ -2852,8 +3133,7 @@ Internet-Draft PACT September 2026 - -Sharma Expires 20 March 2027 [Page 51] +Sharma Expires 20 March 2027 [Page 56] Internet-Draft PACT September 2026 @@ -2885,13 +3165,13 @@ Internet-Draft PACT September 2026 The -00 revision committed harness_uri as a string. The bytes at that URI were covered by nothing. A Buyer could therefore sign a contract, replace the acceptance instrument afterwards, run the - replacement, and submit its failure as a textbook-valid fraud proof. - Cost of the attack: one file overwrite. The mirror attack works - against a Seller that hosts the input sample. Section 5.1 requires a - sibling hash over the dereferenced bytes for every URI inside - committed content, and Section 7.2 requires a Verdict to commit to - the instrument it actually ran, which closes the same attack from the - verification side. + replacement, and submit its failure as a textbook-valid proof of + nonconformance. Cost of the attack: one file overwrite. The mirror + attack works against a Seller that hosts the input sample. + Section 5.1 requires a sibling hash over the dereferenced bytes for + every URI inside committed content, and Section 7.2 requires a + Verdict to commit to the instrument it actually ran, which closes the + same attack from the verification side. 17.5. Fetching Committed Content @@ -2909,7 +3189,7 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 52] +Sharma Expires 20 March 2027 [Page 57] Internet-Draft PACT September 2026 @@ -2965,7 +3245,7 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 53] +Sharma Expires 20 March 2027 [Page 58] Internet-Draft PACT September 2026 @@ -2977,11 +3257,11 @@ Internet-Draft PACT September 2026 A re-execution profile that does not state what determinism it assumes cuts both ways. An honest Seller doing model-assisted work - is convicted by a re-execution that differs for ordinary reasons. A - cheating Seller escapes any fraud proof by asserting nondeterminism, - unfalsifiably. A verification profile MUST state whether it is - deterministic and what tolerance applies, and a contract naming one - that does not is not safely enforceable by anyone. + is found wrong by a re-execution that differs for ordinary reasons. + A cheating Seller escapes any proof of nonconformance by asserting + nondeterminism, unfalsifiably. A verification profile MUST state + whether it is deterministic and what tolerance applies, and a + contract naming one that does not cannot be judged safely by anyone. 17.11. Fabricated History @@ -2997,16 +3277,17 @@ Internet-Draft PACT September 2026 17.12. Retrieval - A GET on a contract's Status or Outcome Record MUST be refused unless - the requester is a party named in the contract's parties, the - identifier in the contract's parent.facilitator, or a party the - Facilitator has chosen to admit; a Facilitator MAY open retrieval - more widely and SHOULD say so in its capability document. How a - requester proves which identifier it is, on a GET with no body to - sign, is an HTTP-layer matter this document leaves to the deployment. - The -01 revision left retrieval unauthenticated by default, which - published every contract graph a Facilitator held to anyone who could - guess an identifier. + A GET on a contract's Status or Outcome Record MUST be refused + (retrieval-restricted) unless the requester is a party named in the + contract's parties, the identifier in the contract's + parent.facilitator, or a party the Facilitator has chosen to admit; a + Facilitator MAY open retrieval more widely and SHOULD say so in its + capability document (retrieval, Section 3.9). How a requester proves + which identifier it is, on a GET with no body to sign, is an HTTP- + layer matter this document leaves to the deployment. The -01 + revision left retrieval unauthenticated by default, which published + every contract graph a Facilitator held to anyone who could guess an + identifier. 17.13. Key Compromise and Rotation @@ -3014,29 +3295,33 @@ Internet-Draft PACT September 2026 signs contracts the party never agreed to. Rotation and revocation belong to the identity method behind the kid (Section 14.1.1), and this document does not restate them. Two things it does require: a - Facilitator MUST record, with each record it accepts, the key - material or its digest as resolved at the time of acceptance, so that - a later rotation does not make an earlier signature unverifiable; and - a Facilitator MUST NOT accept a record whose kid resolves to a key + Facilitator MUST retain, for as long as it retains a record it + accepted, the key material or its digest as resolved at the time of + acceptance, and SHOULD make it available to a party retrieving the -Sharma Expires 20 March 2027 [Page 54] +Sharma Expires 20 March 2027 [Page 59] Internet-Draft PACT September 2026 - the identity method marks as revoked at the time of acceptance. + record, so that a later rotation does not make an earlier signature + unverifiable; and a Facilitator MUST NOT accept a record whose kid + resolves to a key the identity method marks as revoked at the time of + acceptance. 17.14. Denial of Service by Challenge Every accepted Challenge costs an independent evaluation. Without a cost to the Challenger, a party can exhaust a Verifier's or a - Facilitator's capacity by challenging every Delivery. The deposit of - Section 7.3 is one defence, and it is a MAY because a deposit also - deters the honest challenger an open model relies on. A Facilitator - that requires no deposit SHOULD rate-limit Challenges per Challenger - and per contract, and SHOULD publish that it does so. + Facilitator's capacity by challenging every Delivery. A deposit + required by a terms profile, advertised as challenge_deposit + (Section 7.3), is one defence, and this document requires none, since + a deposit also deters the honest Challenger an open model relies on. + A Facilitator whose profiles require no deposit SHOULD rate-limit + Challenges per Challenger and per contract, and SHOULD publish that + it does so. 18. Privacy Considerations @@ -3072,12 +3357,7 @@ Internet-Draft PACT September 2026 - - - - - -Sharma Expires 20 March 2027 [Page 55] +Sharma Expires 20 March 2027 [Page 60] Internet-Draft PACT September 2026 @@ -3085,18 +3365,18 @@ Internet-Draft PACT September 2026 18.3. Challenger Access An open challenge model requires that some party outside the contract - can obtain the deliverable and the input in order to build a fraud - proof. That is in direct conflict with confidentiality of both. The - conflict is real and this document does not dissolve it. What it - does is make the choice visible: a contract whose content cannot be - disclosed to a Challenger will receive no Challenge from outside its - parties, and a terms profile that counts on one has counted on - nothing. + can obtain the deliverable and the input in order to build a proof of + nonconformance. That is in direct conflict with confidentiality of + both. The conflict is real and this document does not dissolve it. + What it does is make the choice visible: a contract whose content + cannot be disclosed to a Challenger will receive no Challenge from + outside its parties, and a terms profile that counts on one has + counted on nothing. 18.4. Retention - Retention duties stated for dispute purposes can conflict with - erasure rights asserted by a data subject. Contracts SHOULD state a + Retention periods stated for dispute purposes can conflict with + erasure requests from a data subject. Contracts SHOULD state a retention period, and implementers should be aware that a hash commitment survives deletion of the content it commits to, which is usually the property they want and occasionally the one they must @@ -3133,7 +3413,7 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 56] +Sharma Expires 20 March 2027 [Page 61] Internet-Draft PACT September 2026 @@ -3146,8 +3426,9 @@ Internet-Draft PACT September 2026 Interoperability considerations: Objects MUST be canonicalized per [RFC8785] before hashing or signing. Implementations that canonicalize by sorting object keys on Unicode code point rather - than UTF-16 code unit will produce divergent digests for keys - outside the Basic Multilingual Plane. + than UTF-16 code unit can produce a divergent digest when a key + outside the Basic Multilingual Plane is compared with one whose + first differing unit lies in U+E000 to U+FFFF. Published specification: This document @@ -3188,8 +3469,7 @@ Internet-Draft PACT September 2026 - -Sharma Expires 20 March 2027 [Page 57] +Sharma Expires 20 March 2027 [Page 62] Internet-Draft PACT September 2026 @@ -3223,8 +3503,9 @@ Internet-Draft PACT September 2026 The -01 revision asked for these in the standards tree under the names pact-contract+json and so on. Registration in that tree from outside the IETF stream needs approval this document does not have - ([RFC6838], Section 3.1), and the vendor tree is where an - individual's specification belongs. + ([RFC6838], Section 3.1), and [RFC6838] Section 3.2 opens the vendor + tree to anyone who interchanges files associated with a publicly + available product. 19.2. Well-Known URI @@ -3244,8 +3525,7 @@ Internet-Draft PACT September 2026 - -Sharma Expires 20 March 2027 [Page 58] +Sharma Expires 20 March 2027 [Page 63] Internet-Draft PACT September 2026 @@ -3260,14 +3540,18 @@ Internet-Draft PACT September 2026 registration. Each is the identifier in the table appended to the prefix tag:laxsharma79@gmail.com,2026:pact:problem:, a tag URI [RFC4151] under the author's control. A tag URI is an identifier and - is not dereferenceable, which is why it was chosen over the -01 - revision's prefix on a code-hosting site: an identifier should not - change when hosting does. Documentation for every type is maintained - in the repository named in Section 16. Each entry carries the - identifier, the HTTP status it accompanies, and the section stating - the rule it reports. A terms profile that refuses a request defines - its own types under its own prefix and reports them as Section 13.3 - says. + does not dereference. [RFC9457] Section 4 says a type URI SHOULD + resolve to documentation; this document departs from that on purpose, + so that an identifier does not change when hosting does, which the + -01 revision's prefix on a code-hosting site could not promise, and + the section named for each type is its documentation. The list of + types, each with its status and the section that defines it, is + printed by the reference implementation in the repository named in + Section 16, and the section named for each type says what it means. + Each entry carries the identifier, the HTTP status it accompanies, + and the section stating the rule it reports. A terms profile that + refuses a request defines its own types under its own prefix and + reports them as Section 13.3 says. +=============================+========+===============+ | Identifier | Status | Defined in | @@ -3294,18 +3578,18 @@ Internet-Draft PACT September 2026 +-----------------------------+--------+---------------+ | no-recorded-delivery | 409 | Section 7.2 | +-----------------------------+--------+---------------+ - | object-conflict | 409 | Section 13.2 | - +-----------------------------+--------+---------------+ - | parent-unresolvable | 422 | Section 10.2 | - +-----------------------------+--------+---------------+ -Sharma Expires 20 March 2027 [Page 59] +Sharma Expires 20 March 2027 [Page 64] Internet-Draft PACT September 2026 + | object-conflict | 409 | Section 13.2 | + +-----------------------------+--------+---------------+ + | parent-unresolvable | 422 | Section 10.2 | + +-----------------------------+--------+---------------+ | parties-not-distinct | 422 | Section 14.2 | +-----------------------------+--------+---------------+ | payload-too-large | 413 | Section 13 | @@ -3318,9 +3602,9 @@ Internet-Draft PACT September 2026 +-----------------------------+--------+---------------+ | settlement-unsupported | 422 | Section 13.1 | +-----------------------------+--------+---------------+ - | signature-invalid | 401 | Section 14.1 | + | signature-invalid | 400 | Section 14.1 | +-----------------------------+--------+---------------+ - | signature-missing | 401 | Section 14.2 | + | signature-missing | 400 | Section 14.2 | +-----------------------------+--------+---------------+ | signatures-unordered | 422 | Section 14.1 | +-----------------------------+--------+---------------+ @@ -3348,20 +3632,21 @@ Internet-Draft PACT September 2026 20. Normative References - [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate - Requirement Levels", BCP 14, RFC 2119, - DOI 10.17487/RFC2119, March 1997, - . -Sharma Expires 20 March 2027 [Page 60] +Sharma Expires 20 March 2027 [Page 65] Internet-Draft PACT September 2026 + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . @@ -3388,6 +3673,12 @@ Internet-Draft PACT September 2026 (JOSE)", RFC 8037, DOI 10.17487/RFC8037, January 2017, . + [RFC9864] Jones, M.B. and O. Steele, "Fully-Specified Algorithms for + JSON Object Signing and Encryption (JOSE) and CBOR Object + Signing and Encryption (COSE)", RFC 9864, + DOI 10.17487/RFC9864, October 2025, + . + [RFC8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, DOI 10.17487/RFC8032, January 2017, @@ -3397,6 +3688,16 @@ Internet-Draft PACT September 2026 DOI 10.17487/RFC7517, May 2015, . + + + + + +Sharma Expires 20 March 2027 [Page 66] + +Internet-Draft PACT September 2026 + + [RFC8615] Nottingham, M., "Well-Known Uniform Resource Identifiers (URIs)", RFC 8615, DOI 10.17487/RFC8615, May 2019, . @@ -3411,13 +3712,6 @@ Internet-Draft PACT September 2026 DOI 10.17487/RFC8259, December 2017, . - - -Sharma Expires 20 March 2027 [Page 61] - -Internet-Draft PACT September 2026 - - [RFC9162] Laurie, B., Messeri, E., and R. Stradling, "Certificate Transparency Version 2.0", RFC 9162, DOI 10.17487/RFC9162, December 2021, . @@ -3450,35 +3744,44 @@ Internet-Draft PACT September 2026 Addresses", RFC 4193, DOI 10.17487/RFC4193, October 2005, . - [I-D.bhutton-json-schema] - Wright, A., Andrews, H., Hutton, B., and G. Dennis, "JSON - Schema: A Media Type for Describing JSON Documents", Work - in Progress, Internet-Draft, draft-bhutton-json-schema-01, - 10 June 2022, . + + + + + +Sharma Expires 20 March 2027 [Page 67] + +Internet-Draft PACT September 2026 + [DID-CORE] W3C, "Decentralized Identifiers (DIDs) v1.0", W3C Recommendation, 19 July 2022, . [DID-WEB] W3C Credentials Community Group, "did:web Method - Specification", 2026, + Specification", Unofficial draft, undated; accessed 16 + September 2026, . -21. Informative References - - - -Sharma Expires 20 March 2027 [Page 62] - -Internet-Draft PACT September 2026 + [I-D.bhutton-json-schema] + Wright, A., Andrews, H., Hutton, B., and G. Dennis, "JSON + Schema: A Media Type for Describing JSON Documents", Work + in Progress, Internet-Draft, draft-bhutton-json-schema-01, + 10 June 2022, . +21. Informative References [RFC9334] Birkholz, H., Thaler, D., Richardson, M., Smith, N., and W. Pan, "Remote ATtestation procedureS (RATS) Architecture", RFC 9334, DOI 10.17487/RFC9334, January 2023, . + [RFC6979] Pornin, T., "Deterministic Usage of the Digital Signature + Algorithm (DSA) and Elliptic Curve Digital Signature + Algorithm (ECDSA)", RFC 6979, DOI 10.17487/RFC6979, August + 2013, . + [RFC9711] Lundblade, L., Mandyam, G., O'Donoghue, J., and C. Wallace, "The Entity Attestation Token (EAT)", RFC 9711, DOI 10.17487/RFC9711, April 2025, @@ -3500,6 +3803,13 @@ Internet-Draft PACT September 2026 DOI 10.17487/RFC8725, February 2020, . + + +Sharma Expires 20 March 2027 [Page 68] + +Internet-Draft PACT September 2026 + + [RFC7942] Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", BCP 205, RFC 7942, DOI 10.17487/RFC7942, July 2016, @@ -3510,6 +3820,18 @@ Internet-Draft PACT September 2026 RFCs", RFC 8792, DOI 10.17487/RFC8792, June 2020, . + [CAIP-2] Chain Agnostic Standards Alliance, "CAIP-2: Blockchain ID + Specification", Status: Final, 5 December 2019, + . + + [X402COMPLIANCE] + wowlegend (Tersign), pull request author, "Extension: + compliance-fields", Open pull request 2853 to x402- + foundation/x402, specs/extensions/compliance_fields.md, + unmerged as of September 2026, July 2026, + . + [RFC8555] Barnes, R., Hoffman-Andrews, J., McCarney, D., and J. Kasten, "Automatic Certificate Management Environment (ACME)", RFC 8555, DOI 10.17487/RFC8555, March 2019, @@ -3521,15 +3843,6 @@ Internet-Draft PACT September 2026 (CRL) Profile", RFC 5280, DOI 10.17487/RFC5280, May 2008, . - - - - -Sharma Expires 20 March 2027 [Page 63] - -Internet-Draft PACT September 2026 - - [RFC3647] Chokhani, S., Ford, W., Sabett, R., Merrill, C., and S. Wu, "Internet X.509 Public Key Infrastructure Certificate Policy and Certification Practices Framework", RFC 3647, @@ -3540,6 +3853,19 @@ Internet-Draft PACT September 2026 Version 1.0", RFC 2801, DOI 10.17487/RFC2801, April 2000, . + + + + + + + + +Sharma Expires 20 March 2027 [Page 69] + +Internet-Draft PACT September 2026 + + [I-D.ietf-httpapi-idempotency-key-header] Jena, J. and S. Dalal, "The Idempotency-Key HTTP Header Field", Work in Progress, Internet-Draft, draft-ietf- @@ -3571,32 +3897,31 @@ Internet-Draft PACT September 2026 aims-00>. [I-D.stone-vcap-ap2-binding] - Stone, B. E. N. S. S. T. O. N., "VCAP-AP2 Binding: - Verified Delivery Settlement for the Agent Payments - Protocol", Work in Progress, Internet-Draft, draft-stone- - vcap-ap2-binding-01, 4 September 2026, - . - - - - -Sharma Expires 20 March 2027 [Page 64] - -Internet-Draft PACT September 2026 - + Stone, B., "VCAP-AP2 Binding: Verified Delivery Settlement + for the Agent Payments Protocol", Work in Progress, + Internet-Draft, draft-stone-vcap-ap2-binding-01, 4 + September 2026, . [I-D.sahu-agent-action-receipts] - sahu, N., "Signed, Hash-Chained Action Receipts for AI + Sahu, N., "Signed, Hash-Chained Action Receipts for AI Agents", Work in Progress, Internet-Draft, draft-sahu- agent-action-receipts-00, 16 August 2026, . [I-D.mih-sato-agent-accountability-composition] - Mih, S., Sato, Schrock, I., Bu, S., and A. Sokolov, "Agent - Accountability: Composition and Conformance", Work in - Progress, Internet-Draft, draft-mih-sato-agent- + Mih, S., Sato, T., Schrock, I., Bu, S., and A. Sokolov, + "Agent Accountability: Composition and Conformance", Work + in Progress, Internet-Draft, draft-mih-sato-agent- + + + +Sharma Expires 20 March 2027 [Page 70] + +Internet-Draft PACT September 2026 + + accountability-composition-01, 16 August 2026, . @@ -3622,6 +3947,13 @@ Internet-Draft PACT September 2026 . + [I-D.laxsharma-pact-00] + Sharma, L., "PACT: A Contract Layer for Autonomous Agent + Commerce", Internet-Draft, draft-laxsharma-pact-00, + superseded, 27 July 2026, + . + [ASOKAN98] Asokan, N., Shoup, V., and M. Waidner, "Asynchronous Protocols for Optimistic Fair Exchange", Proceedings of the IEEE Symposium on Security and Privacy, 1998, @@ -3637,7 +3969,11 @@ Internet-Draft PACT September 2026 -Sharma Expires 20 March 2027 [Page 65] + + + + +Sharma Expires 20 March 2027 [Page 71] Internet-Draft PACT September 2026 @@ -3662,21 +3998,25 @@ Appendix A. An Example Terms Profile: bonded-restitution Section 1.4 has something to run against and the vectors in the reference repository have something to reproduce. It is the -01 revision's settlement content written as a schedule over the events - of Section 4.2, with the choices the -01 revision left open now made, - and it is offered as an example of the form a profile takes, not as a - recommendation of these terms. What the figures below mean between - the parties to a contract that names this profile is a question this - document does not answer and its author is not qualified to answer; a - profile meant for use needs an owner who is. + of Section 4.2, with the choices the -01 revision left open now made + and two of its own choices changed where the arithmetic or its text + required (Appendix A.5), and it is offered as an example of the form + a profile takes, not as a recommendation of these terms. What the + figures below mean between the parties to a contract that names this + profile is a question this document does not answer and its author is + not qualified to answer; a profile meant for use needs an owner who + is. Until such a profile exists, this one is also the only profile a + Facilitator can list, since terms_profiles must have an entry; that + is a fact about the present and not a rule of this document. A.1. Identity and Bundle Identifier: tag:laxsharma79@gmail.com,2026:pact:bonded-restitution. The bundle in the reference repository, under profiles/bonded- - restitution/, contains README.md (this text), parameters.schema.json - and vectors.json; profile_hash is the manifest digest over those - three files and Section 15 prints it. Problem types this profile - reports are under the prefix + restitution/, contains README.md (the prose of this appendix, in + Markdown), parameters.schema.json and vectors.json; profile_hash is + the manifest digest over those three files and Section 15 prints it. + Problem types this profile reports are under the prefix tag:laxsharma79@gmail.com,2026:pact:bonded-restitution:problem:. A.2. Parameters @@ -3684,20 +4024,23 @@ A.2. Parameters seller_bond: amount, required. What the Seller posts before performance. - verification_fund: amount, required. What the Buyer posts to pay - for checking. + verification_fund: amount, required. What the Seller posts to pay - cap: amount, required. The most that leaves the Seller's accounts - under this contract. - -Sharma Expires 20 March 2027 [Page 66] +Sharma Expires 20 March 2027 [Page 72] Internet-Draft PACT September 2026 + for checking; the -01 prose never said who posts it and its figure + drew it from the Seller, which this profile follows. + + cap: amount, required. The most that leaves the bond under this + contract; it bounds ranks 3 to 5 together, and what the bond holds + beyond it returns to the Seller. + restitution_basis: string, required. released or price. remainder_to: string, optional. buyer or sink; sink when absent. @@ -3710,7 +4053,15 @@ Internet-Draft PACT September 2026 closed. assurance: object, required. mode (certain, committed-sample or - open) and q_min (a number greater than zero and at most one). + open), q_min (a number greater than zero and at most one) and, + under committed-sample, sample_rate (a number greater than zero + and at most one: the declared fraction of deliveries verified; the + draw MUST derive from a seed the Buyer committed before the + Delivery was submitted, combined with the Delivery's digest; how + the seed is committed is outside the profile). + + This profile defines no Challenge deposit; a Facilitator that + advertises challenge_deposit does not do so under this profile. The -01 revision's four release modes map onto flow and principal_on as Appendix B shows. @@ -3730,14 +4081,31 @@ A.4. Admission B >= P * (1 - q) / q + E + + + + +Sharma Expires 20 March 2027 [Page 73] + +Internet-Draft PACT September 2026 + + and reports assurance-constraint-unsatisfied when it does not hold, or when assurance.mode is open alone. The inequality is the classical deterrence bound ([POLINSKY99]; [BELENKIY08] Theorem 1 for outsourced computation), with E the one term the -01 revision added: - value that moved before a Verdict cannot be recovered by the - schedule, so it raises what the Seller must post one for one. A - contract whose seller_bond or verification_fund exceeds cap is - reported as parameters-inconsistent. + principal that moves before any Verdict is outside what the + Verifier's check can withhold, so it raises what the Seller must post + one for one. The bound deters nonconformance against that check and + says nothing about what a later Challenge recovers; after a PASS is + overturned the restitution of the schedule is bounded by the bond and + the cap, whatever principal_on was. A contract whose seller_bond or + verification_fund exceeds cap is reported as parameters-inconsistent. + + The rule is falsified, and this profile with it, if the constraint + proves unworkable at the prices and verification costs real + deployments exhibit. That was the -01 revision's own failure + condition, restated here where the rule now lives. A.5. Schedule @@ -3747,14 +4115,7 @@ A.5. Schedule contract and the trace prefix; "released" is the sum of principal entries emitted so far. - - -Sharma Expires 20 March 2027 [Page 67] - -Internet-Draft PACT September 2026 - - - funded: buyer to escrow, P, lock; seller to bond, B, bond; buyer to + funded: buyer to escrow, P, lock; seller to bond, B, bond; seller to fund, verification_fund, fund. delivered: if principal_on is delivered: escrow to seller, the @@ -3770,46 +4131,56 @@ Internet-Draft PACT September 2026 principal. terminal, FINAL: escrow to seller, the escrow balance, principal; - bond to seller, the bond balance, return; fund to buyer, the fund + bond to seller, the bond balance, return; fund to seller, the fund balance, fund-return. terminal, ABANDONED: escrow to buyer, the escrow balance, reverse; - bond to seller, the bond balance, return; fund to buyer, the fund + bond to seller, the bond balance, return; fund to seller, the fund balance, fund-return. The -01 revision said the bond was slashed - "to the extent of" the basis here and never said by how much; with - the price reversed the Buyer's loss is zero under either basis, so - nothing is slashed. + "to the extent of" the basis here, and its Section 5.3 defined the + + + +Sharma Expires 20 March 2027 [Page 74] + +Internet-Draft PACT September 2026 + + + basis as an amount, the value already released or the full price, + without relating either to a loss; with the price reversed the + Buyer's loss is zero under either basis, so this profile slashes + nothing here, which under basis price is a departure. terminal, SETTLED: in five ranks, each drawing only what remains. (1) escrow to buyer, the escrow balance, reverse. (2) if challenge_upheld: fund to the Challenger whose Challenge the - standing Verdict answers, the lesser of that Challenge's costs and - the fund balance, costs. (3) bond to buyer, the lesser of the bond - balance, cap, and the Buyer's loss, restitution; the loss is - "released" under basis released and P minus the rank-1 entry under - basis price, which differ only when the price moved in part. (4) - if challenge_upheld: bond to that Challenger, the bond balance, - bounty. (5) bond to buyer or sink per remainder_to, the bond - balance, remainder. Then fund to buyer, the fund balance, fund- - return. + standing Verdict answers, the lesser of that Challenge's costs + when stated in the contract's currency (otherwise nothing) and the + fund balance, costs. (3) bond to buyer, the lesser of the bond + balance, the cap room and the Buyer's loss, restitution; the loss + is "released" under basis released and P minus the rank-1 entry + under basis price, which coincide under this schedule, since every + principal entry moves the whole escrow balance; the parameter is + kept for a profile that adds partial release. (4) if + challenge_upheld: bond to that Challenger, the lesser of the bond + balance and the cap room, bounty. (5) bond to buyer or sink per + remainder_to, the lesser of the bond balance and the cap room, + remainder. Then bond to seller, the bond balance, return, which + is what the cap kept; then fund to seller, the fund balance, fund- + return. The cap room at each rank is cap less what the entries so + far have moved out of the bond. Ranks 2 and 4 pay one Challenger, the one whose Challenge the - standing Verdict answers. A Challenge that was not answered by the + standing Verdict answers. The -01 revision required the reward to be + non-exclusive, paying every independent discoverer in full; one bond + cannot fund that for two discoverers, so this profile pays one and + records the departure here. A Challenge that was not answered by the standing Verdict, whether lapsed, rejected or superseded, receives nothing. Rank 4 gives the whole remaining bond, because the -01 revision forbade capping it at a fraction chosen for tidiness and fixed no figure; a profile owner who wants a different rule changes this line and the vectors with it. - - - - -Sharma Expires 20 March 2027 [Page 68] - -Internet-Draft PACT September 2026 - - A.6. Vectors With P 180.00, B 18.00, fund 0.50, cap 180.00, basis released, @@ -3818,8 +4189,18 @@ A.6. Vectors claiming costs of 1.20. Amounts are in USDC. Trace indexes count from zero. The lists below are what vectors.json carries for the two paths in the figures of this document; the repository's file also - carries the SETTLED-by-Verifier and ABANDONED paths and the price - basis. + carries the SETTLED-by-Verifier, ABANDONED, verdict-lapsed and + delivery-first paths, the price basis, and two admission vectors, one + refused and one admitted at the boundary of the constraint. + + + + + +Sharma Expires 20 March 2027 [Page 75] + +Internet-Draft PACT September 2026 + trace 0 accepted 1 funded 2 delivered 3 verdict PASS 4 window-opened 5 window-closed 6 children-final @@ -3828,10 +4209,10 @@ A.6. Vectors event from to amount code 1 buyer escrow 180.00 lock 1 seller bond 18.00 bond - 1 buyer fund 0.50 fund + 1 seller fund 0.50 fund 3 escrow seller 180.00 principal 7 bond seller 18.00 return - 7 fund buyer 0.50 fund-return + 7 fund seller 0.50 fund-return Figure 14: FINAL: the path of Figure 1 @@ -3843,12 +4224,12 @@ A.6. Vectors event from to amount code 1 buyer escrow 180.00 lock 1 seller bond 18.00 bond - 1 buyer fund 0.50 fund + 1 seller fund 0.50 fund 3 escrow seller 180.00 principal 8 fund challenger: 0.50 costs 8 bond buyer 18.00 restitution - Figure 15: SETTLED on an upheld Challenge: the path of Figure 5 + Figure 15: SETTLED on an upheld Challenge: the path of Figure 7 In the second vector rank 1 emits nothing because the escrow is empty, rank 2 pays the lesser of 1.20 and the fund's 0.50, rank 3 @@ -3857,25 +4238,26 @@ A.6. Vectors because nothing remains. Both lists satisfy closure: after the last entry the three internal accounts hold zero. +Appendix B. Changes from -01 + + This revision separates the protocol from the meaning of its terms. + The -01 revision, in its title, abstract, Section 1.2 and throughout, + made who owed whom the subject of the document; one reader on the + IETF dispatch list, Rich Salz, read it in September 2026 as a legal + framework with a protocol attached, and another, John C Klensin, + wrote that its framing was tied closely enough to legal terminology + that the IETF was the wrong place to evaluate it; both were right. + What follows is the list of what changed, with the wire consequences + first. -Sharma Expires 20 March 2027 [Page 69] +Sharma Expires 20 March 2027 [Page 76] Internet-Draft PACT September 2026 -Appendix B. Changes from -01 - - This revision separates the protocol from the meaning of its terms. - The -01 revision, in its title, abstract, Section 1.2 and throughout, - made who owed whom the subject of the document; two readers on the - IETF dispatch list observed in September 2026 that this placed it - outside what the IETF is placed to evaluate, and they were right. - What follows is the list of what changed, with the wire consequences - first. - * The pact version is 0.2 and every committed digest changed (Section 15). The -01 digests were computed by a canonicalizer that serialized the number one as 1.0, which [RFC8785] does not @@ -3914,14 +4296,6 @@ Appendix B. Changes from -01 (Section 4.2); RELEASING and PROPOSED are gone, AWAITING_CHILDREN is added. - - - -Sharma Expires 20 March 2027 [Page 70] - -Internet-Draft PACT September 2026 - - * delivery_hash covers the Delivery's signature; every digest covers the signature set (Section 2). Signature sets are sorted and ECDSA is low-S (Section 14.1). Merkle leaves cover signatures @@ -3931,6 +4305,15 @@ Internet-Draft PACT September 2026 revision treated it as a FAIL Verdict (Section 6). The Buyer countersignature sentence is withdrawn. + + + + +Sharma Expires 20 March 2027 [Page 77] + +Internet-Draft PACT September 2026 + + * A Verdict may carry challenge_hash; a Challenge may carry costs; a Seller-signed Challenge is refused (Section 7). @@ -3938,7 +4321,10 @@ Internet-Draft PACT September 2026 outcome supply, child-unresolved, a finite latest finality instant per contract and the rule L(child) before L(parent); the depth and cycle rules are withdrawn with the reason (Section 10). The -01 - Section 10.2 is one sentence in Section 10.3. + Section 10.2, which had liability cascade upward as recovery and + not downward as discharge, is withdrawn to the profile; + Section 10.3 says only that this document does not state what a + child's outcome means for its parent. * Section 3 is a data dictionary and a role table (Section 3); no sentence in it requires anything of a party. @@ -3956,13 +4342,50 @@ Internet-Draft PACT September 2026 * The experiment is restated over protocol observables (Section 1.4). + * A contract carries exactly one signature per party and no other; + the -01 revision accepted further signers (Section 14.2). + + * verification.arbiter is withdrawn; nothing read it. + + * The capability document gains issued_at and retrieval, and its + flows must list verdict-first (Section 7.1). The well-known URI + is registered provisionally, with the author as change controller. + + * A Verdict is accepted while one stands only in answer to a + Challenge (Section 4.2). + + * The -01 rule that confidential content MUST NOT declare open + assurance is the profile's now; Appendix A.4 refuses open + assurance alone. + + * signature-invalid and signature-missing are 400 and not 401, since + no HTTP authentication scheme is involved; V-26 names the + protected-header members a verifier rejects (Section 14.1). + + + + +Sharma Expires 20 March 2027 [Page 78] + +Internet-Draft PACT September 2026 + + + * Section 16 names the repository licence, Apache License 2.0; the + -01 said Revised BSD, which was wrong. + + * alg names are the fully specified ones of [RFC9864], Ed25519 for + an Ed25519 key; the polymorphic EdDSA identifier the -01 used is + refused. + Acknowledgements - Rich Salz and John C Klensin, on the IETF dispatch list in September - 2026, read the -01 revision as a document about who owes whom with a - protocol attached, and said so; this revision's split between records - and terms is the consequence, and the author is grateful for the - reading. The UTF-16 key-ordering vector that exposed a latent + On the IETF dispatch list in September 2026, Rich Salz read the -01 + revision as a legal framework with a protocol attached and said so, + and John C Klensin wrote that the framing of its terms was tied + closely enough to legal terminology that the IETF was the wrong place + to evaluate it. Both were right; this revision's split between + records and terms is the consequence, and the author is grateful for + the reading. The UTF-16 key-ordering vector that exposed a latent canonicalization defect in the reference validator, and the formulation of verifier independence as a relation the evaluator derives rather than a field the record declares, came from Tersign @@ -3970,14 +4393,6 @@ Acknowledgements verification tiers say how work is checked and never who checks it came from msaleme on the same thread. Rich Smith's A2A Settlement Extension was the clearest instance of the pattern the -01 revision - - - -Sharma Expires 20 March 2027 [Page 71] - -Internet-Draft PACT September 2026 - - corrected, and he engaged with the critique on a2aproject/A2A discussion 1576. @@ -4006,27 +4421,4 @@ Author's Address - - - - - - - - - - - - - - - - - - - - - - - -Sharma Expires 20 March 2027 [Page 72] +Sharma Expires 20 March 2027 [Page 79] diff --git a/draft/draft-laxsharma-pact-02.xml b/draft/draft-laxsharma-pact-02.xml index 5aacdb3..c2c5057 100644 --- a/draft/draft-laxsharma-pact-02.xml +++ b/draft/draft-laxsharma-pact-02.xml @@ -17,22 +17,13 @@ Merkle tree - Autonomous agents can already prove who they are, show whose - authority they act under, find one another, call one another, and pay. - What they cannot do with any existing specification is agree on a task + Autonomous agents can already prove who they are, show whose authority they act under, find and call one another, and pay. What no existing specification lets them do is agree on a task in a form a third party can check, deliver against it, have the delivery judged by someone other than the performer, and carry away a record of the outcome that a stranger can verify. This document specifies PACT, a set of signed JSON records that closes that gap. - PACT defines four things: a co-signed task contract whose digest - covers its signature set, so the commitment proves who agreed and not - only what was written; a Verdict record bound by digest to the Delivery - record it judges; a Facilitator-signed event trace and Outcome Record - for every contract, so what happened is recorded once, in one order, - by a party that is not the performer; and a Merkle commitment from a - parent contract's Outcome Record to the Outcome Records of its - subcontracts. + PACT defines four things: a co-signed task contract whose digest covers its signature set; a Verdict record bound by digest to the Delivery it judges; a Facilitator-signed event trace and Outcome Record for every contract, recorded once, in one order, by a party other than the performer; and a Merkle commitment from a parent's Outcome Record to its subcontracts' Outcome Records. Settlement terms are carried by reference to a profile defined outside this document. This document specifies no escrow, custody or @@ -58,14 +49,10 @@ to have that result judged by a third implementation against criteria fixed before the work began, and no record of the outcome that a fourth implementation can verify without trusting any of the first - three. Receipts record that an action occurred. Audit records - establish whether behaviour matched intent. Payment schemes move value - on the payer's instruction. None of them says what was agreed, what + three. Receipts record that an action occurred, audit records establish whether behaviour matched intent, and payment schemes move value on the payer's instruction. None of them says what was agreed, what was delivered, or whether the one met the other. - That gap is not an oversight in those documents; it is outside - their scope, and correctly so. It is the gap this document - addresses, and only that gap. + Those documents leave that gap on purpose, since it is outside their scope, and correctly so. It is the gap this document addresses, and only that gap.

    What This Document Specifies, and What It Does Not @@ -76,7 +63,7 @@ trace, signed by a Facilitator, from which one Outcome Record per contract is produced (, ); and a Merkle commitment from a parent's - Outcome Record to its children's (). + Outcome Record to its children's (). A contract names its settlement terms by reference: a profile identifier, a digest over the profile's bytes, and a parameter object @@ -84,7 +71,10 @@ terms mean, and everything about who holds or moves value under them, is the profile's to say. This document specifies the records, their digests, who signs each one, the order in which a Facilitator records - events, and a commitment across records. That is the whole of it. + events, and a commitment across records. That is the whole of it. A + contract carries a price and names a settlement binding, since a + task contract without them is not one; what happens to the price is + the profile's, and what the binding reports is the binding's. A deployment relies on other specifications, agreements or arrangements for: the meaning of the terms a contract names; agent @@ -96,9 +86,7 @@ do not settle. Carrying terms by reference is an old pattern in this series. - ACME carries a terms-of-service URL and - requires a client to assert agreement to it before an account is - created, without defining a single term. A certificate carries its + ACME carries a terms-of-service URL and, where a server chooses to require it, has the client assert agreement to those terms before an account is created, without defining a single term. A certificate carries its policy as an identifier whose rules live outside the IETF (, Section 4.2.1.4), and the framework for writing those rules says it does not aim to @@ -106,7 +94,7 @@ specified the messages of a trade and left the trade's terms to the parties. PACT follows that line. - Two mechanisms present in the -00 revision remain withdrawn: + Two mechanisms present in the -00 revision remain withdrawn: contract channels, and the sealed-bid award procedure. The reasons are recorded in and are not repeated. The change from -01 to this revision is listed in @@ -126,9 +114,7 @@ directly. carries Work Completion Records and an audit-verified settlement timing; binds verified commerce - settlement to the Agent Payments Protocol. Neither carries a co-signed - contract whose digest covers its signatures, and PACT is designed to - be usable alongside either. + settlement to the Agent Payments Protocol. This document binds to neither and is designed to be usable alongside either. Five bodies of IETF work touch the same records, and the relationship to each is stated here so that it is not left to the @@ -165,19 +151,14 @@ .
    WIMSE.
    gives workload and agent identity a home. PACT does not define an identity - format; a kid resolves as says, and - that section is written so that an identity system defined - elsewhere can be named without changing this document.
    + format; a kid resolves as says. An identity system defined elsewhere is used by naming its identifiers in one of the two forms that section resolves; a further form needs one resolution rule added there, and nothing else in this document changes.
    SATP.
    transfers a digital asset between two gateways with evidence a third party can - verify. An Outcome Record is not an asset transfer and does not - move one; it is a signed statement that certain records were - received in a certain order, and what any of that means for an + verify. An Outcome Record moves no asset. It is a signed statement that certain records were received in a certain order, and what any of that means for an asset is the terms profile's to say.
    - Verification evidence formats for hardware-attested tiers are - specified in and . + Verification evidence for hardware-attested tiers follows the architecture of and the EAT format of . Signed, hash-chained action receipts , composition of accountability records @@ -190,11 +171,9 @@
    The Experiment - This document is Experimental. The question it tests is stated - over protocol observables only. Given the same sequence of posted - records and the same clock readings, two independent Facilitator - implementations should produce the same event trace - (). Given the same trace and the same terms + This document is Experimental, and an individual submission with no formal standing in the standards process: no working group has adopted it and the IETF has not endorsed it. The question it tests is stated + over protocol observables only. Given the same sequence of posted records, the same clock readings and the same reports from the settlement binding, two independent Facilitator + implementations should produce the same event trace (). Given the same trace and the same terms profile, they should produce the same Outcome Record body (), byte for byte after canonicalization. The experiment succeeds if two independent Facilitators, serving @@ -222,8 +201,7 @@ JCS before hashing or signing. Implementations MUST order object keys by UTF-16 code unit as Section 3.2.3 requires. Sorting by Unicode code - point is a common substitution; it agrees with the required order - throughout the Basic Multilingual Plane and diverges above it. Numbers + point is a common substitution; it agrees with the required order until a key outside the Basic Multilingual Plane is compared with one whose first differing unit lies in U+E000 to U+FFFF, where the two orders disagree. Numbers MUST be serialized as Section 3.2.2.3 requires, which is how ECMAScript prints them: the number one is 1, whatever type held it, and never 1.0. @@ -239,8 +217,7 @@ canonical form of the whole object, including every signature member it carries. Every hash member in this document that names another object (vtc_hash, delivery_hash, challenge_hash, - the object member of a trace entry, and the leaves of - ) is that object's digest. A digest that + the object member of a trace entry) is that object's digest, and an element of the list D in is the 32 bytes that digest's hexadecimal encodes. A digest that excluded signatures would prove what was written and not who agreed to it; the -00 revision had that defect and the -01 revision fixed it for the contract only. This revision applies one construction @@ -259,9 +236,7 @@ recognise is inside the commitment and cannot be ignored safely. An implementation MUST reject an object whose pact version it does not implement, and MUST reject an object carrying a member this - document does not define for it, with one exception: the contents of - terms.parameters () are defined by the - named profile and this document reads none of them. Extension is by a + document does not define for it, with two exceptions: the contents of terms.parameters (), which the named profile defines and this document does not read; and the members of a Delivery's evidence, a Challenge's proof and a TaskSpec's constraints beyond those names, which the verification profile defines. Extension is by a new version, not by adding members. Time. Every timestamp is an RFC 3339 date-time @@ -269,16 +244,15 @@ Facilitator's clock governs every deadline and window in this document: the instant at which the Facilitator records an event is the instant that counts, that instant is what the trace carries, and - parties should allow for skew when acting near a boundary. + parties SHOULD allow for skew when acting near a boundary. says what that clock can and cannot prove. - Amounts. An amount is a decimal string with no exponent and a + Amounts. An amount is a decimal string with no sign, no exponent and a fractional part of two to eighteen digits; comparisons are exact and no rounding is implied. A currency is an asset identifier whose namespace is defined by the settlement binding named in - price.settlement, and need not be an ISO 4217 code. A network - is a ledger identifier in the form the same binding defines. This + price.settlement, and need not be an ISO 4217 code. A network is a ledger identifier in the form the same binding defines; the examples use chain identifiers. This document carries amounts; it does not say what any amount is for. Where a record produced under this document lists amounts, as terms_result does (), the meaning @@ -290,7 +264,7 @@ this document is made after that normalization.
    Terminology - Four words in this document have meanings elsewhere that are + Five words in this document have meanings elsewhere that are close enough to mislead, and are defined here once.
    Contract:
    Used in this document for a co-signed JSON @@ -306,10 +280,7 @@ hardware-attested tier, and are still different roles.
    Evidence:
    The evidence member of a Delivery is the set of artefacts a Verifier evaluates, produced by the Seller. - It is not Evidence in the sense of . The - member name is kept from -01 because renaming it would change every - committed digest for no gain in clarity that this note does not - provide.
    + It is not Evidence in the sense of . The member name is kept because it is the ordinary word for what the member holds; the RATS term names a role in an attestation architecture, and this note is the disambiguation.
    Facilitator:
    The party that runs the state machine of for a contract: it accepts or refuses the records posted to it, records events in one order on its own clock, @@ -328,8 +299,7 @@ that carries it, with its type, whether it is required in that object, and what it commits to. It is a dictionary and not a rulebook: the rule that a record omitting a required member, or carrying one this document - does not define for it, does not conform is stated once in - ; the rules a Facilitator applies when it + does not define for it, does not conform is stated once, in ; the rules a Facilitator applies when it accepts or refuses a record are in and in the section that defines the record. No sentence in this section requires anything of any party. Where a member's meaning is the named @@ -376,8 +346,7 @@
    task:
    object, required. spec_hash (digest, required) commits to a TaskSpec (); spec_uri (URI, optional) says where its bytes may be - fetched; deadline (timestamp, required) is the instant - after which the deadline-passed event may be recorded + fetched; deadline (timestamp, required) is the instant at or after which the deadline-passed event may be recorded ().
    price:
    object, required. amount (amount, required), currency (string, required), @@ -389,10 +358,9 @@ (string, required), profile (string or URI, required; ), criteria_hash (digest, required; the manifest digest of the acceptance instrument per - ), max_verdict_seconds (integer, - required; the longest interval after delivered within + ), max_verdict_seconds (integer, required, greater than zero; the longest interval after delivered within which a first Verdict is recorded before verdict-lapsed - may be), arbiter (URI, optional). Commits to how a + may be). Commits to how a Delivery is judged and by what.
    flow:
    string, required. One of verdict-first, delivery-first, no-window @@ -408,8 +376,7 @@ profile says.
    challenge:
    object, required. window_seconds (integer, required, greater than zero) is the - duration of the challenge window; max_dispute_seconds - (integer, required) is the longest interval after a + duration of the challenge window; max_dispute_seconds (integer, required, greater than zero) is the longest interval after a challenge event within which a Verdict on that Challenge is recorded before dispute-lapsed may be.
    parent:
    object, optional; present only in a @@ -459,13 +426,10 @@
    work_uri:
    URI, optional. Where the bytes may be fetched, subject to .
    input_hash:
    digest, required for tiers whose - fraud proof re-executes. Commits to the production input actually + proof of nonconformance re-executes. Commits to the production input actually consumed.
    evidence:
    object, required. Members profiled by - verification.tier and verification.profile; for - the acceptance profile, profile, - instrument_hash, results_hash and - results_uri. Conformance to the profile is a validity + verification.tier and verification.profile; for the acceptance profile, profile, instrument_hash and results_hash (required) and results_uri (optional). Conformance to the profile is a validity condition of the Delivery, not a judgement on the work.
    @@ -485,8 +449,7 @@ digest, required. The verification profile applied and the digest of the instrument actually run, which equals the contract's criteria_hash. -
    results_hash:
    digest, required. Commits to the - Verifier's own results.
    +
    results_hash:
    digest, required. The digest of the results document the verification profile defines; for acceptance, the bytes of the results file the instrument wrote.
    evaluated_at:
    timestamp, required. The Verifier's own clock; informational, since the trace carries the Facilitator's.
    @@ -501,13 +464,9 @@ digest, required. Identify the contract and commit to the Delivery challenged.
    proof:
    object, required. Members profiled by - verification.profile; for the acceptance profile, - profile, instrument_hash, results_hash, - results_uri and failing_checks (array of - strings).
    + verification.profile; for the acceptance profile, profile, instrument_hash and results_hash (required), results_uri and failing_checks (array of strings; optional).
    costs:
    object, optional. amount and - currency: a figure the Challenger asserts for producing the - proof. This document records it in the trace and reads it for + currency: an amount the Challenger states. This document records it in the trace and reads it for nothing; its meaning is the named terms profile's.
    @@ -515,7 +474,7 @@
    Contract Status Members Carried in the Contract Status (), media type application/vnd.pact.status+json, the Facilitator's - signed response to every accepted request. + signed response to every accepted POST.
    vtc_id, vtc_hash:
    string and digest, required.
    @@ -524,8 +483,7 @@
    trace:
    array of objects, required. The event trace so far, in the order recorded (). Each entry carries event (string, required), at - (timestamp, required), object (digest, required where the - event was caused by a posted record), and the event-specific + (timestamp, required), object (digest, required where the event was caused by a posted record, and on dispute-lapsed, where it names the Challenge that lapsed), and the event-specific members listed in .
    issued_at:
    timestamp, required. When this status was signed.
    @@ -552,8 +510,7 @@
    terms_result:
    object, required (). profile and profile_hash (copied from the contract), currency - (string), and transfers (array of objects), each with - from (string), to (string), amount + (string), and transfers (array of objects), each with event (integer, the zero-based index of the trace entry the transfer follows), from (string), to (string), amount (amount) and code (string). The entries are the named profile's output for the trace; this document defines their form and two arithmetic invariants over them, and nothing about their @@ -571,22 +528,22 @@
    facilitator:
    URI, required. The identifier that appears in parties.facilitator.
    +
    issued_at:
    timestamp, required. When the + document was signed. Nothing in a document survives its Facilitator withdrawing a profile; says when to fetch it again.
    settlement_bindings:
    array of objects, - required. Each with id (URI), networks and - assets (arrays of strings).
    -
    flows:
    array of strings, required. The flows - of the Facilitator implements.
    -
    verification_profiles:
    array of strings, - required.
    + required. Each with id (URI), networks and assets (arrays of strings), all required.
    +
    flows:
    array of strings, required. The flows of the Facilitator implements; requires verdict-first among them.
    +
    verification_profiles:
    array of strings, required. A contract naming a profile not listed is refused ().
    terms_profiles:
    array of objects, required, with at least one entry. Each with id (URI) and profile_hash (digest): the terms profiles, at the revisions named, whose schedules this Facilitator evaluates.
    -
    max_contract_value:
    object, optional. - amount and currency.
    +
    max_contract_value:
    object, optional. amount and currency; a contract whose price, stated in the same currency, exceeds it is refused, as is one whose price is stated in another currency ().
    challenge_deposit:
    object, optional. amount and currency; see .
    +
    retrieval:
    string, optional. parties, + the default of , or open.
    endpoints:
    object, required. Maps each endpoint name in to an absolute URI.
    @@ -603,11 +560,11 @@ Buyerparties.buyer - the contract; a child registration - () + the contract Contract Status, Outcome Record Sellerparties.seller - the contract; the Delivery + the contract; the Delivery; as the Buyer of a child, + that child's contract () Contract Status, Outcome Record Facilitatorparties.facilitator, parent.facilitator, the capability document @@ -624,16 +581,12 @@ One identifier may play more than one role across contracts, and - says which combinations within one - contract a Facilitator refuses. + and say which combinations within one contract a Facilitator refuses.
    Protocol Overview - A contract passes through four phases. Propose establishes the - record. Agree co-signs it and a Facilitator accepts it. Complete - produces a Delivery and a Verdict on it. Record produces an Outcome - Record. Every step after Agree is an event the Facilitator records on + A contract passes through four phases: Propose establishes the record, Agree co-signs it and a Facilitator accepts it, Complete produces a Delivery and a Verdict on it, and Record produces an Outcome Record. Every step after Agree is an event the Facilitator records on its own clock, in one order, and the sequence of those events is the contract's trace. The trace is the protocol's central object: the state machine is defined over it, every response a Facilitator gives carries @@ -666,7 +619,7 @@ ]]> - Every accepted request is answered with a Contract Status + Every accepted POST is answered with a Contract Status (), a Facilitator-signed object carrying the state and the trace so far. Nothing in the figure moves value, and no arrow in it is named for a movement of value. What a terms profile does @@ -702,17 +655,13 @@ The figure omits three arrows that the table carries: a FAIL Verdict recorded in DELIVERED or in WINDOW_OPEN also leads to - AWAITING_CHILDREN; under the no-window flow DELIVERED leads - there directly; and a Verdict that is late (verdict-lapsed) + AWAITING_CHILDREN; under the no-window flow delivered leads there directly; and a Verdict that is late (verdict-lapsed) opens the window without one. FINAL, SETTLED and ABANDONED are - terminal and each produces exactly one Outcome Record. The -01 - revision named one of these states for a movement of value; no state - here is. + terminal and each produces exactly one Outcome Record. The -01 revision had a state, RELEASING, named for a movement of value; it is gone. FUNDED remains, named for the event the settlement binding reports (), and nothing here says what that report means. The state named PROPOSED in earlier revisions is gone. Between the parties' signatures and the Facilitator's acceptance a contract exists - only on the parties' side, so no Facilitator could observe that state - and the reference implementation never reported it. + only on the parties' side, so no Facilitator could observe that state, and the -01 reference implementation never reported it, although the -01 Section 12.1 example printed it in a 201 response.
    Events @@ -729,64 +678,54 @@ Events: the state each is recorded in, the state that follows, and what the entry carries - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + +
    EventRecorded in; thenMembers and condition
    EventRecorded in; then. Members and condition
    acceptednone; then ACCEPTEDobject is vtc_hash. The contract passed .
    fundedACCEPTED; then FUNDEDref (string, optional, in the form the settlement binding defines). Recorded when every account the named terms profile requires shows finality on the settlement binding named in price.settlement; how a Facilitator observes that is the binding's to say, and this is the only sentence in this document that mentions an account.
    deadline-passedACCEPTED or FUNDED; then AWAITING_CHILDRENtask.deadline has passed with no delivered entry.
    deliveredFUNDED; then DELIVEREDobject is the Delivery's digest. The Delivery passed .
    window-openedDELIVERED; then WINDOW_OPENUnder delivery-first, immediately after delivered; under verdict-first, immediately after a PASS verdict or after verdict-lapsed. closes_at (timestamp, required) is at plus challenge.window_seconds.
    verdictDELIVERED, WINDOW_OPEN or DISPUTED; then see the conditionobject is the Verdict's digest; signer (the kid of its signature); outcome (PASS or FAIL); answers (digest of the Challenge, when the Verdict carries challenge_hash); supersedes (digest of the Verdict it replaces, when one stood). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending.
    verdict-lapsedDELIVERED; then WINDOW_OPENUnder verdict-first, verification.max_verdict_seconds have passed since delivered with no verdict. window-opened follows.
    challengeWINDOW_OPEN or DISPUTED; then DISPUTEDobject is the Challenge's digest; signer (the kid of its signature); costs copied from the Challenge when present. The Challenge passed before closes_at.
    dispute-lapsedDISPUTED; then WINDOW_OPENobject is the Challenge's digest. challenge.max_dispute_seconds have passed since that challenge entry with no Verdict answering it. Leads to WINDOW_OPEN once no Challenge is pending; the earlier Verdict, if any, stands.
    window-closedWINDOW_OPEN; then AWAITING_CHILDRENcloses_at has passed and no Challenge is pending. The window is never extended: a dispute that outlasts it delays this entry and does not move closes_at.
    child-registeredany non-terminal; unchangedobject is the child contract's digest; facilitator (URI). .
    child-finalany non-terminal; unchangedobject is the child's Outcome Record digest; child (the child contract's digest).
    child-unresolvedany non-terminal; unchangedchild (the child contract's digest). The child's latest finality instant () has passed and no Outcome Record for it is held.
    children-finalAWAITING_CHILDREN; then terminal followsEvery registered child has a child-final or child-unresolved entry. A contract with no registered children records this entry on entering AWAITING_CHILDREN.
    terminalAWAITING_CHILDREN; then FINAL, SETTLED or ABANDONEDstate (the terminal state) and challenge_upheld (boolean). ABANDONED where deadline-passed was recorded; SETTLED where the standing Verdict is FAIL, with challenge_upheld true when that Verdict answers a Challenge; FINAL otherwise.
    acceptednone; then ACCEPTED. object is vtc_hash. The contract passed .
    fundedACCEPTED; then FUNDED. ref (string, optional, in the form the settlement binding defines). Recorded when the settlement binding named in price.settlement reports that whatever the named terms profile requires before work starts is in place; how a Facilitator observes that is the binding's to say. Where the profile requires nothing, funded follows accepted in the same operation.
    deadline-passedACCEPTED or FUNDED; then AWAITING_CHILDREN. task.deadline has passed with no delivered entry.
    deliveredFUNDED; then DELIVERED, or under no-window AWAITING_CHILDREN directly. object is the Delivery's digest. The Delivery passed .
    window-openedDELIVERED; then WINDOW_OPEN. Recorded in the same operation as the entry it follows, with the same at: under delivery-first the delivered entry; under verdict-first a PASS verdict or verdict-lapsed. closes_at (timestamp, required) is at plus challenge.window_seconds.
    verdictDELIVERED (verdict-first, no Verdict standing), WINDOW_OPEN (delivery-first, no Verdict standing) or DISPUTED (answering a pending Challenge); then as the condition says. object is the Verdict's digest; signer (the kid of its signature); outcome (PASS or FAIL); answers (digest of the Challenge, present exactly when the Verdict carries challenge_hash); supersedes (digest of the Verdict that stood, present exactly when one did). Then: FAIL leads to AWAITING_CHILDREN; PASS in DELIVERED leads to window-opened; PASS in WINDOW_OPEN changes the state of nothing; PASS in DISPUTED leads to WINDOW_OPEN once no Challenge is pending.
    verdict-lapsedDELIVERED; unchanged. Under verdict-first, verification.max_verdict_seconds have passed since delivered with no verdict. window-opened follows in the same operation.
    challengeWINDOW_OPEN or DISPUTED; then DISPUTED. object is the Challenge's digest; signer (the kid of its signature); costs copied from the Challenge when present. The Challenge passed before closes_at.
    dispute-lapsedDISPUTED; then WINDOW_OPEN. object is the Challenge's digest. challenge.max_dispute_seconds have passed since that challenge entry with no Verdict answering it. Leads to WINDOW_OPEN once no Challenge is pending; the earlier Verdict, if any, stands.
    window-closedWINDOW_OPEN; then AWAITING_CHILDREN. closes_at has passed and no Challenge is pending. The window is never extended: a dispute that outlasts it delays this entry and does not move closes_at.
    child-registeredany non-terminal; unchanged. object is the child contract's digest; facilitator (URI). .
    child-finalany non-terminal; unchanged. object is the child's Outcome Record digest; child (the child contract's digest).
    child-unresolvedany non-terminal; unchanged. child (the child contract's digest). The child's latest finality instant () has passed and no Outcome Record for it is held.
    children-finalAWAITING_CHILDREN; then terminal follows. Every registered child has a child-final or child-unresolved entry. A contract with no registered children records this entry on entering AWAITING_CHILDREN.
    terminalAWAITING_CHILDREN; then FINAL, SETTLED or ABANDONED. state (the terminal state) and challenge_upheld (boolean). ABANDONED where deadline-passed was recorded; SETTLED where the standing Verdict is FAIL, with challenge_upheld true when that Verdict answers a Challenge; FINAL otherwise.
    The standing Verdict is the last verdict entry in the trace that no later entry supersedes. A Challenge is pending from its - challenge entry until a verdict entry answers it or - a dispute-lapsed entry names it. + challenge entry until a verdict entry answers it, a dispute-lapsed entry names it, or a terminal entry is recorded. Every instant in the table is read from the Facilitator's clock, and an entry conditioned on an instant having passed is recorded at - the first opportunity after it, which need not be that instant. Two - Facilitators given the same posted records with the same clock - readings record the same trace; that is the determinism the + the first opportunity after it, which need not be that instant. Two Facilitators given the same posted records, the same clock readings and the same reports from the settlement binding record the same trace; that is the determinism the experiment in tests, and the reason - every condition above is stated over the trace and the clock and - nothing else. + every condition above is stated over the trace, the clock and the binding's report, and nothing else. + + An instant has passed when the Facilitator's clock reads that + instant or a later one, and a record received when the clock reads an + instant has arrived at it, so a Challenge received when the clock + reads closes_at is after the window. Before acting on a posted + record a Facilitator MUST first record every clock-driven entry that + is due, so that the record is judged in the state the clock produced; + entries due at the same instant are recorded in the order of the + table. The at of an entry MUST be no earlier than that of + the entry before it, and a Status's issued_at MUST be no + earlier than the at of its last entry.
    The Verifiable Task Contract A VTC is a JSON object, media type application/vnd.pact.contract+json, with the members in - . A VTC is valid only if every required member - is present, the parties are distinct, and both the Buyer and the Seller + . A VTC is valid only if every required member is present, the Buyer and the Seller are distinct after normalization (), any named Verifier satisfies , and both the Buyer and the Seller have contributed exactly one signature that verifies against a key bound to its identifier (). The Facilitator and any Verifier do not sign the VTC; their assent is expressed by acting on it, @@ -799,60 +738,71 @@ replayable against any facilitator, chain or token contract.
    - A Verifiable Task Contract, signatures abbreviated + A Verifiable Task Contract, signatures and parameters abbreviated +]]>
    - Digests are elided here; the reference repository's values are in - . The parameters object is shown - elided on purpose: nothing in this document depends on what is in - it. + The values are the reference repository's + (), with the signatures abbreviated. The + parameters object is shown elided on purpose: nothing in this + document depends on what is in it.
    Hash Commitments and Content Conveyance Every URI carried inside hash-committed content MUST be accompanied @@ -860,7 +810,7 @@ committed harness_uri as a string while leaving the bytes at that URI uncommitted, which permitted a Buyer to substitute the acceptance instrument after signature, run the substituted - instrument, and submit the failure as a valid fraud proof. The -01 + instrument, and submit the failure as a valid proof of nonconformance. The -01 revision stated the rule and its own reference TaskSpec broke it for three of four URIs; this revision's example carries all four sibling hashes, and the validator checks each. @@ -868,7 +818,7 @@ single octet stream, the commitment MUST be computed as SHA-256(JCS(M)) where M is an object mapping each file's path, relative to the bundle root and expressed with "/" separators, - to SHA-256 of its bytes, over every file in the bundle. A + to the digest of its bytes in the string form of , over every file in the bundle. A file whose name, or any directory on whose path, begins with a dot is not part of a bundle. A manifest of per-file digests is specified rather than an archive digest because archive formats carry ordering, timestamp and permission metadata that is not stable across producers. The same @@ -888,7 +838,7 @@ tiers, a proof statement with its verifying key for proving tiers, or rubric_uri and rubric_hash for judgment tiers. An empty acceptance object MUST be rejected. The -00 revision's - schema permitted one, which made every fraud proof impossible. + schema permitted one, which made every proof of nonconformance impossible. Thresholds MUST be stated so that they cannot be satisfied by returning almost nothing. A threshold expressed only as a rate over @@ -920,11 +870,7 @@ A Facilitator MUST refuse a contract whose terms.profile and terms.profile_hash do not match an entry in the terms_profiles array of its own capability document - (), so that no party signs terms the - Facilitator will not evaluate, and MUST refuse a contract whose - parameters do not validate against the named profile's - parameters.schema.json. It reads parameters for no - other purpose. The rule of that an + (; terms-unsupported), so that no party signs terms the Facilitator will not evaluate, and MUST refuse a contract whose parameters do not validate against the named profile's parameters.schema.json (terms-parameters-invalid). This document reads parameters for no other purpose; the named profile's schedule and admission rule read them as the profile's. The rule of that an undefined member is rejected does not apply inside parameters; the profile's schema governs there. @@ -937,9 +883,10 @@ profile author would rather not think about: a lapsed dispute, an unresolved child, a contract abandoned before it was funded. Deterministic means the result depends on the contract, the trace and - nothing else, so that any party holding those can recompute it. The - prose also names the accounts the schedule uses and how each one's - opening amount is computed from the contract. This document does not + nothing else, so that any party holding those can recompute it. The prose also names the accounts the schedule moves value between, and says which of them are internal, opened empty and required to close empty, and which are external. A profile MAY also state + an admission rule: a condition on the contract, evaluated once at + accepted, whose failure is reported with a problem type from + the profile's namespace (). This document does not register profiles and defines none normatively; carries one for the experiment. @@ -993,22 +940,28 @@ { "pact": "0.2", "type": "Delivery", - "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ - 5fbbd4ebace4fb980f1c2", - "work_hash": "sha256:9c1f...", - "work_uri": "https://cdn.dataforge.example/o/9c1f", - "input_hash": "sha256:41ab...", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff49493\ + 04c2c855ca1d746ba6459", + "work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee2\ + 80a3170b6b9a5037036ef0", + "work_uri": "https://cdn.dataforge.example/o/a26d", + "input_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c9\ + 75a1d1ce519d582306338b1", "evidence": { - "profile": "acceptance", - "instrument_hash":"sha256:0bdde1ab6b081d2b4bda580c5393756ae95\ - c10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:7e02...", - "results_uri": "https://cdn.dataforge.example/o/7e02" + "profile": "acceptance", + "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\ + 5c10b8351c9c55eb9316416265fc1b", + "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf84\ + 8ebaca540214b4f6d2165688a51", + "results_uri": "https://cdn.dataforge.example/o/28d3" }, - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } } - ]]> +]]> The -01 revision said that a Buyer countersignature over the @@ -1023,8 +976,7 @@
    Flows The flow member selects one of three shapes for the state - machine of . A conformant Facilitator MUST - implement verdict-first; the others are OPTIONAL, and a + machine of . A conformant Facilitator MUST implement verdict-first and MUST list it in the flows member of its capability document (); the others are OPTIONAL, and a Facilitator MUST refuse a contract naming a flow it does not advertise (flow-unsupported).
    @@ -1033,18 +985,15 @@ verdict-lapsed; a FAIL Verdict ends the contract without a window.
    delivery-first:
    The window opens at - delivered. A Verdict MAY be recorded inside the window - without a Challenge; a FAIL ends the contract, a PASS changes - nothing.
    + delivered. One Verdict MAY be recorded inside the window without a Challenge, and a second only in answer to one; a FAIL ends the contract, a PASS changes nothing.
    no-window:
    No window opens and no Verdict is accepted; delivered is followed by the terminal path.
    - The -01 revision had four release modes, named for when value - moved. Two of them, on-window and optimistic, - produce the same trace and differed only in which event a profile - acts on, which is a profile parameter and not a protocol matter. The - mapping is in . + The -01 revision had four release modes. Two of them produce the + same trace and differed only in which event a profile acts on, which + is a profile parameter and not a protocol matter. The mapping is in + . The window opens at the instant of the window-opened entry and closes at that instant plus challenge.window_seconds, @@ -1068,18 +1017,22 @@ { "pact": "0.2", "type": "Verdict", - "vtc_id": "vtc_7f3a91", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ - f8aaaf2f186702504fba242ffb", - "outcome": "PASS", - "profile": "acceptance", + "vtc_id": "vtc_9f2c11", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45\ + 370cb3023331bc8bfbd924fcbe", + "outcome": "PASS", + "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c\ 10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:7e02...", - "evaluated_at": "2026-11-10T09:14:22Z", - "signature": { "protected": "...", "signature": "..." } + "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf848e\ + baca540214b4f6d2165688a51", + "evaluated_at": "2026-11-10T09:14:22Z", + "signature": { + "protected": "...", + "signature": "..." + } } - ]]> +]]> The Verifier is the party identified by the kid of the @@ -1092,13 +1045,11 @@ (no-recorded-delivery); one whose delivery_hash does not match that entry, or whose profile or instrument_hash does not match the contract - (verdict-nonconformant); one received in a state the table - in does not list for it, or under the + (verdict-nonconformant); one received in a state, or under conditions, that the table in does not list for it, or under the no-window flow (wrong-state); and one carrying challenge_hash that names no pending Challenge, or omitting it while the contract is DISPUTED (verdict-nonconformant). - A Verdict that answers a Challenge supersedes the Verdict that stood - before it, and both stay in the trace. + A Verdict recorded while one stands supersedes it, and both stay in the trace; since a Verdict is accepted while one stands only in DISPUTED, only a Verdict that answers a Challenge ever supersedes. A Verdict commits to the instrument it ran and to the results it produced. Without instrument_hash a Verifier could run @@ -1119,8 +1070,7 @@
    Challenges A Challenge is a JSON object, media type application/vnd.pact.challenge+json, with the members in - , by which a party submits a fraud - proof inside the window. A Facilitator MUST refuse a Challenge + , by which a party submits a proof of nonconformance (what optimistic systems call a fraud proof) inside the window. A Facilitator MUST refuse a Challenge received when the contract is not in WINDOW_OPEN or DISPUTED, or after closes_at (challenge-window-closed); one whose delivery_hash does not match the delivered @@ -1129,21 +1079,15 @@ one whose signer it cannot resolve (signature-invalid); and one signed by the contract's Seller (unexpected-signer), since a performer's statement against its own Delivery is not a - fraud proof and the -01 revision left the case open. A Facilitator + proof of nonconformance and the -01 revision left the case open. A Facilitator MUST NOT refuse a Challenge on the ground that its signer is the contract's Buyer. A Challenge that is accepted is evaluated by a party satisfying - , whose finding is a Verdict carrying - challenge_hash; the Challenger's own assertion is not a - finding. The Challenger is the party identified by the kid of + , whose finding is a Verdict carrying challenge_hash; the Challenger's own assertion is not a finding, unless the Challenger is the verifier the contract names, whose Verdict is the finding by definition. A named Verifier that finds its own PASS wrong posts a Challenge and answers it. The Challenger is the party identified by the kid of the Challenge's signature. - A Facilitator MAY require that a Challenge be accompanied by a - deposit in the amount its capability document advertises as - challenge_deposit. How a deposit is posted is the settlement - binding's, what becomes of it is the terms profile's, and this - document says nothing further about it. + A capability document MAY advertise challenge_deposit. Whether anything must accompany a Challenge, how it is posted and what becomes of it are the terms profile's and the settlement binding's to say; this document carries the member and reads it for no purpose. discusses what a deposit does and does not prevent.
    @@ -1154,21 +1098,28 @@ { "pact": "0.2", "type": "Challenge", - "vtc_id": "vtc_7f3a91", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2\ - f8aaaf2f186702504fba242ffb", + "vtc_id": "vtc_9f2c11", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45\ + 370cb3023331bc8bfbd924fcbe", "proof": { - "profile": "acceptance", + "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae9\ 5c10b8351c9c55eb9316416265fc1b", - "results_hash": "sha256:a91e...", - "results_uri": "https://watch.example/o/a91e", - "failing_checks": ["schema_valid_rate", "row_count_min"] + "results_hash": "sha256:7f05a60223e24ca9393e0c241d0448cb5f39e\ + 5edb71c117fb7d3d593d5861b40", + "results_uri": "https://watch.example/o/a91e", + "failing_checks": ["schema_valid_rate", "row_count_min"] }, - "costs": { "amount": "1.20", "currency": "USDC" }, - "signature": { "protected": "...", "signature": "..." } + "costs": { + "amount": "1.20", + "currency": "USDC" + }, + "signature": { + "protected": "...", + "signature": "..." + } } - ]]> +]]>
    @@ -1214,7 +1165,7 @@
    Facilitator Capability Discovery Before a Buyer and Seller can co-sign a VTC they must agree on a Facilitator and know what it implements. This document registers one - well-known URI for that purpose, per . + well-known URI for that purpose, per . A client SHOULD fetch the document again before it proposes, since nothing in it survives the Facilitator withdrawing a profile. This is deliberately narrower than agent discovery, which is the subject of separate work and is not restated here. What is discovered @@ -1223,8 +1174,7 @@ A Facilitator SHOULD publish a JSON document, media type application/vnd.pact.facilitator+json, with the members in - , at the path - /.well-known/pact-facilitator of its origin. The document MUST + , at the path /.well-known/pact-facilitator of its origin: for an https: identifier that origin, and for a did:web identifier https:// followed by the host the method encodes. The document MUST be served over HTTPS. It MUST be signed, and the signature MUST verify against a key bound to the identifier in facilitator. An unsigned capability document is not usable for contract formation, @@ -1240,39 +1190,45 @@ "pact": "0.2", "type": "FacilitatorCapabilities", "facilitator": "did:web:settle.example", + "issued_at": "2026-11-01T09:00:00Z", "settlement_bindings": [ - { "id": "https://settle.example/bindings/ledger-1", + { + "id": "https://settle.example/bindings/ledger-1", "networks": ["eip155:8453"], - "assets": ["USDC"] } + "assets": ["USDC"] + } ], - "flows": ["verdict-first", "delivery-first"], - "verification_profiles": ["acceptance", "bisection"], + "flows": ["verdict-first", "delivery-first"], + "verification_profiles": ["acceptance"], "terms_profiles": [ - { "id": - "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f\ - 5fc1147743326dabd45bda546dc62" } + { + "id": "tag:laxsharma79@gmail.com,2026:pact:bonded-restituti\ + on", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124c\ + cf6956e835df2653b99d71d363a68" + } ], - "max_contract_value": { "amount": "50000.00", - "currency": "USDC" }, + "max_contract_value": { + "amount": "50000.00", + "currency": "USDC" + }, "endpoints": { - "contract": "https://settle.example/pact/v2/contracts", - "delivery": "https://settle.example/pact/v2/deliveries", - "verdict": "https://settle.example/pact/v2/verdicts", + "contract": "https://settle.example/pact/v2/contracts", + "delivery": "https://settle.example/pact/v2/deliveries", + "verdict": "https://settle.example/pact/v2/verdicts", "challenge": "https://settle.example/pact/v2/challenges", - "outcome": "https://settle.example/pact/v2/outcomes" + "outcome": "https://settle.example/pact/v2/outcomes" }, - "signature": { "protected": "...", "signature": "..." } + "signature": { + "protected": "...", + "signature": "..." + } } - ]]> +]]> A client MUST NOT infer any capability from the absence of a member. - A Facilitator that does not publish a capability document can still be - named in a VTC by prior arrangement; discovery is a convenience, not a - precondition. A Facilitator MUST NOT list a terms profile whose - vectors () its own implementation does not - reproduce. + A client may hold a Facilitator\'s capability document by prior arrangement rather than fetch it from the well-known path; the path is a convenience, the document is not, since a Facilitator refuses what its document does not advertise (, ). A Facilitator lists only the terms profiles whose vectors () its own implementation reproduces.
    Verification Profiles @@ -1289,15 +1245,8 @@ profile largely does. Consider one task, a bulk data transformation, under two profiles at - the same nominal tier. Re-executing the whole computation and comparing - outputs costs approximately what performing it cost. Running a committed - acceptance instrument against the delivered artifact costs a small - fraction of a percent. Those two differ by more than two orders of - magnitude in what checking costs relative to the price. A terms profile - may make that ratio matter; this document requires only that a - verification profile state an order-of-magnitude estimate of its cost - relative to the work, since a figure nobody can estimate is a figure - nobody can use. + the same nominal tier. Re-executing the whole computation and comparing outputs costs about what performing it cost; running a committed acceptance instrument against the delivered artifact costs a small fraction of that. Those are the author's estimates, not measurements (the measurements mentions are of the protocol, not the work), and the two can differ by orders of magnitude in what checking costs relative to the price. A terms profile + may make that ratio matter; this document requires of a verification profile the five statements listed after the profiles below, one of which is an order-of-magnitude estimate of its cost relative to the work, since a figure nobody can estimate is a figure nobody can use. Implementations SHOULD select the cheapest profile that detects the failures they actually care about, rather than the strongest-sounding @@ -1306,10 +1255,9 @@
    acceptance:
    Run the instrument committed by - criteria_hash against the Delivery. The fraud proof is a + criteria_hash against the Delivery. The proof of nonconformance is a failing evaluation. Deterministic by construction, since the - instrument is fixed before work begins. Cost: a small fraction of a - percent of the work for a data transformation.
    + instrument is fixed before work begins. Cost: a small fraction of the work for a data transformation, by estimate.
    bisection:
    Interactive narrowing to a single disputed step, which is then checked directly. Cost grows logarithmically in the size of the computation rather than @@ -1320,11 +1268,18 @@ approximately the work.
    + A verification profile usable with this document states five + things: what artefact is evaluated and against what; what constitutes a + valid proof of nonconformance, including whether absence of evidence + is one; that its proof can be evaluated by a party other than the + Seller; its cost relative to the work, to order of magnitude; and + whether it is deterministic and with what tolerance + (). acceptance states these below; the other two are sketches that a full profile document completes; a profile defined elsewhere states them in its own document. +
    Verifier Independence and Identifier Normalization Independence is a relation between the party that signs a Verdict - and the parties to the contract. It MUST be derived by the evaluator - and MUST NOT be satisfied by a field in which a record declares - itself independent. A Facilitator MUST refuse a Verdict whose signer + and the parties to the contract. It MUST be derived by the Facilitator + and MUST NOT be satisfied by a field in which a record declares itself independent. Rules of this kind are stated for evaluation after the fact in ; this document binds them at contract formation. A Facilitator MUST refuse a Verdict whose signer is, after normalization, the contract's Buyer, Seller or Facilitator, and MUST refuse a contract whose parties.verifier is any of those three (verifier-not-independent). The last case is the @@ -1358,7 +1313,7 @@
    Registration and Children Final - The parent's Facilitator learns of a child when the parent's Seller - registers it: a POST of the child's co-signed contract to the - parent's contract resource (). The - registering party is the child's Buyer, which is why it holds the - child's contract and why it is authorised: it is a party to both. + The parent's Facilitator learns of a child when the child's co-signed + contract is posted to the parent's children resource + (). Any holder of that contract may post it; + the registration is authenticated by the child's own signatures, and + the child's Buyer, which is the parent's Seller, is the party that + ordinarily holds it. A registered child is identified at the parent's + venue by its digest, so its id need not be unique there. A Facilitator MUST refuse a registration, with the problem type - named, when: the body is not a valid contract - (); its parent.vtc_hash is not the + named, when: the body is not a valid contract (, the + rules on members and signatures; its terms and deadline are its own + Facilitator's to check); its parent.vtc_hash is not the parent's digest or its parent.facilitator is not this Facilitator (parent-unresolvable); its parties.buyer is not the parent's parties.seller @@ -1429,13 +1387,10 @@ A child becomes final for its parent when the parent's Facilitator holds the child's Outcome Record. It may obtain that record itself, - by retrieving it from the child's Facilitator, or receive it from the - parent's Seller by a POST to the same resource + by retrieving it from the child's Facilitator, or receive it by a POST to the child's entry under that resource (). Either way the Facilitator MUST verify the record's Facilitator signature against a key bound to the - identifier the registration recorded, and MUST verify that its - vtc_hash is the registered child's digest, before recording - child-final. Where the child's latest finality instant passes + identifier the registration recorded, and MUST verify that its vtc_hash is the registered child's digest, refusing a record that fails either check (child-outcome-invalid), before recording child-final. Where the child's latest finality instant passes with no record held, the Facilitator records child-unresolved. children-final follows when every registered child has one entry or the other, and the parent's @@ -1515,39 +1470,58 @@ carries the contract's state and the trace recorded so far.
    - A Contract Status after the Verdict of Figure 1 + A Contract Status after the Verdict of +]]>
    - Two rules make a Status worth keeping. A Facilitator MUST issue a - Status for every request it accepts, carrying the entry that request + Two rules make a Status worth keeping. A Facilitator MUST issue a Status for every POST it accepts, carrying the entry that request caused, so that the requester holds a signed receipt of what was recorded and when. And the trace in every Status a Facilitator issues for a contract MUST be a prefix of the trace in every later one; two @@ -1572,7 +1546,7 @@ A Facilitator MUST issue exactly one Outcome Record for every contract that reaches a terminal state, including SETTLED and ABANDONED, MUST sign it, and MUST NOT require the signature of any - other party on it. The -00 revision's record needed the signature of + other party on it. A Facilitator MUST serve the bytes of the record it signed rather than sign it again on retrieval; under a randomized signature scheme a second signing would produce a second record with a different digest. The -00 revision's record needed the signature of the party it recorded against, which made a reputation layer built on it structurally incapable of recording a loss. The Facilitator signature is what makes the record evidence: without it the record is @@ -1588,57 +1562,109 @@ { "pact": "0.2", "type": "OutcomeRecord", - "vtc_id": "vtc_7f3a91", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a322\ - 5fbbd4ebace4fb980f1c2", + "vtc_id": "vtc_9f2c11", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff49493\ + 04c2c855ca1d746ba6459", "parties": { - "buyer": "did:web:acme.example", - "seller": "did:web:dataforge.example", + "buyer": "did:web:buyer.example:agents:procure-1", + "seller": "did:web:dataforge.example:agents:etl-3", "facilitator": "did:web:settle.example", - "verifier": "did:web:audit.example" + "verifier": "did:web:audit.example" }, - "outcome": { "state": "SETTLED", "challenge_upheld": true }, - "work_hash": "sha256:9c1f...", + "outcome": { + "state": "SETTLED", + "challenge_upheld": true + }, + "work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee2\ + 80a3170b6b9a5037036ef0", "trace": [ - { "event": "accepted", "at": "...", - "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ - 225fbbd4ebace4fb980f1c2" }, - { "event": "funded", "at": "..." }, - { "event": "delivered", "at": "...", - "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ - aaf2f186702504fba242ffb" }, - { "event": "verdict", "at": "...", - "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8\ - e09452a74bac15e6752ca81", "outcome": "PASS" }, - { "event": "window-opened", "at": "...", "closes_at": "..." }, - { "event": "challenge", "at": "...", - "object": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df\ - 6cfef12d12d1340c15e5d85" }, - { "event": "verdict", "at": "...", - "object": "sha256:012bab194f444012f87ff8986419537f9de9f3dfe\ - 845c6623ac6a7488fb98b73", "outcome": "FAIL", - "answers": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4d\ - f6cfef12d12d1340c15e5d85", - "supersedes": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51\ - cee8e09452a74bac15e6752ca81" }, - { "event": "children-final", "at": "..." }, - { "event": "terminal", "at": "...", "state": "SETTLED", - "challenge_upheld": true } + { + "event": "accepted", + "at": "2026-11-01T10:00:00Z", + "object": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff494\ + 9304c2c855ca1d746ba6459" + }, + { + "event": "funded", + "at": "2026-11-01T10:00:00Z" + }, + { + "event": "delivered", + "at": "2026-11-10T08:30:12Z", + "object": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370\ + cb3023331bc8bfbd924fcbe" + }, + { + "event": "verdict", + "at": "2026-11-10T09:14:30Z", + "object": "sha256:1ac94d72dbdd1f51e523ecddb3a3b360703976215\ + b13324485db355c948adfc0", + "signer": "did:web:audit.example#k1", + "outcome": "PASS" + }, + { + "event": "window-opened", + "at": "2026-11-10T09:14:30Z", + "closes_at": "2026-11-10T10:14:30Z" + }, + { + "event": "challenge", + "at": "2026-11-10T09:40:00Z", + "object": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093e\ + c2c6795962e67a6e2ff55ff", + "signer": "did:web:watch.example#k1", + "costs": { + "amount": "1.20", + "currency": "USDC" + } + }, + { + "event": "verdict", + "at": "2026-11-10T09:58:05Z", + "object": "sha256:10d537e7b8face8bd7695541d36d32568394a5319\ + 7653280c404e2d84a63d46d", + "signer": "did:web:audit.example#k1", + "outcome": "FAIL", + "answers": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093\ + ec2c6795962e67a6e2ff55ff", + "supersedes": "sha256:1ac94d72dbdd1f51e523ecddb3a3b36070397\ + 6215b13324485db355c948adfc0" + }, + { + "event": "children-final", + "at": "2026-11-10T09:58:05Z" + }, + { + "event": "terminal", + "at": "2026-11-10T09:58:05Z", + "state": "SETTLED", + "challenge_upheld": true + } ], "terms_result": { - "profile": - "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5f\ - c1147743326dabd45bda546dc62", - "currency": "USDC", + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restit\ + ution", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf\ + 6956e835df2653b99d71d363a68", + "currency": "USDC", "transfers": [ - { "event": 8, "from": "...", "to": "...", "amount": "...", - "code": "..." } + { + "event": 8, + "from": "...", + "to": "...", + "amount": "...", + "code": "..." + } ] }, - "signatures": [ { "protected": "...", "signature": "..." } ] + "signatures": [ + { + "protected": "...", + "signature": "..." + } + ] } - ]]> +]]> The record carries one signature, the Facilitator's. The Seller did @@ -1659,23 +1685,18 @@ line that produced the entry). This document defines the form of the list and two arithmetic - facts about it, and nothing about what any entry means. Over the - accounts and opening amounts the profile declares for the contract - (): no entry takes from an account more than + facts about it, and nothing about what any entry means. Over the internal accounts the profile declares (), which open empty: no entry takes from an account more than that account holds at that point in the list; and after the last - entry every account the profile marks internal holds zero. A - Facilitator MUST NOT sign an Outcome Record whose list breaks either - fact, and MUST NOT sign one whose list differs from what the - profile's schedule produces for the record's own trace. Any party + entry every account the profile marks internal holds zero. The two facts are constraints on a profile, checked against its vectors before a Facilitator lists it (); a Facilitator MUST NOT sign an Outcome Record whose list breaks either, since such a list shows the profile it evaluates to be defective, and MUST NOT sign one whose list differs from what the profile's schedule produces for the record's own trace. Any party holding the contract, the trace and the profile's bundle can recompute the list; that is the property the experiment in depends on. vectors.json in a profile's bundle is an array of objects, each with name, contract (a VTC, or the - members of one the schedule reads), trace (a complete - trace), and transfers (the list the schedule produces for - it). A Facilitator MUST reproduce every vector of a profile before + members of one the schedule reads), and then either trace (a complete + trace) with transfers (the list the schedule produces for + it) and accounts (the profile's internal accounts, which open empty and must close empty), or admission (an object carrying either admitted, true, or refused, the problem type the admission rule answers with for that contract). A Facilitator MUST reproduce every vector of a profile before listing that profile in its capability document (), which is the only conformance requirement this document places on a profile implementation. @@ -1693,25 +1714,19 @@ largest power of two smaller than n. The shape is therefore fixed by n alone, and two implementations that agree on D agree on the root. - The domain separation is not optional. Without distinct prefixes an - attacker can present an interior node as though it were a leaf, and so - claim an inclusion proof for a subtree that never existed. + The domain separation is not optional, because the prefixes are what make MTH the function defines, and a second implementation must compute the same root. The second-preimage attack the prefixes guard against, a leaf input chosen to equal an interior node's input, needs a leaf of that input's length; the fixed 32-byte digests in D cannot supply one, so here the prefixes buy agreement with the RFC rather than a defence the construction would otherwise lack. The member is present when at least one child is registered and absent otherwise; it MUST NOT be present with an empty or zero value, which would be indistinguishable from a tree whose children were withheld. Where every registered child is unresolved D is empty and the root is MTH of the empty list, SHA-256 of the empty string; the child-unresolved entries in the trace say which records the - root does not cover. The -01 revision computed leaves over records - with their signatures removed, which let a record be re-signed - without changing the root. + root does not cover. The -01 revision did not say whether a leaf covered the record's signatures; this revision says it does, so a record cannot be re-signed without changing the root.
    Protocol Endpoints - This section specifies the operations a Facilitator exposes. Base - URIs are not fixed by this document; they are discovered from the - endpoints member of the capability document + This section specifies the operations a Facilitator exposes. This document fixes no base URI; each is discovered from the endpoints member of the capability document (), so a Facilitator may mount them anywhere on its origin. @@ -1724,8 +1739,7 @@ {contract}/{id}/children; body, the child's contract; 201 with a Status.
    Supply a child's outcome:
    POST - {contract}/{id}/children/{child_id}; body, the child's - Outcome Record; 200 with a Status.
    + {contract}/{id}/children/{child_hash}, where child_hash is the digest by which the registration identifies the child; body, the child's Outcome Record; 200 with a Status. This resource is keyed by digest, so the id rule of does not apply to it.
    Submit a Delivery:
    POST {delivery}; body, a Delivery; 202 with a Status.
    Record a Verdict:
    POST {verdict}; body, a @@ -1736,13 +1750,17 @@ {outcome}/{id}; 200 with the Outcome Record.
    + A request naming a contract the Facilitator does not hold is refused + as unknown-contract (404). A body larger than the Facilitator + accepts is refused as payload-too-large (413). A failure inside + the Facilitator is reported as internal-error (500), the one + problem type that names no rule. A GET of an Outcome Record before the + terminal entry is refused as wrong-state. + All requests and responses use the media types defined in . All requests MUST be made over HTTPS, following the recommendations of . Status codes are as - defined in . A Delivery and a Challenge are - answered 202 (Accepted) rather than 201 because - acceptance of the bytes is not acceptance of the work; what follows - depends on a Verdict the Facilitator does not itself produce. + defined in . A Delivery and a Challenge are answered 202 (Accepted) because, in the sense of Section 15.3.3, their processing is not complete when the response is sent: what either record leads to may depend on a Verdict the Facilitator does not itself produce. A contract and a Verdict are answered 201 (Created); the contract is the resource the Location header names, and a Verdict, to which this document gives no resource of its own, is identified by the digest the Status in the response carries.
    A Facilitator authenticates the sender of a POST by the signature on the body, and by nothing else in this document: it MUST reject a @@ -1759,10 +1777,10 @@ , and before creating the resource, MUST refuse a contract whose parties.facilitator is not itself or - whose price.settlement, network or asset it does not + whose price.settlement, network or currency it does not advertise (facilitator-mismatch, settlement-unsupported), and MUST refuse otherwise with the - problem type that names the rule. + problem type that names the rule. settlement-unsupported also covers a price stated in a currency other than max_contract_value's and a price above it; a verification profile the Facilitator does not list is refused as settlement-unsupported.
    @@ -1797,8 +1815,7 @@ Content-Type: application/vnd.pact.status+json Where a POST carries the same object id as an existing resource but a different digest, the Facilitator MUST respond - 409 (Conflict) (object-conflict). Retrying a - submission is therefore always safe, and altering one never is. + 409 (Conflict) (object-conflict). Retrying a submission is therefore safe when the same bytes are resent, and altering one never is. A record signed afresh is a different record with a different digest, not a retry: ECDSA signatures are randomized unless produced as describes, so a client signing with ES256 or ES384 SHOULD sign deterministically or keep the bytes it sent and resend those.
    Error Responses @@ -1816,16 +1833,18 @@ Content-Type: application/vnd.pact.status+json failure of the specification. @@ -1833,7 +1852,7 @@ Content-Type: application/problem+json
    Exchange
    - HTTP exchange for the flow in Figure 1 + HTTP exchange for the flow in Signatures Every signature carried by a VTC, Delivery, Verdict, Challenge, - Status, Outcome Record or capability document is a JWS - in the General JSON Serialization of - Section 7.2.1 of that document, with the payload detached as its - Appendix F describes. The payload is BASE64URL of the JCS-canonical + Status, Outcome Record or capability document has, for each signer, the form of one signature object of the JWS General JSON Serialization, Section 7.2.1 of that document, with the payload detached as its Appendix F describes. The payload is BASE64URL of the JCS-canonical bytes of the object with the signing member removed, so the JWS Signing Input is ASCII(BASE64URL(UTF8(protected)) || "." || BASE64URL(JCS(object))) exactly as Section 5.1 of @@ -1881,10 +1897,7 @@ Content-Type: application/problem+json
    • The protected header MUST carry alg, kid and typ.
    • -
    • alg MUST be ES256 or ES384 - , or EdDSA - with an Ed25519 key; a verifier MAY also accept Ed448. A verifier - MUST reject any other value, and MUST reject none. Absent +
    • alg MUST be Ed25519 with an Ed25519 key, or ES256 or ES384 with a P-256 or P-384 key. A verifier MUST reject any other value, including the polymorphic EdDSA identifier of that deprecates, and MUST reject none (algorithm-not-permitted). Absent an allowlist an attacker selects the algorithm, which permits both unsigned acceptance and confusion of a public key for a symmetric secret.
    • @@ -1893,6 +1906,16 @@ Content-Type: application/problem+json signed bytes is rewritable in transit, which allows an attacker who can publish a key document to re-attribute a genuine signature to itself. +
    • The protected header MUST NOT carry jwk, jku, + x5c, x5u, x5t, x5t#S256 or + crit, and a signature entry MUST NOT carry an unprotected + header; a verifier MUST reject an entry carrying any of them + (signature-invalid). A key travels by reference and never + inline, so that the kid rule cannot be bypassed.
    • +
    • The resolved key MUST be of the type and curve alg + requires: Ed25519 for Ed25519, P-256 for + ES256, P-384 for ES384. A mismatch is + signature-invalid.
    • typ MUST be the full media type of the object signed, including the application/ prefix, so that a signature over one object type cannot be replayed as a signature over @@ -1916,7 +1939,7 @@ Content-Type: application/problem+json encoding and not which of the two valid s values is accepted; accepting both lets anyone holding a valid signature produce a second one over the same bytes without the key, and a - second signature is a second digest. EdDSA verification per + second signature is a second digest. Ed25519 verification per already rejects a non-canonical S, so the rule is stated for ECDSA only.
    @@ -1942,8 +1965,7 @@ Content-Type: application/problem+json establishes that the holder of that key signed; that the key belongs to the party is a property of the identity method, and this document does not add to it. An identity system for agents defined - elsewhere, such as , is used by - naming its identifiers here and resolving them by its rules.
    + elsewhere, such as , is used by naming its identifiers here in one of these two forms; a further form needs a resolution rule added to this list, which is the one change it would take.
    @@ -1958,8 +1980,7 @@ Content-Type: application/problem+json (signature-missing, unexpected-signer). A count of signatures is not sufficient: two signatures covering one identifier MUST be rejected. -
  • challenge.window_seconds MUST be greater than zero, - and task.deadline MUST be later than the instant of +
  • challenge.window_seconds, challenge.max_dispute_seconds and verification.max_verdict_seconds MUST be greater than zero, and task.deadline MUST be later than the instant of acceptance (deadline-invalid).
  • Every URI member inside hash-committed content MUST have a sibling hash member, and a validator MUST reject content carrying @@ -1973,15 +1994,18 @@ Content-Type: application/problem+json terms.parameters MUST validate against that profile's schema (terms-unsupported, terms-parameters-invalid).
  • -
  • Every amount MUST have the form in - (amount-invalid), and every - object MUST validate against the schema published for its media - type (schema-invalid).
  • +
  • Every object MUST validate against the schema published for its + media type (schema-invalid), which includes the form of + every amount (); an amount carrying more + decimal places than the settlement binding named in + price.settlement supports is refused + (amount-invalid).
  • Test Vectors - Each rule above has an accepting and a rejecting form. A + Most rules above have an accepting and a rejecting form; the table + carries the ones a suite most often gets wrong. A conformance suite built from this section alone, with no reference to any implementation, should reach the same verdicts. Rejecting vectors name the rule they violate. @@ -2026,8 +2050,7 @@ Content-Type: application/problem+json L(parent)reject V-17Verdict signed by the seller reject - V-18object keys ordered by code point, with a - supplementary-plane keydigest mismatch + V-18object keys ordered by code point rather than UTF-16 unit, a supplementary-plane key beside one in U+E000 to U+FFFFdigest mismatch V-19buyer and seller differing only in the case of a did:web pathaccept V-20object carrying a member this document @@ -2044,6 +2067,8 @@ Content-Type: application/problem+json V-25a number serialized by the host language's default formatter, such as 1.0 for the float onedigest mismatch + V-26a protected header carrying a member forbids: jwk, jku, x5c, x5u, x5t, x5t#S256 or crit + reject @@ -2057,19 +2082,17 @@ Content-Type: application/problem+json half of the V-18 mistake: prints numbers as ECMAScript does, so the float one is 1 and never 1.0. The -02 reference canonicalizer printed 1.0 - until this vector caught it, and every digest in - changed when it was fixed. + until this vector caught it, and the spec_hash, vtc_hash and delivery_hash of changed when it was fixed.
    Worked Example - The tables and digests below are the reference repository's, at the + The digests below are the reference repository's, at the tag named in . The object figures in earlier - sections use short illustrative identifiers for page width; the - repository examples carry the full ones, and the digests here are - computed over those. The figures that the -01 revision printed here - about a bond and a required detection rate are now the profile's, and - carries them. + sections are the repository's objects with their signatures + abbreviated and the contract's parameters elided; the digests + here are computed over the full objects. The figures that the -01 revision printed here + about a bond and a required detection rate are now the profile's; carries the rule and the parameters. A buyer commissions a data transformation at a price of 180.00 USDC under the verdict-first flow, the acceptance @@ -2085,12 +2108,12 @@ Content-Type: application/problem+json d27ff6bee37f05531823b72 criteria_hash sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b83\ 51c9c55eb9316416265fc1b - profile_hash sha256:00d71829f6f9192b43b929d0154a6eb409f5fc114\ - 7743326dabd45bda546dc62 - vtc_hash sha256:3e755194b949b7327db8bb6a716add3b40828d9a3\ - 225fbbd4ebace4fb980f1c2 - delivery_hash sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8a\ - aaf2f186702504fba242ffb + profile_hash sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf6956\ + e835df2653b99d71d363a68 + vtc_hash sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff494\ + 9304c2c855ca1d746ba6459 + delivery_hash sha256:6bcbb831ea27a8754a0df9b44361be12411e45370\ + cb3023331bc8bfbd924fcbe ]]> criteria_hash is the manifest digest of @@ -2102,38 +2125,25 @@ Content-Type: application/problem+json digest of the signed contract, and delivery_hash of the signed Delivery, both per . - Every value above changed from the -01 revision, for four reasons - that are each recorded so that a reader comparing the two documents can - account for the difference: spec_hash because the TaskSpec - now carries the sibling hashes always required; - vtc_hash because the contract's members changed - () and because spec_hash did; - delivery_hash because it now covers the Delivery's signature; - and profile_hash because it did not exist. - - The trace the reference implementation records for this contract - on the path of , and on the dispute path of - , together with the transfer lists the - profile produces for each, are the vectors in the profile's bundle, - and prints them. + The -01 revision printed three of the values above, spec_hash, criteria_hash and vtc_hash, and each differs from what it printed, for reasons recorded so that a reader comparing the two documents can account for the difference. spec_hash and vtc_hash differ because the -01 canonicalizer serialized numbers as the host language printed them (, V-25); spec_hash also because the TaskSpec now carries the sibling hashes always required, and vtc_hash also because the contract's members changed (). criteria_hash carries no number; it differs because the two files of the instrument bundle were edited to drop their mention of the withdrawn call-for-bids example. profile_hash is new, and delivery_hash, which the -01 figures showed only as a placeholder, now covers the Delivery's signature. + + The traces the reference implementation records for this contract on the path of and on the dispute path of carry the event sequences of the first two vectors in the profile's bundle, and the transfer lists the profile produces for them are those vectors' lists; the vectors name their objects by placeholder digests, so the match is of sequence and lists, not of bytes. prints both lists.
    Implementation Status This section records the status of known implementations of this - document per , and is to be removed before - publication as an RFC. + document per . The section and the reference to are to be removed before publication as an RFC, and the listing of an implementation here implies no endorsement by the IETF. One implementation is known to the author, and the author wrote it: - https://github.com/pact-spec/spec, under the Revised BSD licence. At + https://github.com/pact-spec/spec, under the Apache License 2.0. At tag v0.2.0 it comprises the object schemas, the examples whose digests - prints, a conformance validator that runs - 103 checks including every vector of + prints, a conformance validator that runs 107 checks including every vector of , a Facilitator serving the endpoints of with the profile of , and clients for the other roles. Its previous tag, v0.1.0, implemented the -01 revision and is the source of the measurements the author has published about it. No second implementation exists, so nothing in has - been tested, and this document claims no interoperability. + been tested, and this document claims no interoperability. It is an individual submission and the product of no working group; the implementation is a prototype, the information is current as of the tag named above, and the contact is the author.
    Security Considerations @@ -2160,7 +2170,7 @@ Content-Type: application/problem+json anyone holding the contract Seller cannot deliver against a substituted instrument or input; - cannot judge its own Delivery; cannot re-sign a record without + is refused as Verifier when it signs under a party identifier; cannot re-sign a record without changing every digest over it its signature on the contract and the Delivery; the Verdicts and Challenges on its Delivery @@ -2199,9 +2209,7 @@ Content-Type: application/problem+json client SHOULD retain every Status it receives, and a party that submitted a record and holds no Status for it has a claim it can make only outside this protocol. Making omission attributable needs - a witness the Facilitator does not control, such as a monitor with a - gossip path of the kind assumes, and this - document specifies none. + a witness the Facilitator does not control, such as the client gossip that Section 11.3 mentions and leaves undefined, and this document specifies none. Time is the Facilitator's. Every instant in a trace is read from its clock, and nothing in this document lets a party prove that a recorded instant is wrong. This document therefore states the @@ -2251,7 +2259,7 @@ Content-Type: application/problem+json The -00 revision committed harness_uri as a string. The bytes at that URI were covered by nothing. A Buyer could therefore sign a contract, replace the acceptance instrument afterwards, run the - replacement, and submit its failure as a textbook-valid fraud proof. + replacement, and submit its failure as a textbook-valid proof of nonconformance. Cost of the attack: one file overwrite. The mirror attack works against a Seller that hosts the input sample. requires a sibling hash over the dereferenced @@ -2329,12 +2337,11 @@ Content-Type: application/problem+json
    Nondeterminism as Shield and as Weapon A re-execution profile that does not state what determinism it - assumes cuts both ways. An honest Seller doing model-assisted work is - convicted by a re-execution that differs for ordinary reasons. A - cheating Seller escapes any fraud proof by asserting nondeterminism, + assumes cuts both ways. An honest Seller doing model-assisted work is found wrong by a re-execution that differs for ordinary reasons. A + cheating Seller escapes any proof of nonconformance by asserting nondeterminism, unfalsifiably. A verification profile MUST state whether it is deterministic and what tolerance applies, and a contract naming one - that does not is not safely enforceable by anyone. + that does not cannot be judged safely by anyone.
    Fabricated History @@ -2350,12 +2357,10 @@ Content-Type: application/problem+json
    Retrieval - A GET on a contract's Status or Outcome Record MUST be refused - unless the requester is a party named in the contract's + A GET on a contract's Status or Outcome Record MUST be refused (retrieval-restricted) unless the requester is a party named in the contract's parties, the identifier in the contract's parent.facilitator, or a party the Facilitator has chosen to - admit; a Facilitator MAY open retrieval more widely and SHOULD say so - in its capability document. How a requester proves which identifier + admit; a Facilitator MAY open retrieval more widely and SHOULD say so in its capability document (retrieval, ). How a requester proves which identifier it is, on a GET with no body to sign, is an HTTP-layer matter this document leaves to the deployment. The -01 revision left retrieval unauthenticated by default, which published every contract graph a @@ -2367,10 +2372,7 @@ Content-Type: application/problem+json signs contracts the party never agreed to. Rotation and revocation belong to the identity method behind the kid (), and this document does not restate them. - Two things it does require: a Facilitator MUST record, with each - record it accepts, the key material or its digest as resolved at the - time of acceptance, so that a later rotation does not make an earlier - signature unverifiable; and a Facilitator MUST NOT accept a record + Two things it does require: a Facilitator MUST retain, for as long as it retains a record it accepted, the key material or its digest as resolved at the time of acceptance, and SHOULD make it available to a party retrieving the record, so that a later rotation does not make an earlier signature unverifiable; and a Facilitator MUST NOT accept a record whose kid resolves to a key the identity method marks as revoked at the time of acceptance.
    @@ -2378,10 +2380,7 @@ Content-Type: application/problem+json
    Denial of Service by Challenge Every accepted Challenge costs an independent evaluation. Without a cost to the Challenger, a party can exhaust a Verifier's or a - Facilitator's capacity by challenging every Delivery. The deposit of - is one defence, and it is a MAY because a - deposit also deters the honest challenger an open model relies on. A - Facilitator that requires no deposit SHOULD rate-limit Challenges per + Facilitator's capacity by challenging every Delivery. A deposit required by a terms profile, advertised as challenge_deposit (), is one defence, and this document requires none, since a deposit also deters the honest Challenger an open model relies on. A Facilitator whose profiles require no deposit SHOULD rate-limit Challenges per Challenger and per contract, and SHOULD publish that it does so.
    @@ -2417,10 +2416,10 @@ Content-Type: application/problem+json with mechanisms specified elsewhere and is not specified here.
    -
    Challenger Access +
    Challenger Access An open challenge model requires that some party outside the contract can obtain the deliverable and the input in order to build a - fraud proof. That is in direct conflict with confidentiality of both. + proof of nonconformance. That is in direct conflict with confidentiality of both. The conflict is real and this document does not dissolve it. What it does is make the choice visible: a contract whose content cannot be disclosed to a Challenger will receive no Challenge from outside its @@ -2429,8 +2428,7 @@ Content-Type: application/problem+json
    Retention - Retention duties stated for dispute purposes can conflict with - erasure rights asserted by a data subject. Contracts SHOULD state a + Retention periods stated for dispute purposes can conflict with erasure requests from a data subject. Contracts SHOULD state a retention period, and implementers should be aware that a hash commitment survives deletion of the content it commits to, which is usually the property they want and occasionally the one they must @@ -2470,9 +2468,7 @@ Content-Type: application/problem+json
    Interoperability considerations:
    Objects MUST be canonicalized per before hashing or signing. Implementations that canonicalize by sorting object keys - on Unicode code point rather than UTF-16 code unit will produce - divergent digests for keys outside the Basic Multilingual - Plane.
    + on Unicode code point rather than UTF-16 code unit can produce a divergent digest when a key outside the Basic Multilingual Plane is compared with one whose first differing unit lies in U+E000 to U+FFFF.
    Published specification:
    This document
    Applications that use this media type:
    Services and autonomous agents forming and recording task contracts under this @@ -2516,8 +2512,7 @@ Content-Type: application/problem+json The -01 revision asked for these in the standards tree under the names pact-contract+json and so on. Registration in that tree from outside the IETF stream needs approval this document does - not have (, Section 3.1), and the vendor - tree is where an individual's specification belongs. + not have (, Section 3.1), and Section 3.2 opens the vendor tree to anyone who interchanges files associated with a publicly available product.
    Well-Known URI @@ -2544,12 +2539,7 @@ Content-Type: application/problem+json permits without registration. Each is the identifier in the table appended to the prefix tag:laxsharma79@gmail.com,2026:pact:problem:, a tag URI - under the author's control. A tag URI is - an identifier and is not dereferenceable, which is why it was - chosen over the -01 revision's prefix on a code-hosting site: an - identifier should not change when hosting does. Documentation for - every type is maintained in the repository named in - . Each entry carries the identifier, the + under the author's control. A tag URI is an identifier and does not dereference. Section 4 says a type URI SHOULD resolve to documentation; this document departs from that on purpose, so that an identifier does not change when hosting does, which the -01 revision's prefix on a code-hosting site could not promise, and the section named for each type is its documentation. The list of types, each with its status and the section that defines it, is printed by the reference implementation in the repository named in , and the section named for each type says what it means. Each entry carries the identifier, the HTTP status it accompanies, and the section stating the rule it reports. A terms profile that refuses a request defines its own types under its own prefix and reports them as @@ -2599,9 +2589,9 @@ Content-Type: application/problem+json settlement-unsupported422 - signature-invalid401 + signature-invalid400 - signature-missing401 + signature-missing400 signatures-unordered422 @@ -2640,6 +2630,7 @@ Content-Type: application/problem+json + @@ -2652,7 +2643,6 @@ Content-Type: application/problem+json - Decentralized Identifiers (DIDs) v1.0 W3C @@ -2662,18 +2652,35 @@ Content-Type: application/problem+json did:web Method Specification W3C Credentials Community Group - + + Unofficial draft, undated; accessed 16 September 2026 + Informative References + + + CAIP-2: Blockchain ID Specification + Chain Agnostic Standards Alliance + + Status: Final + + + + Extension: compliance-fields + wowlegend (Tersign), pull request author + + + Open pull request 2853 to x402-foundation/x402, specs/extensions/compliance_fields.md, unmerged as of September 2026 + @@ -2682,9 +2689,54 @@ Content-Type: application/problem+json - - - + + + VCAP-AP2 Binding: Verified Delivery Settlement for the Agent Payments Protocol + + SwarmSync.AI + + + + This document defines a binding between Verified Commerce for Agent Protocols (VCAP) and the Agent Payments Protocol (AP2). AP2 supplies agent-commerce authorization evidence through IntentMandate, CartMandate, and PaymentMandate artifacts. VCAP supplies delivery verification, settlement evidence, escrow directives, timeout handling, and dispute handoff. This revision deliberately does not model AP2 as an escrow or settlement state machine. Current AP2 positions itself as an authorization and security layer used within a surrounding commerce protocol, including Universal Commerce Protocol (UCP). Accordingly, this binding references AP2 mandates by cryptographic digest or opaque identifier and leaves payment capture, refund, and settlement transitions to the commerce protocol and payment rail. + + + + + + + Signed, Hash-Chained Action Receipts for AI Agents + + kriya native + + + + This document specifies a format for action receipts: compact, individually signed JSON records that state that a specific AI agent attempted a specific action at a specific time, under a specific policy decision, and what the outcome was. Receipts are linked into an append-only hash chain so that deletion, insertion, reordering, or modification of any previously recorded receipt is detectable by a verifier that holds only the records and the signer's public key. The format is deliberately small and self-contained. Verification requires no network access, no service operated by the producer of the receipts, and no state beyond the records themselves and a trust anchor obtained out of band. This document specifies the record fields, the canonical byte sequence that is signed, the chain linkage rule, the verification procedure, and test vectors. + + + + + + + Agent Accountability: Composition and Conformance + + Action State Group, Inc. + + + MyAuberge K.K. + + + EMILIA Protocol, Inc. + + + Independent + + + Tyche Institute + + + + + @@ -2695,6 +2747,14 @@ Content-Type: application/problem+json Internet-Draft, draft-laxsharma-pact-01, superseded by this document + + + PACT: A Contract Layer for Autonomous Agent Commerce + + + + Internet-Draft, draft-laxsharma-pact-00, superseded + Asynchronous Protocols for Optimistic Fair Exchange @@ -2745,20 +2805,18 @@ Content-Type: application/problem+json in has something to run against and the vectors in the reference repository have something to reproduce. It is the -01 revision's settlement content written as a schedule over - the events of , with the choices the -01 - revision left open now made, and it is offered as an example of the + the events of , with the choices the -01 revision left open now made and two of its own choices changed where the arithmetic or its text required (), and it is offered as an example of the form a profile takes, not as a recommendation of these terms. What the figures below mean between the parties to a contract that names this profile is a question this document does not answer and its author is - not qualified to answer; a profile meant for use needs an owner who - is. + not qualified to answer; a profile meant for use needs an owner who is. Until such a profile exists, this one is also the only profile a Facilitator can list, since terms_profiles must have an entry; that is a fact about the present and not a rule of this document.
    Identity and Bundle Identifier: tag:laxsharma79@gmail.com,2026:pact:bonded-restitution. The bundle in the reference repository, under profiles/bonded-restitution/, contains - README.md (this text), parameters.schema.json and + README.md (the prose of this appendix, in Markdown), parameters.schema.json and vectors.json; profile_hash is the manifest digest over those three files and prints it. Problem types this profile reports are under the prefix @@ -2769,10 +2827,8 @@ Content-Type: application/problem+json
    seller_bond:
    amount, required. What the Seller posts before performance.
    -
    verification_fund:
    amount, required. What - the Buyer posts to pay for checking.
    -
    cap:
    amount, required. The most that leaves - the Seller's accounts under this contract.
    +
    verification_fund:
    amount, required. What the Seller posts to pay for checking; the -01 prose never said who posts it and its figure drew it from the Seller, which this profile follows.
    +
    cap:
    amount, required. The most that leaves the bond under this contract; it bounds ranks 3 to 5 together, and what the bond holds beyond it returns to the Seller.
    restitution_basis:
    string, required. released or price.
    remainder_to:
    string, optional. @@ -2783,9 +2839,16 @@ Content-Type: application/problem+json which the price moves to the Seller: verdict (a PASS Verdict), delivered, or window-closed.
    assurance:
    object, required. mode - (certain, committed-sample or open) and - q_min (a number greater than zero and at most one).
    + (certain, committed-sample or open), + q_min (a number greater than zero and at most one) and, + under committed-sample, sample_rate (a number + greater than zero and at most one: the declared fraction of + deliveries verified; the draw MUST derive from a seed the Buyer committed before the Delivery was submitted, combined with the Delivery's digest; how the seed is committed is outside the + profile).
    + This profile defines no Challenge deposit; a Facilitator that + advertises challenge_deposit does not do so under this + profile. The -01 revision's four release modes map onto flow and principal_on as shows.
    @@ -2811,12 +2874,14 @@ Content-Type: application/problem+json not hold, or when assurance.mode is open alone. The inequality is the classical deterrence bound (; Theorem 1 - for outsourced computation), with E the one term the -01 revision - added: value that moved before a Verdict cannot be recovered by the - schedule, so it raises what the Seller must post one for one. A + for outsourced computation), with E the one term the -01 revision added: principal that moves before any Verdict is outside what the Verifier's check can withhold, so it raises what the Seller must post one for one. The bound deters nonconformance against that check and says nothing about what a later Challenge recovers; after a PASS is overturned the restitution of the schedule is bounded by the bond and the cap, whatever principal_on was. A contract whose seller_bond or verification_fund exceeds cap is reported as parameters-inconsistent. + The rule is falsified, and this profile with it, if the constraint + proves unworkable at the prices and verification costs real + deployments exhibit. That was the -01 revision's own failure + condition, restated here where the rule now lives.
    Schedule @@ -2827,7 +2892,7 @@ Content-Type: application/problem+json the sum of principal entries emitted so far.
    funded:
    buyer to escrow, P, lock; - seller to bond, B, bond; buyer to fund, + seller to bond, B, bond; seller to fund, verification_fund, fund.
    delivered:
    if principal_on is delivered: escrow to seller, the escrow balance, @@ -2842,32 +2907,22 @@ Content-Type: application/problem+json to seller, the escrow balance, principal.
    terminal, FINAL:
    escrow to seller, the escrow balance, principal; bond to seller, the bond balance, - return; fund to buyer, the fund balance, + return; fund to seller, the fund balance, fund-return.
    terminal, ABANDONED:
    escrow to buyer, the escrow balance, reverse; bond to seller, the bond balance, - return; fund to buyer, the fund balance, - fund-return. The -01 revision said the bond was slashed - "to the extent of" the basis here and never said by how much; with - the price reversed the Buyer's loss is zero under either basis, so - nothing is slashed.
    + return; fund to seller, the fund balance, + fund-return. The -01 revision said the bond was slashed "to the extent of" the basis here, and its Section 5.3 defined the basis as an amount, the value already released or the full price, without relating either to a loss; with the price reversed the Buyer's loss is zero under either basis, so this profile slashes nothing here, which under basis price is a departure.
    terminal, SETTLED:
    in five ranks, each drawing only what remains. (1) escrow to buyer, the escrow balance, reverse. (2) if challenge_upheld: fund to the Challenger whose Challenge the standing Verdict answers, the lesser - of that Challenge's costs and the fund balance, - costs. (3) bond to buyer, the lesser of the bond balance, - cap, and the Buyer's loss, restitution; the loss + of that Challenge's costs when stated in the contract's currency (otherwise nothing) and the fund balance, + costs. (3) bond to buyer, the lesser of the bond balance, the cap room and the Buyer's loss, restitution; the loss is "released" under basis released and P minus the rank-1 - entry under basis price, which differ only when the price - moved in part. (4) if challenge_upheld: bond to that - Challenger, the bond balance, bounty. (5) bond to buyer or - sink per remainder_to, the bond balance, - remainder. Then fund to buyer, the fund balance, - fund-return.
    + entry under basis price, which coincide under this schedule, since every principal entry moves the whole escrow balance; the parameter is kept for a profile that adds partial release. (4) if challenge_upheld: bond to that Challenger, the lesser of the bond balance and the cap room, bounty. (5) bond to buyer or sink per remainder_to, the lesser of the bond balance and the cap room, remainder. Then bond to seller, the bond balance, return, which is what the cap kept; then fund to seller, the fund balance, fund-return. The cap room at each rank is cap less what the entries so far have moved out of the bond.
    - Ranks 2 and 4 pay one Challenger, the one whose Challenge the - standing Verdict answers. A Challenge that was not answered by the + Ranks 2 and 4 pay one Challenger, the one whose Challenge the standing Verdict answers. The -01 revision required the reward to be non-exclusive, paying every independent discoverer in full; one bond cannot fund that for two discoverers, so this profile pays one and records the departure here. A Challenge that was not answered by the standing Verdict, whether lapsed, rejected or superseded, receives nothing. Rank 4 gives the whole remaining bond, because the -01 revision forbade capping it at a fraction chosen for tidiness and @@ -2882,12 +2937,10 @@ Content-Type: application/problem+json with q 1.0, under the verdict-first flow, and a Challenge claiming costs of 1.20. Amounts are in USDC. Trace indexes count from zero. The lists below are what vectors.json carries - for the two paths in the figures of this document; the repository's - file also carries the SETTLED-by-Verifier and ABANDONED paths and - the price basis. + for the two paths in the figures of this document; the repository's file also carries the SETTLED-by-Verifier, ABANDONED, verdict-lapsed and delivery-first paths, the price basis, and two admission vectors, one refused and one admitted at the boundary of the constraint.
    - FINAL: the path of Figure 1 + FINAL: the path of
    - SETTLED on an upheld Challenge: the path of Figure 5 + SETTLED on an upheld Challenge: the path of 0.50 costs 8 bond buyer 18.00 restitution @@ -2933,9 +2986,7 @@ Content-Type: application/problem+json
    Changes from -01 This revision separates the protocol from the meaning of its terms. The -01 revision, in its title, abstract, Section 1.2 and throughout, - made who owed whom the subject of the document; two readers on the - IETF dispatch list observed in September 2026 that this placed it - outside what the IETF is placed to evaluate, and they were right. What + made who owed whom the subject of the document; one reader on the IETF dispatch list, Rich Salz, read it in September 2026 as a legal framework with a protocol attached, and another, John C Klensin, wrote that its framing was tied closely enough to legal terminology that the IETF was the wrong place to evaluate it; both were right. What follows is the list of what changed, with the wire consequences first.
      @@ -2994,8 +3045,7 @@ Content-Type: application/problem+json child outcome supply, child-unresolved, a finite latest finality instant per contract and the rule L(child) before L(parent); the depth and cycle rules are withdrawn with the reason - (). The -01 Section 10.2 is one sentence in - . + (). The -01 Section 10.2, which had liability cascade upward as recovery and not downward as discharge, is withdrawn to the profile; says only that this document does not state what a child's outcome means for its parent.
    • Section 3 is a data dictionary and a role table (); no sentence in it requires anything of a party.
    • @@ -3010,15 +3060,34 @@ Content-Type: application/problem+json ().
    • The experiment is restated over protocol observables ().
    • +
    • A contract carries exactly one signature per party and no other; + the -01 revision accepted further signers ().
    • +
    • verification.arbiter is withdrawn; nothing read it.
    • +
    • The capability document gains issued_at and + retrieval, and its flows must list verdict-first (). The + well-known URI is registered provisionally, with the author as change + controller.
    • +
    • A Verdict is accepted while one stands only in answer to a + Challenge ().
    • +
    • The -01 rule that confidential content MUST NOT declare open + assurance is the profile's now; + refuses open assurance alone.
    • +
    • signature-invalid and signature-missing are 400 + and not 401, since no HTTP authentication scheme is involved; V-26 + names the protected-header members a verifier rejects + ().
    • +
    • names the repository licence, Apache License 2.0; the -01 said Revised BSD, which was wrong.
    • +
    • alg names are the fully specified ones of , Ed25519 for an Ed25519 key; the polymorphic EdDSA identifier the -01 used is refused.
    Acknowledgements - Rich Salz and John C Klensin, on the IETF dispatch list in - September 2026, read the -01 revision as a document about who owes - whom with a protocol attached, and said so; this revision's split - between records and terms is the consequence, and the author is - grateful for the reading. The UTF-16 key-ordering vector that exposed + On the IETF dispatch list in September 2026, Rich Salz read the -01 + revision as a legal framework with a protocol attached and said so, and + John C Klensin wrote that the framing of its terms was tied closely enough + to legal terminology that the IETF was the wrong place to evaluate it. + Both were right; this revision's split between records and terms is + the consequence, and the author is grateful for the reading. The UTF-16 key-ordering vector that exposed a latent canonicalization defect in the reference validator, and the formulation of verifier independence as a relation the evaluator derives rather than a field the record declares, came from Tersign diff --git a/examples/challenge.json b/examples/challenge.json index d9a27bb..7412a5f 100644 --- a/examples/challenge.json +++ b/examples/challenge.json @@ -2,7 +2,7 @@ "pact": "0.2", "type": "Challenge", "vtc_id": "vtc_9f2c11", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370cb3023331bc8bfbd924fcbe", "proof": { "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", @@ -18,7 +18,7 @@ "currency": "USDC" }, "signature": { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6d2F0Y2guZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0LmNoYWxsZW5nZStqc29uIn0", - "signature": "EnmMGEE08LUGshIdvord3_FzthJV8Sy2SG5uoQkNB29Sj97dfzfpoA-5yJgbyZHnpsI6ahTrVLeU_0cuM48JDw" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjp3YXRjaC5leGFtcGxlI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QuY2hhbGxlbmdlK2pzb24ifQ", + "signature": "O-Rv_I-1ae8fnpQQ118HPC1swAOfi0G-vI4IkAvTx-yskjhuZZ_BwbCVe12zxZ-ZH3DFnA_xllI3jxSydneiBw" } } diff --git a/examples/delivery.json b/examples/delivery.json index 9c914f9..348ba42 100644 --- a/examples/delivery.json +++ b/examples/delivery.json @@ -2,9 +2,9 @@ "pact": "0.2", "type": "Delivery", "vtc_id": "vtc_9f2c11", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2", - "work_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c975a1d1ce519d582306338b1", - "work_uri": "https://cdn.dataforge.example/o/d7f4", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff4949304c2c855ca1d746ba6459", + "work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee280a3170b6b9a5037036ef0", + "work_uri": "https://cdn.dataforge.example/o/a26d", "input_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c975a1d1ce519d582306338b1", "evidence": { "profile": "acceptance", @@ -13,7 +13,7 @@ "results_uri": "https://cdn.dataforge.example/o/28d3" }, "signature": { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6ZGF0YWZvcmdlLmV4YW1wbGU6YWdlbnRzOmV0bC0zI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QuZGVsaXZlcnkranNvbiJ9", - "signature": "9CVlezLLYF2raBHqijKrLTB94BIrXmxNr-5vhFWVilodcH4WXj2ri5Z1QTcQSh34DPmNtZKMf_li4GdlkASzCg" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjpkYXRhZm9yZ2UuZXhhbXBsZTphZ2VudHM6ZXRsLTMjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5kZWxpdmVyeStqc29uIn0", + "signature": "nfFbSutJoIrmHR2YBuvt_csj1uY8Je5POBuVEQpgHxpbPzAXBoUa-SvyRGegpbRUjmBRWkVPa5shiIU8FxRQCQ" } } diff --git a/examples/legacy-00/README.md b/examples/legacy-00/README.md index 3db79dc..699b06b 100644 --- a/examples/legacy-00/README.md +++ b/examples/legacy-00/README.md @@ -1,10 +1,10 @@ # Legacy -00 examples `draft-laxsharma-pact-01` removes the sealed-bid second-price award procedure -from the document (see its Section 1.2). These three examples correspond to +from the document (see its Section 1.2). These four files correspond to Sections 4.1 and 5 of the -00 and are retained here so the published -00 remains checkable, not because they are current. -They are not validated against the -01 schemas and are not part of the +They are not validated against the current schemas and are not part of the conformance surface. If the award procedure returns as a separate draft, they move back. diff --git a/examples/outcome.json b/examples/outcome.json index 927c77a..9148042 100644 --- a/examples/outcome.json +++ b/examples/outcome.json @@ -2,7 +2,7 @@ "pact": "0.2", "type": "OutcomeRecord", "vtc_id": "vtc_9f2c11", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff4949304c2c855ca1d746ba6459", "parties": { "buyer": "did:web:buyer.example:agents:procure-1", "seller": "did:web:dataforge.example:agents:etl-3", @@ -13,12 +13,12 @@ "state": "SETTLED", "challenge_upheld": true }, - "work_hash": "sha256:d7f43b3a51c28274adc4ebe535cc9de80f0b6d6c975a1d1ce519d582306338b1", + "work_hash": "sha256:a26dc519a79ac70835a71ea58c9e34f0625778aee280a3170b6b9a5037036ef0", "trace": [ { "event": "accepted", "at": "2026-11-01T10:00:00Z", - "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2" + "object": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff4949304c2c855ca1d746ba6459" }, { "event": "funded", @@ -27,12 +27,12 @@ { "event": "delivered", "at": "2026-11-10T08:30:12Z", - "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb" + "object": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370cb3023331bc8bfbd924fcbe" }, { "event": "verdict", "at": "2026-11-10T09:14:30Z", - "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8e09452a74bac15e6752ca81", + "object": "sha256:1ac94d72dbdd1f51e523ecddb3a3b360703976215b13324485db355c948adfc0", "signer": "did:web:audit.example#k1", "outcome": "PASS" }, @@ -44,7 +44,7 @@ { "event": "challenge", "at": "2026-11-10T09:40:00Z", - "object": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df6cfef12d12d1340c15e5d85", + "object": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093ec2c6795962e67a6e2ff55ff", "signer": "did:web:watch.example#k1", "costs": { "amount": "1.20", @@ -54,11 +54,11 @@ { "event": "verdict", "at": "2026-11-10T09:58:05Z", - "object": "sha256:012bab194f444012f87ff8986419537f9de9f3dfe845c6623ac6a7488fb98b73", + "object": "sha256:10d537e7b8face8bd7695541d36d32568394a53197653280c404e2d84a63d46d", "signer": "did:web:audit.example#k1", "outcome": "FAIL", - "answers": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df6cfef12d12d1340c15e5d85", - "supersedes": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8e09452a74bac15e6752ca81" + "answers": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093ec2c6795962e67a6e2ff55ff", + "supersedes": "sha256:1ac94d72dbdd1f51e523ecddb3a3b360703976215b13324485db355c948adfc0" }, { "event": "children-final", @@ -73,7 +73,7 @@ ], "terms_result": { "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5fc1147743326dabd45bda546dc62", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf6956e835df2653b99d71d363a68", "currency": "USDC", "transfers": [ { @@ -92,7 +92,7 @@ }, { "event": 1, - "from": "buyer", + "from": "seller", "to": "fund", "amount": "0.50", "code": "fund" @@ -122,8 +122,8 @@ }, "signatures": [ { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6c2V0dGxlLmV4YW1wbGUjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5vdXRjb21lK2pzb24ifQ", - "signature": "eOufu8kaI-zRjU13rTVFSSJbn-zPC2QMn0QpHP4V65OpunfT2KNN3p-YESE7ht9tf2uUPHxWkDZuH1cgVJVsDQ" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjpzZXR0bGUuZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0Lm91dGNvbWUranNvbiJ9", + "signature": "aCCOZ9nppKxHUDSzPWgHryznQCNXaNtClwPUQPgpwGKe05D3FrXDVnsbsFSSyUeYW9Qq3I99SaF4oD2VZi5QAA" } ] } diff --git a/examples/status.json b/examples/status.json index 64acd5b..721c6cd 100644 --- a/examples/status.json +++ b/examples/status.json @@ -2,13 +2,13 @@ "pact": "0.2", "type": "ContractStatus", "vtc_id": "vtc_9f2c11", - "vtc_hash": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2", + "vtc_hash": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff4949304c2c855ca1d746ba6459", "state": "WINDOW_OPEN", "trace": [ { "event": "accepted", "at": "2026-11-01T10:00:00Z", - "object": "sha256:3e755194b949b7327db8bb6a716add3b40828d9a3225fbbd4ebace4fb980f1c2" + "object": "sha256:7af52ecee9592740ca9a8b3e0bbd097cf765ff4949304c2c855ca1d746ba6459" }, { "event": "funded", @@ -17,12 +17,12 @@ { "event": "delivered", "at": "2026-11-10T08:30:12Z", - "object": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb" + "object": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370cb3023331bc8bfbd924fcbe" }, { "event": "verdict", "at": "2026-11-10T09:14:30Z", - "object": "sha256:2e74fdf948aca2d610aa9a3b3ac90e7f72d51cee8e09452a74bac15e6752ca81", + "object": "sha256:1ac94d72dbdd1f51e523ecddb3a3b360703976215b13324485db355c948adfc0", "signer": "did:web:audit.example#k1", "outcome": "PASS" }, @@ -34,7 +34,7 @@ ], "issued_at": "2026-11-10T09:14:30Z", "signature": { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6c2V0dGxlLmV4YW1wbGUjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5zdGF0dXMranNvbiJ9", - "signature": "LZOcBREP7-LMJ8iveBuuSzhuETdwERvf7nbA3adNdgzt8oQ_Udf9VV5D_rRpzjl_CDHvwmkZJ7tYxir5Qs9yAw" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjpzZXR0bGUuZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0LnN0YXR1cytqc29uIn0", + "signature": "eJ-uBo6M9y2X8NE6jXMzLdtrMEJWYByGLXrTxzsrJqJSVCSSad3fcjnwMMjVxrAF6-8mepveUhMTdD4dOSwBBA" } } diff --git a/examples/verdict-on-challenge.json b/examples/verdict-on-challenge.json index 03da6f8..3ba3271 100644 --- a/examples/verdict-on-challenge.json +++ b/examples/verdict-on-challenge.json @@ -2,15 +2,15 @@ "pact": "0.2", "type": "Verdict", "vtc_id": "vtc_9f2c11", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb", - "challenge_hash": "sha256:2393288d1fba2d966a7c66767935e40b06c2cc4df6cfef12d12d1340c15e5d85", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370cb3023331bc8bfbd924fcbe", + "challenge_hash": "sha256:2ac4e296e79f681446a43e55aef572ff0bfd2093ec2c6795962e67a6e2ff55ff", "outcome": "FAIL", "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", "results_hash": "sha256:7f05a60223e24ca9393e0c241d0448cb5f39e5edb71c117fb7d3d593d5861b40", "evaluated_at": "2026-11-10T09:57:40Z", "signature": { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6YXVkaXQuZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0LnZlcmRpY3QranNvbiJ9", - "signature": "xOsLGEHXP3D9PzyxUjUNhKLLiWH4eNh-vPylihJkTBuPY9Oy7gHwP2_bsRx87yNDrG9MoRbXj1x1Bri7q6UmDg" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjphdWRpdC5leGFtcGxlI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QudmVyZGljdCtqc29uIn0", + "signature": "HRlR3epaYLHMeSJwPZ0Zgexq0Ooz8KEoeCLYCNmlmfrXSp15L46TK8BItIokn8t80MZ_SiJJAyY6VzERCdbUCg" } } diff --git a/examples/verdict.json b/examples/verdict.json index 9cdc1b2..25289e1 100644 --- a/examples/verdict.json +++ b/examples/verdict.json @@ -2,14 +2,14 @@ "pact": "0.2", "type": "Verdict", "vtc_id": "vtc_9f2c11", - "delivery_hash": "sha256:2c0df3c3b39181641300c765a7a94bd6d883a2f8aaaf2f186702504fba242ffb", + "delivery_hash": "sha256:6bcbb831ea27a8754a0df9b44361be12411e45370cb3023331bc8bfbd924fcbe", "outcome": "PASS", "profile": "acceptance", "instrument_hash": "sha256:0bdde1ab6b081d2b4bda580c5393756ae95c10b8351c9c55eb9316416265fc1b", "results_hash": "sha256:28d334e0e0e0771eb5708452612ebcc6fbf848ebaca540214b4f6d2165688a51", "evaluated_at": "2026-11-10T09:14:22Z", "signature": { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6YXVkaXQuZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0LnZlcmRpY3QranNvbiJ9", - "signature": "_Ze8kPxtnvnLGC7UYRvzv13wt6NDO_M9gGDopGbXryCn_E0R6N6Ejn5tTemPpte4SQaOAHG973ovIgjtgbHBAw" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjphdWRpdC5leGFtcGxlI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QudmVyZGljdCtqc29uIn0", + "signature": "tnHxaGqov66ndryyKvfEJ09ANdb6s7QUkLtTvbxq7Y-Pz0BFe8s6itKawEnhXX67YYifEyYYuFUOhLoSB-aBAQ" } } diff --git a/examples/vtc.json b/examples/vtc.json index 7bec125..1302d7f 100644 --- a/examples/vtc.json +++ b/examples/vtc.json @@ -28,7 +28,7 @@ "flow": "verdict-first", "terms": { "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5fc1147743326dabd45bda546dc62", + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf6956e835df2653b99d71d363a68", "parameters": { "seller_bond": "18.00", "verification_fund": "0.50", @@ -48,12 +48,12 @@ }, "signatures": [ { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6YnV5ZXIuZXhhbXBsZTphZ2VudHM6cHJvY3VyZS0xI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QuY29udHJhY3QranNvbiJ9", - "signature": "YsInkxWfby7uxxorS4D9oW9RSEpuVzu8D2WnYWevPFa-5GGZfjyvn6Y7dg66QYGl_QS7ITNNJ-ACxnXsw1mXCw" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjpidXllci5leGFtcGxlOmFnZW50czpwcm9jdXJlLTEjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5jb250cmFjdCtqc29uIn0", + "signature": "XTmTlYvMegh86T8JcpRAxEiAfd_pcPIg3CI73bvRkymDrCalxCDV43Q8yW97WSGhr5KNaibi99K56gLlErsUCw" }, { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6ZGF0YWZvcmdlLmV4YW1wbGU6YWdlbnRzOmV0bC0zI2sxIiwidHlwIjoiYXBwbGljYXRpb24vdm5kLnBhY3QuY29udHJhY3QranNvbiJ9", - "signature": "L8fBf_iz83igBpY5iyvSBnqx9STMjX3fJaXQ48ym39EgzwdvEQhCEYrkYiZEIvYbSeZH8W69nmgeXxhNl8QZAA" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjpkYXRhZm9yZ2UuZXhhbXBsZTphZ2VudHM6ZXRsLTMjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5jb250cmFjdCtqc29uIn0", + "signature": "UBgQmb7JBzwc6bLzi3uJpl2fXhD5sATF-70rmazKrQ__-HYcupeF0J8YN_5KAff0f6W7qtDuaja70wgIpOZ2AA" } ] } diff --git a/examples/well-known/pact-facilitator.json b/examples/well-known/pact-facilitator.json index 3bb7fea..c929ad9 100644 --- a/examples/well-known/pact-facilitator.json +++ b/examples/well-known/pact-facilitator.json @@ -2,6 +2,7 @@ "pact": "0.2", "type": "FacilitatorCapabilities", "facilitator": "did:web:settle.example", + "issued_at": "2026-11-01T09:00:00Z", "settlement_bindings": [ { "id": "https://settle.example/bindings/ledger-1", @@ -18,13 +19,12 @@ "delivery-first" ], "verification_profiles": [ - "acceptance", - "bisection" + "acceptance" ], "terms_profiles": [ { "id": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", - "profile_hash": "sha256:00d71829f6f9192b43b929d0154a6eb409f5fc1147743326dabd45bda546dc62" + "profile_hash": "sha256:9fff6e3f3d99b26eb437a84b9de5b35124ccf6956e835df2653b99d71d363a68" } ], "max_contract_value": { @@ -39,7 +39,7 @@ "outcome": "https://settle.example/pact/v2/outcomes" }, "signature": { - "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6c2V0dGxlLmV4YW1wbGUjazEiLCJ0eXAiOiJhcHBsaWNhdGlvbi92bmQucGFjdC5mYWNpbGl0YXRvcitqc29uIn0", - "signature": "hSax9MOodgYWoYjkTk9OMtj-vIAzCwETfS9gSXQpVUprqF_nNYpKtEo3OS1joYgIFiIFQwXh0hRqdrn2erHoDg" + "protected": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOndlYjpzZXR0bGUuZXhhbXBsZSNrMSIsInR5cCI6ImFwcGxpY2F0aW9uL3ZuZC5wYWN0LmZhY2lsaXRhdG9yK2pzb24ifQ", + "signature": "8gd373Neg1u8nXuae2cQvFbbjwBoovlZ4NT1hs_cdHntSEOJdSfXQXqXahrv8QyLaFGrU1inged3ajGJwrsQBg" } } diff --git a/profiles/bonded-restitution/README.md b/profiles/bonded-restitution/README.md index c48d902..ed557e8 100644 --- a/profiles/bonded-restitution/README.md +++ b/profiles/bonded-restitution/README.md @@ -23,13 +23,13 @@ profile meant for use needs an owner who is. | Member | Type | Meaning | |---|---|---| | `seller_bond` | amount, required | what the Seller posts before performance | -| `verification_fund` | amount, required | what the Buyer posts to pay for checking | -| `cap` | amount, required | the most that leaves the Seller's accounts under the contract | +| `verification_fund` | amount, required | what the Seller posts to pay for checking; the -01 prose never said who posts it and its figure drew it from the Seller | +| `cap` | amount, required | the most that leaves the bond under the contract | | `restitution_basis` | `released` or `price`, required | what the Buyer's loss is measured against | | `remainder_to` | `buyer` or `sink`, optional | where a remaining bond goes; `sink` when absent | | `verifier_fee` | amount, optional | paid from the fund at each Verdict; `0.00` when absent | | `principal_on` | `verdict`, `delivered` or `window-closed`, required | the event at which the price moves to the Seller | -| `assurance` | object, required | `mode` (`certain`, `committed-sample`, `open`) and `q_min` in (0, 1] | +| `assurance` | object, required | `mode` (`certain`, `committed-sample`, `open`), `q_min` in (0, 1], and under `committed-sample` `sample_rate` in (0, 1], the declared fraction of deliveries verified; the draw MUST derive from a seed the Buyer committed before the Delivery was submitted, combined with the Delivery's digest | The -01 release modes map onto the contract's `flow` and this profile's `principal_on`: on-verification is verdict-first with `verdict`; on-window is @@ -67,7 +67,7 @@ nothing. Amounts are computed from the contract and the trace prefix; "released" the sum of `principal` entries emitted so far; "the balance" of an account is what it holds at that point in the list. -- `funded`: buyer to escrow, P, `lock`; seller to bond, B, `bond`; buyer to fund, +- `funded`: buyer to escrow, P, `lock`; seller to bond, B, `bond`; seller to fund, `verification_fund`, `fund`. - `delivered`: if `principal_on` is `delivered`: escrow to seller, the escrow balance, `principal`. @@ -77,21 +77,25 @@ it holds at that point in the list. - `window-closed`: if `principal_on` is `window-closed` and the standing Verdict is not FAIL: escrow to seller, the escrow balance, `principal`. - `terminal` FINAL: escrow to seller, the escrow balance, `principal`; bond to - seller, the bond balance, `return`; fund to buyer, the fund balance, + seller, the bond balance, `return`; fund to seller, the fund balance, `fund-return`. - `terminal` ABANDONED: escrow to buyer, the escrow balance, `reverse`; bond to - seller, the bond balance, `return`; fund to buyer, the fund balance, + seller, the bond balance, `return`; fund to seller, the fund balance, `fund-return`. With the price reversed the Buyer's loss is zero under either basis, so nothing is slashed. - `terminal` SETTLED, in five ranks, each drawing only what remains: (1) escrow to buyer, the escrow balance, `reverse`; (2) if `challenge_upheld`, fund to the Challenger whose Challenge the standing Verdict answers, the lesser of that - Challenge's `costs` and the fund balance, `costs`; (3) bond to buyer, the lesser - of the bond balance, `cap` and the Buyer's loss, `restitution`, the loss being + Challenge's `costs` when stated in the contract's currency (otherwise nothing) + and the fund balance, `costs`; (3) bond to buyer, the lesser + of the bond balance, the cap room and the Buyer's loss, `restitution`, the loss being "released" under basis `released` and P minus the rank-1 entry under basis - `price`; (4) if `challenge_upheld`, bond to that Challenger, the bond balance, - `bounty`; (5) bond to buyer or sink per `remainder_to`, the bond balance, - `remainder`. Then fund to buyer, the fund balance, `fund-return`. + `price`; (4) if `challenge_upheld`, bond to that Challenger, the lesser of the bond + balance and the cap room, `bounty`; (5) bond to buyer or sink per `remainder_to`, + the lesser of the bond balance and the cap room, `remainder`. Then bond to seller, + the bond balance, `return`, which is what the cap kept; then fund to seller, the + fund balance, `fund-return`. The cap room at each rank is `cap` less what the + entries so far have moved out of the bond. Ranks 2 and 4 pay one Challenger, the one whose Challenge the standing Verdict answers. A Challenge that was lapsed, rejected or superseded receives nothing. Rank @@ -100,8 +104,32 @@ for tidiness and fixed no figure. `cap` bounds ranks 3 to 5 together. ## Vectors -`vectors.json` is an array of `{name, contract, trace, transfers}` objects, each a -complete trace with the list the schedule produces for it, generated by +`vectors.json` is an array of `{name, contract, trace, transfers, accounts}` objects, each a +complete trace with the list the schedule produces for it, followed by two +`{name, contract, admission}` objects whose `admission` is `{"admitted": true}` or +`{"refused": }`, generated by `tools/profile.py` and checked against the lists printed in Appendix A.6 of the draft by `tools/validate.py`. A Facilitator reproduces every vector before listing this profile in its capability document (Section 12.1). + +This profile defines no Challenge deposit; a Facilitator that advertises +`challenge_deposit` does not do so under this profile. + +## Departures from -01 + +Three things here are not what the -01 revision said, and all are deliberate: + +- The -01 revision required the Challenger's reward to be non-exclusive, paying + every independent discoverer in full. One bond cannot fund that for two + discoverers, so ranks 2 and 4 pay the one Challenger whose Challenge the + standing Verdict answered. +- On a missed deadline the -01 slashed the bond "to the extent of" the basis, + which under basis `price` meant the full price even though the price had just + been returned. This profile slashes nothing at ABANDONED under either basis, + because the reversed price leaves the Buyer no loss. +- The -01 prose never said who posts the verification fund; its message-flow + figure drew it from the Seller, and this profile does the same. An earlier + draft of this profile had the Buyer post it. + +`cap` bounds ranks 3 to 5 together, and the refusal order at `accepted` is +open mode, then the inequality, then the cap rule, as A.4 states them. diff --git a/profiles/bonded-restitution/parameters.schema.json b/profiles/bonded-restitution/parameters.schema.json index 1eb9cd8..7642af6 100644 --- a/profiles/bonded-restitution/parameters.schema.json +++ b/profiles/bonded-restitution/parameters.schema.json @@ -69,7 +69,23 @@ "maximum": 1 } }, - "additionalProperties": false + "additionalProperties": false, + "if": { + "properties": { + "mode": { + "const": "committed-sample" + } + }, + "required": [ + "mode" + ] + }, + "then": { + "required": [ + "sample_rate" + ] + }, + "$comment": "sample_rate is the declared fraction of deliveries verified under committed-sample; how the draw is made is outside the profile." } }, "additionalProperties": false, diff --git a/profiles/bonded-restitution/vectors.json b/profiles/bonded-restitution/vectors.json index 4ee2c1a..58f5156 100644 --- a/profiles/bonded-restitution/vectors.json +++ b/profiles/bonded-restitution/vectors.json @@ -1,6 +1,6 @@ [ { - "name": "FINAL: PASS, window closes, Figure 1", + "name": "FINAL: PASS, window closes (the verdict-first path)", "contract": { "price": { "amount": "180.00", @@ -82,7 +82,7 @@ }, { "event": 1, - "from": "buyer", + "from": "seller", "to": "fund", "amount": "0.50", "code": "fund" @@ -104,14 +104,21 @@ { "event": 7, "from": "fund", - "to": "buyer", + "to": "seller", "amount": "0.50", "code": "fund-return" } - ] + ], + "accounts": { + "internal": [ + "escrow", + "bond", + "fund" + ] + } }, { - "name": "SETTLED on an upheld Challenge, Figure 5", + "name": "SETTLED on an upheld Challenge (the dispute path)", "contract": { "price": { "amount": "180.00", @@ -208,7 +215,7 @@ }, { "event": 1, - "from": "buyer", + "from": "seller", "to": "fund", "amount": "0.50", "code": "fund" @@ -234,7 +241,14 @@ "amount": "18.00", "code": "restitution" } - ] + ], + "accounts": { + "internal": [ + "escrow", + "bond", + "fund" + ] + } }, { "name": "SETTLED: the Verifier records FAIL", @@ -310,7 +324,7 @@ }, { "event": 1, - "from": "buyer", + "from": "seller", "to": "fund", "amount": "0.50", "code": "fund" @@ -332,11 +346,18 @@ { "event": 5, "from": "fund", - "to": "buyer", + "to": "seller", "amount": "0.50", "code": "fund-return" } - ] + ], + "accounts": { + "internal": [ + "escrow", + "bond", + "fund" + ] + } }, { "name": "ABANDONED: deadline with no Delivery", @@ -404,7 +425,7 @@ }, { "event": 1, - "from": "buyer", + "from": "seller", "to": "fund", "amount": "0.50", "code": "fund" @@ -426,11 +447,18 @@ { "event": 4, "from": "fund", - "to": "buyer", + "to": "seller", "amount": "0.50", "code": "fund-return" } - ] + ], + "accounts": { + "internal": [ + "escrow", + "bond", + "fund" + ] + } }, { "name": "SETTLED on an upheld Challenge, basis price", @@ -530,7 +558,7 @@ }, { "event": 1, - "from": "buyer", + "from": "seller", "to": "fund", "amount": "0.50", "code": "fund" @@ -556,7 +584,14 @@ "amount": "18.00", "code": "restitution" } - ] + ], + "accounts": { + "internal": [ + "escrow", + "bond", + "fund" + ] + } }, { "name": "FINAL after verdict-lapsed", @@ -638,7 +673,7 @@ }, { "event": 1, - "from": "buyer", + "from": "seller", "to": "fund", "amount": "0.50", "code": "fund" @@ -660,11 +695,18 @@ { "event": 7, "from": "fund", - "to": "buyer", + "to": "seller", "amount": "0.50", "code": "fund-return" } - ] + ], + "accounts": { + "internal": [ + "escrow", + "bond", + "fund" + ] + } }, { "name": "FINAL under delivery-first, principal at window-closed", @@ -742,7 +784,7 @@ }, { "event": 1, - "from": "buyer", + "from": "seller", "to": "fund", "amount": "0.50", "code": "fund" @@ -764,10 +806,73 @@ { "event": 6, "from": "fund", - "to": "buyer", + "to": "seller", "amount": "0.50", "code": "fund-return" } - ] + ], + "accounts": { + "internal": [ + "escrow", + "bond", + "fund" + ] + } + }, + { + "name": "admission: refused, seller_bond 17.99 at q_min 0.9091 on 180.00", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "17.99", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 0.9091 + } + } + } + }, + "admission": { + "refused": "assurance-constraint-unsatisfied" + } + }, + { + "name": "admission: admitted, seller_bond 18.00 at q_min 0.9091 on 180.00", + "contract": { + "price": { + "amount": "180.00", + "currency": "USDC" + }, + "flow": "verdict-first", + "terms": { + "profile": "tag:laxsharma79@gmail.com,2026:pact:bonded-restitution", + "parameters": { + "seller_bond": "18.00", + "verification_fund": "0.50", + "cap": "180.00", + "restitution_basis": "released", + "remainder_to": "sink", + "principal_on": "verdict", + "assurance": { + "mode": "certain", + "q_min": 0.9091 + } + } + } + }, + "admission": { + "admitted": true + } } ] diff --git a/schemas/challenge.schema.json b/schemas/challenge.schema.json index 2e9910b..bb1465b 100644 --- a/schemas/challenge.schema.json +++ b/schemas/challenge.schema.json @@ -42,7 +42,7 @@ }, "results_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" }, "failing_checks": { "type": "array", @@ -51,8 +51,22 @@ } } }, - "$comment": "Section 7.3. Profile-conditional members enforced in code.", - "additionalProperties": false + "$comment": "Section 7.3. Profile-conditional members enforced in code. Members beyond those named are the verification profile's (Section 2); for acceptance, instrument_hash and results_hash are required.", + "additionalProperties": true, + "if": { + "properties": { + "profile": { + "const": "acceptance" + } + } + }, + "then": { + "required": [ + "profile", + "instrument_hash", + "results_hash" + ] + } }, "costs": { "$ref": "common.schema.json#/$defs/amount_with_currency", diff --git a/schemas/common.schema.json b/schemas/common.schema.json index 8adbd0f..6113cb5 100644 --- a/schemas/common.schema.json +++ b/schemas/common.schema.json @@ -18,9 +18,8 @@ }, "timestamp": { "type": "string", - "format": "date-time", - "pattern": "Z$", - "$comment": "Section 2: RFC 3339 in UTC with the Z designator." + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$", + "$comment": "Section 2: RFC 3339 in UTC with the Z designator. A pattern rather than format, so that validation does not depend on optional format checkers." }, "tier": { "type": "string", @@ -219,7 +218,7 @@ "type": "boolean" } }, - "$comment": "Section 4.2, Table 1. Which optional members an event carries is stated there and checked in code.", + "$comment": "Section 4.2, Table 2. Which optional members an event carries is stated there and checked in code.", "additionalProperties": false }, "transfer": { diff --git a/schemas/delivery.schema.json b/schemas/delivery.schema.json index 1f571c5..4a1e971 100644 --- a/schemas/delivery.schema.json +++ b/schemas/delivery.schema.json @@ -31,7 +31,7 @@ }, "work_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" }, "input_hash": { "$ref": "common.schema.json#/$defs/hash" @@ -53,11 +53,25 @@ }, "results_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" } }, - "$comment": "Section 6. Tier- and profile-conditional members are enforced in code. delivered_at is gone: the trace carries the Facilitator's time.", - "additionalProperties": false + "$comment": "Section 6. Tier- and profile-conditional members are enforced in code. delivered_at is gone: the trace carries the Facilitator's time. Members beyond those named are the verification profile's (Section 2); for acceptance, instrument_hash and results_hash are required.", + "additionalProperties": true, + "if": { + "properties": { + "profile": { + "const": "acceptance" + } + } + }, + "then": { + "required": [ + "profile", + "instrument_hash", + "results_hash" + ] + } }, "signature": { "$ref": "common.schema.json#/$defs/signature" diff --git a/schemas/facilitator.schema.json b/schemas/facilitator.schema.json index 42fe401..b2d74ab 100644 --- a/schemas/facilitator.schema.json +++ b/schemas/facilitator.schema.json @@ -13,7 +13,8 @@ "verification_profiles", "terms_profiles", "endpoints", - "signature" + "signature", + "issued_at" ], "properties": { "pact": { @@ -31,7 +32,9 @@ "items": { "type": "object", "required": [ - "id" + "id", + "networks", + "assets" ], "properties": { "id": { @@ -110,13 +113,41 @@ "challenge", "outcome" ], - "additionalProperties": { - "type": "string", - "format": "uri" + "additionalProperties": false, + "properties": { + "contract": { + "type": "string", + "pattern": "^https?://[^\\s]+$" + }, + "delivery": { + "type": "string", + "pattern": "^https?://[^\\s]+$" + }, + "verdict": { + "type": "string", + "pattern": "^https?://[^\\s]+$" + }, + "challenge": { + "type": "string", + "pattern": "^https?://[^\\s]+$" + }, + "outcome": { + "type": "string", + "pattern": "^https?://[^\\s]+$" + } } }, "signature": { "$ref": "common.schema.json#/$defs/signature" + }, + "issued_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "retrieval": { + "enum": [ + "parties", + "open" + ] } } } diff --git a/schemas/taskspec.schema.json b/schemas/taskspec.schema.json index 4ca10dc..e1ae436 100644 --- a/schemas/taskspec.schema.json +++ b/schemas/taskspec.schema.json @@ -18,14 +18,14 @@ "properties": { "schema_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" }, "schema_hash": { "$ref": "common.schema.json#/$defs/hash" }, "sample_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" }, "sample_hash": { "$ref": "common.schema.json#/$defs/hash" @@ -53,7 +53,7 @@ }, "schema_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" }, "schema_hash": { "$ref": "common.schema.json#/$defs/hash" @@ -72,18 +72,6 @@ "required": [ "thresholds" ], - "anyOf": [ - { - "required": [ - "harness_uri" - ] - }, - { - "required": [ - "rubric_uri" - ] - } - ], "dependentRequired": { "harness_uri": [ "harness_hash" @@ -95,14 +83,14 @@ "properties": { "harness_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" }, "harness_hash": { "$ref": "common.schema.json#/$defs/hash" }, "rubric_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" }, "rubric_hash": { "$ref": "common.schema.json#/$defs/hash" @@ -112,8 +100,8 @@ "minProperties": 1 } }, - "$comment": "Section 5.2. Members for the attestation and proving tiers are named in prose and not as member names, so a TaskSpec for those tiers does not validate; recorded as a residual.", - "additionalProperties": false + "$comment": "Section 5.2: members are the tier's; harness_uri and rubric_uri, when present, carry sibling hashes (dependentRequired). Not empty.", + "additionalProperties": true }, "constraints": { "type": "object" diff --git a/schemas/vtc.schema.json b/schemas/vtc.schema.json index 94477c4..d470de4 100644 --- a/schemas/vtc.schema.json +++ b/schemas/vtc.schema.json @@ -42,7 +42,7 @@ }, "spec_uri": { "type": "string", - "format": "uri" + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$" }, "deadline": { "$ref": "common.schema.json#/$defs/timestamp" @@ -99,9 +99,6 @@ "type": "integer", "minimum": 1, "$comment": "Section 7.2: bounds the wait for a first Verdict under verdict-first; makes L finite (Section 10.3)." - }, - "arbiter": { - "$ref": "common.schema.json#/$defs/party" } }, "additionalProperties": false diff --git a/tools/README.md b/tools/README.md index b0eead0..ef3df74 100644 --- a/tools/README.md +++ b/tools/README.md @@ -5,13 +5,13 @@ implementation of draft-laxsharma-pact-02. | File | What it is | |---|---| -| `validate.py` | The conformance validator: 103 checks over the committed examples and the profile bundle. Schemas, canonicalization, every digest the objects commit to, every signature (with the public keys in `examples/keys/`), the profile's vectors and the two lists printed in Appendix A.6, the RFC 9162 tree, and every vector of Section 14.3 run through the reference Facilitator. | +| `validate.py` | The conformance validator: 107 checks over the committed examples and the profile bundle. Schemas, canonicalization, every digest the objects commit to, every signature (with the public keys in `examples/keys/`), the profile's vectors and the two lists printed in Appendix A.6, the RFC 9162 tree, and every vector of Section 14.3, the ones that concern a Facilitator through the reference Facilitator and the rest at the schema, canonicalizer or profile level. | | `mint_examples.py` | Mints `examples/` from public seeds. Ed25519 is deterministic, so anyone who runs it gets the same bytes and the same digests the draft prints; `--check` diffs against disk. | | `pactcore.py` | RFC 8785 canonicalization including ECMAScript number formatting, digests, JWS signing and verification over the transmitted protected header, identifier normalization, sorted signature sets, low-S ECDSA, the manifest digest, and the RFC 9162 Merkle tree. | -| `profile.py` | The example terms profile of Appendix A, `bonded-restitution`: its admission rule, its schedule over a trace, its two invariants, and the generator of `profiles/bonded-restitution/vectors.json`. The only file in this directory that knows what an amount is for. | +| `profile.py` | The example terms profile of Appendix A, `bonded-restitution`: its admission rule, its schedule over a trace, its two invariants, and the generator of `profiles/bonded-restitution/vectors.json` (seven traces and two admission vectors). The only file in this directory that knows what an amount is for. | | `facilitator.py` | A reference Facilitator: the operations of Section 13 over a signed event trace, the state machine of Section 4 including verdict-first and delivery-first flows, lapses, contract trees within one venue, a signed Status on every accepted request, one signed Outcome Record per terminal contract, and RFC 9457 refusals that name the section or the profile section. `--rules` prints what it enforces and what it chose; `--problems` prints every problem type it emits. | | `agents.py` | Buyer, Seller, Verifier and Challenger clients that check what they are handed. | -| `measure.py` | Drives ten paths through the terminal states on a clock the harness advances, checks every Outcome Record the way a party would, reproduces the seven profile vectors, exercises 40 refusals and 5 acceptances, and reports what each path cost on the wire. | +| `measure.py` | Drives ten paths through the terminal states on a clock the harness advances, checks every Outcome Record the way a party would, reproduces the seven trace vectors of the profile bundle, exercises 45 refusals and 5 acceptances, and reports what each path cost on the wire. | ``` pip install jsonschema referencing cryptography @@ -22,7 +22,7 @@ python3 tools/facilitator.py --rules `validate.py` runs without `cryptography`, printing `[skip]` for the signature checks and the vectors that go through the Facilitator, which need it. The -count of 103 is with all three packages installed, and that is what CI runs. +count of 107 is with all three packages installed, and that is what CI runs. The Facilitator refuses to start without `jsonschema`, because Section 14.2 makes schema conformance a MUST and a Facilitator that skips it is not one. @@ -59,16 +59,18 @@ to surface. 1. **funded is recorded in the same call as accepted.** There is no rail, so nothing can be observed, and the Status of a 201 already reads FUNDED. 2. **Retrieval is open on the loopback interface.** Section 17.12 restricts it - by default and leaves the mechanism to the deployment. This process emits - no `retrieval-restricted`; a deployment puts authentication in front of it. + by default and leaves the mechanism to the deployment; the capability + document says `retrieval: open`. This process emits no + `retrieval-restricted`; a deployment puts authentication in front of it. 3. **Trees within one venue.** A registered child that lives in this process is noticed when it reaches a terminal state; any other child's Outcome Record must be supplied by POST. No cross-venue GET is made. -4. **Whole cents.** Amounts are settled by the profile's own decimal arithmetic - and a price with more than two decimals is refused as `amount-invalid`. -5. **Nothing else.** Everything the -01 tools had to choose about value, the - bond on ABANDONED, the bounty, rank 2, the net reading of rank 3, is now the - profile's, and Appendix A states each one. +4. **Whole cents.** The one settlement binding this Facilitator advertises + carries cents, so a price with more than two decimals is refused as + `amount-invalid`, which Section 14.2 provides for. +5. **Not implemented, and refused rather than faked:** the `no-window` flow, + challenge deposits, network key resolution, any payment rail, and the + committed-sample draw. ## Measured on 16 September 2026 @@ -79,42 +81,49 @@ same laptop; the order of magnitude is the result. | Path | Exchanges | Request / response bytes | What the profile moved at the end | |---|---|---|---| -| FINAL: verdict-first, PASS, window closes | 4 | 3,301 / 4,636 | principal 180.00 to the seller, bond and fund returned | -| SETTLED: the Verifier records FAIL | 4 | 3,301 / 4,549 | escrow 180.00 back to the buyer, bond 18.00 to the sink | -| ABANDONED: deadline, no Delivery | 2 | 1,735 / 2,320 | escrow back, bond and fund returned | -| SETTLED: PASS overturned by a Challenge | 6 | 4,778 / 8,212 | principal 180.00 stays, restitution 18.00 to the buyer, costs 0.50 to the challenger | -| SETTLED: the same with `restitution_basis` price | 6 | 4,775 / 8,212 | the same amounts (cap 180.00, bond 18.00) | -| FINAL after verdict-lapsed | 4 | 2,646 / 4,372 | principal 180.00 | -| FINAL under delivery-first, principal at window-closed | 3 | 2,653 / 3,487 | principal 180.00 | -| FINAL after dispute-lapsed, the PASS stands | 5 | 4,032 / 6,250 | principal 180.00, costs unpaid | -| FINAL with one child in the same venue | 9 | 8,631 / 11,372 | principal 180.00; `children_merkle_root` recomputes | -| FINAL with one child unresolved at L(child) | 6 | 5,184 / 7,514 | principal 180.00; the root is MTH of the empty list | - -The first seven rows reproduce the seven vectors in -`profiles/bonded-restitution/vectors.json` exactly. Every Outcome Record was +| FINAL (verdict-first, PASS, window closes) | 4 | 3,313 / 4,649 | principal 180.00 | +| SETTLED (the Verifier records FAIL) | 4 | 3,313 / 4,562 | reverse 180.00, remainder 18.00 | +| ABANDONED (deadline, no Delivery) | 2 | 1,741 / 2,327 | reverse 180.00 | +| SETTLED (PASS overturned by a Challenge) | 6 | 4,796 / 8,230 | principal 180.00, costs 0.50, restitution 18.00 | +| SETTLED (overturned, restitution_basis price) | 6 | 4,793 / 8,230 | principal 180.00, costs 0.50, restitution 18.00 | +| FINAL after verdict-lapsed | 4 | 2,655 / 4,385 | principal 180.00 | +| FINAL under delivery-first, principal at window-closed | 3 | 2,662 / 3,497 | principal 180.00 | +| FINAL after dispute-lapsed (the PASS stands) | 5 | 4,047 / 6,266 | principal 180.00 | +| FINAL with one child, in-venue (Merkle root) | 9 | 8,661 / 11,401 | principal 180.00 | +| FINAL with one child unresolved at L(child) | 6 | 5,202 / 7,533 | principal 180.00 | + +The first seven rows reproduce the seven trace vectors in +`profiles/bonded-restitution/vectors.json` exactly; the bundle's two admission +vectors are replayed by `validate.py`. Every Outcome Record was checked the way a party would check it: the Facilitator's signature verifies, the transfer list recomputes from the trace with the named profile, no account is overdrawn and every internal account closes at zero, and every Status received along the way is a prefix of the final trace. -The messages of the first and fourth rows, request bytes and response bytes: +The messages of the first and fourth rows, request bytes / response bytes: ``` -FINAL SETTLED, overturned -POST contracts 201 1735 / 639 POST contracts 201 1735 / 639 -POST deliveries 202 911 / 775 POST deliveries 202 911 / 775 -POST verdicts 201 655 / 1053 POST verdicts 201 655 / 1053 -GET contract 200 0 / 2169 POST challenges 202 731 / 1263 - POST verdicts 201 746 / 1766 - GET contract 200 0 / 2716 +FINAL (verdict-first, PASS, window closes) +POST contracts 201 1741 / 642 +POST deliveries 202 914 / 778 +POST verdicts 201 658 / 1056 +GET vtc_m0001 200 0 / 2173 + +SETTLED (PASS overturned by a Challenge) +POST contracts 201 1741 / 642 +POST deliveries 202 914 / 778 +POST verdicts 201 658 / 1056 +POST challenges 202 734 / 1266 +POST verdicts 201 749 / 1769 +GET vtc_m0004 200 0 / 2719 ``` -Each lifecycle completes in 20 to 85 ms, most of it schema validation of the +Each lifecycle completes in 18 to 123 ms, most of it schema validation of the posted objects (1.7 ms per contract). Per call, medians: canonicalize a -contract 217 us; canonicalize and digest 227 us; sign a Verdict including -canonicalization 151 us; verify a Verdict end to end 263 us; normalize an -identifier 1.9 us; the profile's schedule over the nine-entry overturned trace -27 us; an RFC 9162 root over 2, 8 and 64 leaves 6, 29 and 253 us. +contract 213 us; canonicalize and digest 223 us; sign a Verdict including +canonicalization 146 us; verify a Verdict end to end 272 us; normalize an +identifier 1.8 us; the profile's schedule over the nine-entry overturned trace +26 us; an RFC 9162 root over 2, 8 and 64 leaves 6, 28 and 236 us. Canonicalization is four times slower than the v0.1.0 figure because it no longer delegates to `json.dumps`; see the third correction below. diff --git a/tools/agents.py b/tools/agents.py index e254a95..a9bfde7 100644 --- a/tools/agents.py +++ b/tools/agents.py @@ -78,8 +78,8 @@ def propose(self, vtc): return self.post(f"{BASE_PATH}/contracts", vtc, MEDIA_CO def status(self, vid): return self.get(f"{BASE_PATH}/contracts/{vid}") def register_child(self, parent_id, child_vtc): return self.post(f"{BASE_PATH}/contracts/{parent_id}/children", child_vtc, MEDIA_CONTRACT) - def supply_child_outcome(self, parent_id, child_id, record): - return self.post(f"{BASE_PATH}/contracts/{parent_id}/children/{child_id}", record, MEDIA_OUTCOME) + def supply_child_outcome(self, parent_id, child_hash, record): + return self.post(f"{BASE_PATH}/contracts/{parent_id}/children/{child_hash}", record, MEDIA_OUTCOME) def deliver(self, d): return self.post(f"{BASE_PATH}/deliveries", d, MEDIA_DELIVERY) def verdict(self, v): return self.post(f"{BASE_PATH}/verdicts", v, MEDIA_VERDICT) def challenge(self, c): return self.post(f"{BASE_PATH}/challenges", c, MEDIA_CHALLENGE) @@ -98,7 +98,7 @@ def sign_into(self, obj: dict, typ: str, array: bool = False) -> dict: def make_party(did: str, resolver: pc.KeyResolver, client: Client, - alg: str = "EdDSA") -> Party: + alg: str = "Ed25519") -> Party: key = resolver.register(pc.Key.generate(f"{did}#key-1", alg)) return Party(did=did, key=key, client=client) diff --git a/tools/facilitator.py b/tools/facilitator.py index 7b10c19..c857c00 100644 --- a/tools/facilitator.py +++ b/tools/facilitator.py @@ -29,6 +29,7 @@ import argparse import json +import math import pathlib import re import threading @@ -55,10 +56,11 @@ Section 6 a Delivery is accepted in FUNDED only, signed by the Seller, with evidence conformant to the verification profile; a nonconformant one is refused and recorded nowhere Section 7.1 flows this Facilitator does not advertise are refused; the window is never extended -Section 7.2 a Verdict signer is the named verifier, or independent by Section 9.1; never the - Facilitator; never the Challenger it answers; challenge_hash names a pending - Challenge exactly when the contract is DISPUTED; verdict-lapsed after - max_verdict_seconds +Section 7.2 a Verdict is accepted in DELIVERED (verdict-first) or WINDOW_OPEN (delivery-first) + only while none stands, and in DISPUTED only answering a pending Challenge; its + signer is the named verifier, or independent by Section 9.1, never the Facilitator; + verdict-lapsed after max_verdict_seconds +Section 7.3 a Challenger's own Verdict on its Challenge is refused unless it is the named verifier Section 7.3 a Challenge is accepted before closes_at only, with a conformant proof, never from the Seller, always from the Buyer if otherwise valid Section 7.4 a pending Challenge lapses after max_dispute_seconds and the earlier Verdict stands @@ -121,8 +123,8 @@ "retrieval-restricted": (403, "17.12"), "schema-invalid": (422, "14.2"), "settlement-unsupported": (422, "13.1"), - "signature-invalid": (401, "14.1"), - "signature-missing": (401, "14.2"), + "signature-invalid": (400, "14.1"), + "signature-missing": (400, "14.2"), "signatures-unordered": (422, "14.1"), "terms-parameters-invalid": (422, "5.3"), "terms-unsupported": (422, "5.3"), @@ -222,8 +224,10 @@ def latest_finality(vtc: dict) -> float: def _sig_kind(why: str) -> str: if why.startswith("algorithm"): return "algorithm-not-permitted" - if "no signature" in why: + if "no signature" in why or "carries no signatures" in why: return "signature-missing" + if "more than one signature" in why: + return "unexpected-signer" return "signature-invalid" @@ -300,6 +304,7 @@ def __init__(self, identity: str, key: pc.Key, resolver: pc.KeyResolver, self.seen: dict[tuple[str, str], str] = {} # (kind, digest) -> vtc_id self.lock = threading.RLock() self.message_count = 0 + self._op: float | None = None # one clock reading per operation (Section 4.2) # What this Facilitator advertises, Section 8. Contracts outside it are # refused at propose rather than accepted and stranded. self.settlement_bindings = [ @@ -332,8 +337,16 @@ def _replay(self, kind: str, obj: dict) -> tuple[int, dict] | None: def _profile(self, c: Contract) -> terms.BondedRestitution: return self.profiles[(c.vtc["terms"]["profile"], c.vtc["terms"]["profile_hash"])] + def _begin(self) -> float: + # whole seconds: what the trace prints is what the comparisons use (Section 4.2) + self._op = float(math.floor(self.now())) + return self._op + + def _op_now(self) -> float: + return self._op if self._op is not None else float(math.floor(self.now())) + def _record(self, c: Contract, event: str, **members: Any) -> dict: - entry = {"event": event, "at": iso(self.now())} + entry = {"event": event, "at": iso(self._op_now())} entry.update({k: v for k, v in members.items() if v is not None}) c.trace.append(entry) # Section 5.3: the profile's schedule is invoked with the event and @@ -345,7 +358,7 @@ def _status(self, c: Contract) -> dict: st = { "pact": "0.2", "type": "ContractStatus", "vtc_id": c.id, "vtc_hash": c.digest(), "state": c.state, - "trace": list(c.trace), "issued_at": iso(self.now()), + "trace": list(c.trace), "issued_at": iso(self._op_now()), } st["signature"] = pc.sign(st, self.key, MEDIA_STATUS) self.schemas.check(st, "status.schema.json") @@ -359,13 +372,13 @@ def _record_keys(self, c: Contract, obj: dict) -> None: if key is not None and kid not in c.keys: c.keys[kid] = pc.b64u(pc.public_bytes(key)) - # -- clock-driven edges, Table 1 --------------------------------------- + # -- clock-driven edges, Table 2 --------------------------------------- def _tick(self, c: Contract) -> None: """Advance the contract along every clock-driven edge that is due, in - the order of Table 1, until nothing more is due.""" + the order of Table 2, until nothing more is due.""" for _ in range(16): before = len(c.trace) - now = self.now() + now = self._op_now() if c.state in ("ACCEPTED", "FUNDED") and now >= c.deadline: self._record(c, "deadline-passed") c.state = "AWAITING_CHILDREN" @@ -395,7 +408,7 @@ def _tick(self, c: Contract) -> None: def _children_tick(self, c: Contract) -> None: if c.state in TERMINAL: return - now = self.now() + now = self._op_now() for child in c.children.values(): if child.record is not None or child.unresolved: continue @@ -415,7 +428,7 @@ def _children_final_if_ready(self, c: Contract) -> None: self._terminal(c) def _open_window(self, c: Contract) -> None: - closes = self.now() + c.vtc["challenge"]["window_seconds"] + closes = self._op_now() + c.vtc["challenge"]["window_seconds"] c.window_closes_at = closes self._record(c, "window-opened", closes_at=iso(closes)) c.state = "WINDOW_OPEN" @@ -429,12 +442,16 @@ def _terminal(self, c: Contract) -> None: state, upheld = "SETTLED", "answers" in st else: state, upheld = "FINAL", False + n_trace, n_transfers, prior = len(c.trace), len(c.transfers), c.state self._record(c, "terminal", state=state, challenge_upheld=upheld) - c.state = state prof = self._profile(c) ok, why = prof.check(c.vtc, c.transfers, terminal=True) - if not ok: # the profile broke its own arithmetic; never sign that + if not ok: # the profile broke its own arithmetic; never sign that, and record nothing + del c.trace[n_trace:] + del c.transfers[n_transfers:] + c.state = prior raise Refuse("internal-error", f"terms result fails an invariant: {why}") + c.state = state record = { "pact": "0.2", "type": "OutcomeRecord", "vtc_id": c.id, "vtc_hash": c.digest(), @@ -460,6 +477,8 @@ def _terminal(self, c: Contract) -> None: def _check_contract(self, vtc: dict) -> None: """Everything Section 14 asks of a contract on its own, used both at propose and at child registration.""" + if not vtc.get("signatures"): + raise Refuse("signature-missing", "the contract carries no signatures") self.schemas.check(vtc, "vtc.schema.json") parties = vtc["parties"] buyer, seller = parties["buyer"], parties["seller"] @@ -487,6 +506,7 @@ def _check_contract(self, vtc: dict) -> None: def propose(self, vtc: dict) -> tuple[int, dict]: with self.lock: + self._begin() hit = self._replay("contract", vtc) if hit is not None: return hit @@ -519,6 +539,10 @@ def propose(self, vtc: dict) -> tuple[int, dict]: "advertises in its capability document", settlement=price_m["settlement"], network=price_m["network"], currency=price_m["currency"]) + if price_m["currency"] != self.max_contract_value["currency"]: + raise Refuse("settlement-unsupported", + "price is not stated in the currency of this Facilitator's max_contract_value", + currency=price_m["currency"]) if pc.cents(price_m["amount"]) > pc.cents(self.max_contract_value["amount"]): raise Refuse("settlement-unsupported", f"price exceeds this Facilitator's max_contract_value " @@ -542,12 +566,12 @@ def propose(self, vtc: dict) -> tuple[int, dict]: if err: raise Refuse("terms-parameters-invalid", err, profile=t["profile"]) deadline = parse_rfc3339(vtc["task"]["deadline"]) - if deadline <= self.now(): + if deadline <= self._op_now(): raise Refuse("deadline-invalid", "task.deadline is already past on this Facilitator's clock") prof.admit(vtc) # raises terms.ProfileRefusal, reported per Section 13.3 - c = Contract(vtc=vtc, created_at=self.now(), deadline=deadline) + c = Contract(vtc=vtc, created_at=self._op_now(), deadline=deadline) self.contracts[c.id] = c self._record_keys(c, vtc) self._remember("contract", vtc, c.id) @@ -561,12 +585,14 @@ def propose(self, vtc: dict) -> tuple[int, dict]: # -- Retrieve, Section 11 and 12 -------------------------------------- def get_status(self, vid: str) -> tuple[int, dict]: with self.lock: + self._begin() c = self._contract(vid) self._tick(c) return 200, self._status(c) def get_outcome(self, vid: str) -> tuple[int, dict]: with self.lock: + self._begin() c = self._contract(vid) self._tick(c) if c.outcome is None: @@ -583,11 +609,14 @@ def _contract(self, vid: str) -> Contract: # -- Submit Delivery, Section 6 --------------------------------------- def submit_delivery(self, dlv: dict) -> tuple[int, dict]: with self.lock: + self._begin() hit = self._replay("delivery", dlv) if hit is not None: return hit - for member in ("vtc_id", "vtc_hash", "signature"): - if not isinstance(dlv.get(member), (str, dict)): + if "signature" not in dlv: + raise Refuse("signature-missing", "the Delivery carries no signature") + for member in ("vtc_id", "vtc_hash"): + if not isinstance(dlv.get(member), str): raise Refuse("schema-invalid", f"the Delivery lacks {member}") c = self._contract(dlv["vtc_id"]) self._tick(c) @@ -619,7 +648,7 @@ def submit_delivery(self, dlv: dict) -> tuple[int, dict]: raise Refuse("evidence-nonconformant", reason, state=c.state) c.delivery = dlv - c.delivered_at = self.now() + c.delivered_at = self._op_now() self._record_keys(c, dlv) self._remember("delivery", dlv, c.id) self._record(c, "delivered", object=pc.digest_over(dlv)) @@ -644,7 +673,7 @@ def _delivery_nonconformance(self, c: Contract, dlv: dict) -> str | None: if "results_hash" not in ev: return "the acceptance profile requires results_hash in the evidence" if ver["tier"] == "T0-reexec" and "input_hash" not in dlv: - return "input_hash is required for a tier whose fraud proof re-executes (Section 6)" + return "input_hash is required for a tier whose proof of nonconformance re-executes (Section 6)" return None def _delivery_digest(self, c: Contract) -> str: @@ -656,9 +685,12 @@ def _delivery_digest(self, c: Contract) -> str: # -- Record Verdict, Section 7.2 -------------------------------------- def record_verdict(self, verdict: dict) -> tuple[int, dict]: with self.lock: + self._begin() hit = self._replay("verdict", verdict) if hit is not None: return hit + if "signature" not in verdict: + raise Refuse("signature-missing", "the Verdict carries no signature") self.schemas.check(verdict, "verdict.schema.json") c = self._contract(verdict["vtc_id"]) self._tick(c) @@ -668,10 +700,15 @@ def record_verdict(self, verdict: dict) -> tuple[int, dict]: "delivery_hash does not commit to the recorded Delivery, " "including its signature", expected=recorded, received=verdict["delivery_hash"]) - if c.state not in ("DELIVERED", "WINDOW_OPEN", "DISPUTED") or c.flow == "no-window": + answers = verdict.get("challenge_hash") + admissible = ((c.state == "DELIVERED" and c.flow == "verdict-first" and c.standing is None) + or (c.state == "WINDOW_OPEN" and c.flow == "delivery-first" and c.standing is None) + or c.state == "DISPUTED") + if not admissible: raise Refuse("wrong-state", - f"contract {c.id} is {c.state} under {c.flow}; Table 1 lists no " - f"verdict entry for it") + f"contract {c.id} is {c.state} under {c.flow}" + f"{' with a Verdict standing' if c.standing is not None else ''}; " + f"Table 2 lists no verdict entry for it") ver = c.vtc["verification"] if verdict["instrument_hash"] != ver["criteria_hash"]: raise Refuse("verdict-nonconformant", @@ -681,7 +718,6 @@ def record_verdict(self, verdict: dict) -> tuple[int, dict]: raise Refuse("verdict-nonconformant", f"profile {verdict['profile']!r} is not the contract's " f"{ver['profile']!r}") - answers = verdict.get("challenge_hash") if c.state == "DISPUTED": if answers is None: raise Refuse("verdict-nonconformant", @@ -740,7 +776,7 @@ def _check_verdict_signer(self, c: Contract, kids: list[str], answers: str | Non raise Refuse("verifier-not-independent", f"the Verdict is signed by the contract's {label}", signer=kid) - if answers is not None: + if answers is not None and not named: for ck in pc.signer_kids(c.challenges[answers]): if pc.same_party(ck, kid): raise Refuse("verifier-not-independent", @@ -750,22 +786,25 @@ def _check_verdict_signer(self, c: Contract, kids: list[str], answers: str | Non # -- Open Challenge, Section 7.3 -------------------------------------- def open_challenge(self, ch: dict) -> tuple[int, dict]: with self.lock: + self._begin() hit = self._replay("challenge", ch) if hit is not None: return hit + if "signature" not in ch: + raise Refuse("signature-missing", "the Challenge carries no signature") self.schemas.check(ch, "challenge.schema.json") c = self._contract(ch["vtc_id"]) self._tick(c) + if c.state not in ("WINDOW_OPEN", "DISPUTED"): + raise Refuse("challenge-window-closed", + f"no challenge window is open: contract {c.id} is {c.state}", + state=c.state) recorded = self._delivery_digest(c) if ch["delivery_hash"] != recorded: raise Refuse("object-conflict", "delivery_hash does not commit to the recorded Delivery", expected=recorded, received=ch["delivery_hash"]) - if c.state not in ("WINDOW_OPEN", "DISPUTED"): - raise Refuse("challenge-window-closed", - f"no challenge window is open: contract {c.id} is {c.state}", - state=c.state) - if c.window_closes_at is None or self.now() >= c.window_closes_at: + if c.window_closes_at is None or self._op_now() >= c.window_closes_at: raise Refuse("challenge-window-closed", "the challenge window has closed", closes_at=iso(c.window_closes_at or 0)) proof = ch["proof"] @@ -787,12 +826,12 @@ def open_challenge(self, ch: dict) -> tuple[int, dict]: kids = pc.signer_kids(ch) if any(pc.kid_covers(k, c.seller) for k in kids): raise Refuse("unexpected-signer", "a performer's statement against its own " - "Delivery is not a fraud proof (Section 7.3)", signer=kids[0]) - # A Challenge is a fraud proof submitted for evaluation, not a + "Delivery is not a proof of nonconformance (Section 7.3)", signer=kids[0]) + # A Challenge is a proof of nonconformance submitted for evaluation, not a # finding. The Buyer is admissible; nothing here checks who else is. digest = pc.digest_over(ch) c.challenges[digest] = ch - c.challenge_at[digest] = self.now() + c.challenge_at[digest] = self._op_now() c.pending.append(digest) self._record_keys(c, ch) self._remember("challenge", ch, c.id) @@ -803,6 +842,7 @@ def open_challenge(self, ch: dict) -> tuple[int, dict]: # -- Contract trees, Section 10 --------------------------------------- def register_child(self, parent_id: str, child: dict) -> tuple[int, dict]: with self.lock: + self._begin() p = self._contract(parent_id) self._tick(p) hit = self.seen.get(("child", pc.digest_over(child))) @@ -842,15 +882,20 @@ def register_child(self, parent_id: str, child: dict) -> tuple[int, dict]: self._tick(p) return 201, self._status(p) - def supply_child_outcome(self, parent_id: str, child_id: str, record: dict) -> tuple[int, dict]: + def supply_child_outcome(self, parent_id: str, child_hash: str, record: dict) -> tuple[int, dict]: with self.lock: + self._begin() p = self._contract(parent_id) self._tick(p) - child = next((ch for ch in p.children.values() if ch.vtc["id"] == child_id), None) + child = next((ch for ch in p.children.values() if ch.digest == child_hash), None) if child is None: - raise Refuse("unknown-contract", f"no registered child {child_id} of {parent_id}") - if child.record is not None: + raise Refuse("unknown-contract", f"no registered child with digest {child_hash} under {parent_id}") + if child.record is not None: # a replay of an accepted record is the existing resource (Section 13.2) + if pc.digest_over(record) != pc.digest_over(child.record): + raise Refuse("object-conflict", "a different Outcome Record is already held for this child") return 200, self._status(p) + if p.state in TERMINAL: + raise Refuse("wrong-state", "the parent has reached a terminal state; child records are no longer accepted (Table 2)", state=p.state) self.schemas.check(record, "outcome.schema.json") if record["vtc_hash"] != child.digest: raise Refuse("child-outcome-invalid", @@ -871,11 +916,13 @@ def capability_document(self) -> dict: "pact": "0.2", "type": "FacilitatorCapabilities", "facilitator": self.identity, + "issued_at": iso(self._op_now()), "settlement_bindings": self.settlement_bindings, "flows": self.flows, "verification_profiles": self.verification_profiles, "terms_profiles": [{"id": pid, "profile_hash": ph} for (pid, ph) in self.profiles], "max_contract_value": self.max_contract_value, + "retrieval": "open", # CHOICES C2 "endpoints": { "contract": self.base_url + "/pact/v2/contracts", "delivery": self.base_url + "/pact/v2/deliveries", @@ -981,11 +1028,11 @@ def do_POST(self) -> None: def run() -> None: m = re.match(r"^/pact/v2/contracts/([^/]+)/children(?:/([^/]+))?$", self.path) if m: - parent_id, child_id = m.groups() - if child_id is None: + parent_id, child_hash = m.groups() + if child_hash is None: status, body = self.fac.register_child(parent_id, self._body()) else: - status, body = self.fac.supply_child_outcome(parent_id, child_id, self._body()) + status, body = self.fac.supply_child_outcome(parent_id, child_hash, self._body()) self._send(status, body, MEDIA_STATUS) return m = re.match(r"^/pact/v2/(contracts|deliveries|verdicts|challenges)$", self.path) diff --git a/tools/measure.py b/tools/measure.py index be6cf1e..d1253a9 100644 --- a/tools/measure.py +++ b/tools/measure.py @@ -277,7 +277,7 @@ def scenario_tree_unresolved(h: Harness): ("FINAL (verdict-first, PASS, window closes)", scenario_final, "FINAL: PASS"), ("SETTLED (the Verifier records FAIL)", scenario_settled, "SETTLED: the Verifier"), ("ABANDONED (deadline, no Delivery)", scenario_abandoned, "ABANDONED"), - ("SETTLED (PASS overturned by a Challenge)", scenario_overturned, "SETTLED on an upheld Challenge, Figure"), + ("SETTLED (PASS overturned by a Challenge)", scenario_overturned, "SETTLED on an upheld Challenge (the"), ("SETTLED (overturned, restitution_basis price)", lambda h: scenario_overturned(h, restitution_basis="price"), "SETTLED on an upheld Challenge, basis"), ("FINAL after verdict-lapsed", scenario_verdict_lapsed, "FINAL after verdict-lapsed"), @@ -347,6 +347,19 @@ def alg_none_contract(): pc.sign(vtc, h.seller.key, agents.MEDIA_CONTRACT)]) return c.propose(vtc) + def jwk_in_header(): + vtc = h.contract() + entry = vtc["signatures"][0] + hdr = json.loads(pc.b64u_decode(entry["protected"])) + hdr["jwk"] = {"kty": "OKP", "crv": "Ed25519", "x": pc.b64u(pc.public_bytes(h.buyer.key))} + entry["protected"] = pc.b64u(json.dumps(hdr, separators=(",", ":")).encode()) + return c.propose(vtc) + + def second_verdict_no_challenge(): + vtc, d, v = h.window_open() + again = agents.make_verdict(vtc, d, h.verifier, "FAIL") + return c.verdict(again) + def third_party_signature(): vtc = h.contract(sign=False) return c.propose(agents.cosign(vtc, h.buyer, h.seller, h.watch)) @@ -472,6 +485,32 @@ def child_timing(): child = h.contract(buyer=h.seller, seller=h.sub, days=1, parent=link) return c.register_child(parent["id"], child) + def child_outcome_after_terminal(): + parent = h.contract(days=7) + h.call(c.propose, parent, 201) + link = {"vtc_id": parent["id"], "vtc_hash": pc.digest_over(parent), + "facilitator": h.fac.identity} + child = h.contract(buyer=h.seller, seller=h.sub, days=1, parent=link, + facilitator=h.other.did) + h.call(lambda x: c.register_child(parent["id"], x), child, 201) + dp = agents.make_delivery(parent, h.seller, b"parent work 3", b"parent results") + h.call(c.deliver, dp, 202) + h.call(c.verdict, agents.make_verdict(parent, dp, h.verifier, "PASS"), 201) + h.clock.advance(WINDOW + 1) + h.clock.advance(F.latest_finality(child) - h.clock.t + 1) + h.outcome(parent) # FINAL, the child recorded unresolved + record = { + "pact": "0.2", "type": "OutcomeRecord", "vtc_id": child["id"], + "vtc_hash": pc.digest_over(child), "parties": child["parties"], + "outcome": {"state": "FINAL", "challenge_upheld": False}, + "trace": [{"event": "accepted", "at": iso(h.clock.t), "object": pc.digest_over(child)}, + {"event": "terminal", "at": iso(h.clock.t), "state": "FINAL", + "challenge_upheld": False}], + "terms_result": {"profile": terms.ID, "profile_hash": h.profile.profile_hash, + "currency": "USDC", "transfers": []}, + } + return c.supply_child_outcome(parent["id"], pc.digest_over(child), record) + def child_outcome_wrong_hash(): parent = h.funded() link = {"vtc_id": parent["id"], "vtc_hash": pc.digest_over(parent), @@ -490,7 +529,7 @@ def child_outcome_wrong_hash(): "currency": "USDC", "transfers": []}, } record["signatures"] = [pc.sign(record, h.other.key, agents.MEDIA_OUTCOME)] - return c.supply_child_outcome(parent["id"], child["id"], record) + return c.supply_child_outcome(parent["id"], pc.digest_over(child), record) return [ # propose @@ -523,6 +562,12 @@ def child_outcome_wrong_hash(): ("terms parameters fail the profile schema", 422, "terms-parameters-invalid", False, lambda: c.propose(h.contract(bond="eighteen"))), ("contract signed with alg none", 400, "algorithm-not-permitted", False, alg_none_contract), + ("contract with no signatures", 400, "signature-missing", False, + lambda: c.propose(h.contract(sign=False))), + ("contract whose protected header embeds a jwk", 400, "signature-invalid", False, jwk_in_header), + ("second Verdict inside the window with no Challenge", 409, "wrong-state", False, second_verdict_no_challenge), + ("Challenge before any Delivery", 409, "challenge-window-closed", False, + lambda: c.challenge(agents.make_challenge(h.funded(), agents.make_delivery(h.contract(), h.seller, b"x", b"y"), h.watch, ["x"]))), ("a third party co-signs the contract", 422, "unexpected-signer", False, third_party_signature), ("signature set out of order", 422, "signatures-unordered", False, unsorted), ("same id, different contract", 409, "object-conflict", False, altered_same_id), @@ -541,7 +586,7 @@ def child_outcome_wrong_hash(): ("Verdict over another instrument", 422, "verdict-nonconformant", False, verdict_other_instrument), ("Verdict whose delivery_hash omits the signature", 422, "verdict-nonconformant", False, verdict_wrong_delivery_hash), - ("Verdict signed with the Delivery typ", 401, "signature-invalid", False, verdict_with_delivery_typ), + ("Verdict signed with the Delivery typ", 400, "signature-invalid", False, verdict_with_delivery_typ), ("Verdict in DISPUTED without challenge_hash", 422, "verdict-nonconformant", False, verdict_in_disputed_without_challenge_hash), ("Verdict by the Challenger it answers", 422, "verifier-not-independent", False, @@ -558,6 +603,8 @@ def child_outcome_wrong_hash(): ("child with L(child) not before L(parent)", 422, "finality-ordering-violation", False, child_timing), ("child Outcome Record over the wrong contract", 422, "child-outcome-invalid", False, child_outcome_wrong_hash), + ("child Outcome Record supplied after the parent's terminal entry", 409, "wrong-state", False, + child_outcome_after_terminal), # retrieval ("GET an unknown contract", 404, "unknown-contract", False, lambda: c.status("vtc_nobody")), ("GET the Outcome Record before the terminal entry", 409, "wrong-state", False, @@ -590,13 +637,12 @@ def refusal_leaves_no_entry(): after = h.status_of(vtc)["trace"] return code == 422 and before == after - def pass_in_window_changes_nothing(): + def named_verifier_answers_own_challenge(): vtc, d, v = h.window_open() - again = agents.make_verdict(vtc, d, h.verifier, "PASS") - again["evaluated_at"] = "2099-01-01T00:00:00Z" # different bytes, or it is a replay - again.pop("signature") - code, body = c.verdict(h.verifier.sign_into(again, agents.MEDIA_VERDICT)) - return code == 201 and body["state"] == "WINDOW_OPEN" and body["trace"][-1]["supersedes"] == pc.digest_over(v) + ch = agents.make_challenge(vtc, d, h.verifier, ["row_count"], costs=None) + code1, _ = c.challenge(ch) + code2, body = c.verdict(agents.make_verdict(vtc, d, h.verifier, "FAIL", ch)) + return code1 == 202 and code2 == 201 and body["state"] == "SETTLED" and body["trace"][-1]["challenge_upheld"] is True return [ ("a Challenge alone settles nothing", challenge_alone_does_not_settle), @@ -604,8 +650,8 @@ def pass_in_window_changes_nothing(): resubmit_identical_contract), ("a Buyer's Challenge is admissible", buyer_challenge_admissible), ("a refused request leaves no entry in the trace", refusal_leaves_no_entry), - ("a second PASS inside the window changes the state of nothing", - pass_in_window_changes_nothing), + ("a named Verifier answers its own Challenge and the FAIL settles the contract", + named_verifier_answers_own_challenge), ] @@ -616,6 +662,7 @@ def run_refusals(h: Harness) -> tuple[list[dict], list[dict]]: base = terms.PROBLEM_BASE if is_profile else F.PROBLEM_BASE where = body.get("profile_section") if is_profile else body.get("section") ok = (code == status and body.get("type") == base + kind and bool(where) + and (is_profile or where == F.PROBLEMS[kind][1]) and (not is_profile or body.get("profile") == terms.ID)) rows.append({"name": name, "expected": (status, kind), "got": (code, body.get("type", "?")), "section": where, "ok": ok}) @@ -653,7 +700,7 @@ def run_micro(h: Harness) -> dict: d = agents.make_delivery(vtc, h.seller, b"w", b"r") v = agents.make_verdict(vtc, d, h.verifier, "PASS") signable = pc.signable(v) - vec = h.profile.vectors()[1] + vec = next(v for v in h.profile.vectors() if v["name"].startswith("SETTLED on an upheld Challenge (the")) leaves = [bytes.fromhex(pc.h(str(i).encode())[7:]) for i in range(64)] return { "canonicalize contract (us)": bench(lambda: pc.jcs(vtc), 2000), diff --git a/tools/mint_examples.py b/tools/mint_examples.py index 808dde9..38d04e1 100644 --- a/tools/mint_examples.py +++ b/tools/mint_examples.py @@ -119,7 +119,7 @@ def build() -> dict[str, dict]: pc.sign(vtc, keys["seller"], agents.MEDIA_CONTRACT)]) vtc_hash = pc.digest_over(vtc) - work = (CONTENT / "sample-10k.csv").read_bytes() # stands in for the deliverable + work = b"customers-clean.csv: the deduplicated deliverable, 2,099,959 rows; bytes not carried in the repository\n" results = b'{"schema_valid_rate": 1.0, "dup_rate": 0.0}\n' delivery = { "pact": "0.2", "type": "Delivery", "vtc_id": vtc["id"], "vtc_hash": vtc_hash, @@ -202,10 +202,11 @@ def build() -> dict[str, dict]: capability = { "pact": "0.2", "type": "FacilitatorCapabilities", "facilitator": FACILITATOR, + "issued_at": "2026-11-01T09:00:00Z", "settlement_bindings": [{"id": "https://settle.example/bindings/ledger-1", "networks": ["eip155:8453"], "assets": ["USDC"]}], "flows": ["verdict-first", "delivery-first"], - "verification_profiles": ["acceptance", "bisection"], + "verification_profiles": ["acceptance"], "terms_profiles": [{"id": profile.id, "profile_hash": profile.profile_hash}], "max_contract_value": {"amount": "50000.00", "currency": "USDC"}, "endpoints": { diff --git a/tools/pactcore.py b/tools/pactcore.py index 1dee7e2..6f9b429 100644 --- a/tools/pactcore.py +++ b/tools/pactcore.py @@ -11,22 +11,22 @@ The canonicalizer is the same restricted RFC 8785 implementation validate.py uses. It is correct for the value types PACT objects carry and is not a conforming general JCS implementation; what it does get right, and what the -Section 13.3 vectors pin, is the UTF-16 code unit key order of RFC 8785 +Section 14.3 vectors pin, is the UTF-16 code unit key order of RFC 8785 section 3.2.3, which json.dumps(sort_keys=True) does not implement. -The signatures are real. Section 13.1 fixes the JWS Signing Input as +The signatures are real. Section 14.1 fixes the JWS Signing Input as ASCII(BASE64URL(UTF8(protected)) || "." || BASE64URL(JCS(object))) over the -object with its signing member removed, and Section 6 computes vtc_hash over +object with its signing member removed, and Section 2 computes vtc_hash over the contract *including* its signatures member. Those two facts are why a contract digest proves who agreed rather than merely what was written, and why the signing input and the digest are computed over different bytes. Both are implemented here and exercised by the measurement harness. -The committed examples under examples/ keep their placeholder signature -values on purpose. The published Internet-Draft prints their digests in -Section 14 and cannot be corrected, so re-signing them would silently -desynchronise the repository from the document. Everything in this module -mints fresh keys and fresh contracts at run time instead. +The committed examples under examples/ carry real Ed25519 signatures minted by +mint_examples.py from public seeds, so anyone can reproduce their bytes and the +digests the -02 prints; the -01 examples kept placeholder signature values, and +the -01 text printed digests over them. The measurement harness still mints fresh +keys and contracts at run time. """ from __future__ import annotations @@ -150,7 +150,7 @@ def h(b: bytes) -> str: def digest_over(obj: Any) -> str: """Digest over the whole object as it stands, signatures included. - This is the Section 6 construction. Use it for vtc_hash, and never for a + This is the Section 2 construction. Use it for vtc_hash, and never for a signing input. """ return h(jcs(obj)) @@ -185,7 +185,8 @@ def manifest_digest(dirpath) -> str: import pathlib root = pathlib.Path(dirpath) manifest = {p.relative_to(root).as_posix(): h(p.read_bytes()) - for p in sorted(root.rglob("*")) if p.is_file()} + for p in sorted(root.rglob("*")) + if p.is_file() and not any(part.startswith(".") for part in p.relative_to(root).parts)} return h(jcs(manifest)) @@ -221,7 +222,7 @@ def norm(identifier: str) -> str: never stripped the fragment, which is both too permissive and too strict in different places. """ - s = unicodedata.normalize("NFC", identifier).strip() + s = identifier.strip() s = s.split("#", 1)[0] if ":" in s: @@ -229,14 +230,20 @@ def norm(identifier: str) -> str: scheme = scheme.lower() if scheme == "did": parts = rest.split(":") - if parts: - parts[0] = parts[0].lower() # the DID method - if parts[0] == "web" and len(parts) > 1: - parts[1] = parts[1].lower() # the host + if len(parts) > 1 and parts[0] == "web": + parts[1] = parts[1].lower() # the did:web host only rest = ":".join(parts) - elif scheme in ("http", "https") and rest.startswith("//"): - host, sep, tail = rest[2:].partition("/") - rest = "//" + host.lower() + sep + tail + elif scheme == "https" and rest.startswith("//"): + authority, sep, tail = rest[2:].partition("/") + authority, q, query = authority.partition("?") # a query with no path + userinfo, at, hostport = authority.rpartition("@") + if hostport.startswith("["): # IPv6 literal + end = hostport.find("]") + 1 + hostname, port = hostport[:end], hostport[end:] + else: + hostname, colon, port = hostport.partition(":") + port = colon + port + rest = "//" + userinfo + at + hostname.lower() + port + q + query + sep + tail s = scheme + ":" + rest while s.endswith(("/", ".")): @@ -249,10 +256,10 @@ def same_party(a: str, b: str) -> bool: def signatures_ordered(obj: dict) -> tuple[bool, str]: - """The `signatures` array sorted by kid (facilitator CHOICES C9). + """The `signatures` array sorted by kid (facilitator Section 14.1). Two agents that each attach their own entry and then exchange the object - produce two arrays, two vtc_hash values (Section 6 digests the signature + produce two arrays, two vtc_hash values (Section 2 digests the signature set) and two contracts for one agreement. The order is the Section 9.1 normalized kid, ties broken by the raw kid, both compared as sequences of Unicode code points. Returns (ok, reason); an object with fewer than two @@ -265,7 +272,7 @@ def signatures_ordered(obj: dict) -> tuple[bool, str]: # -------------------------------------------------------------------------- -# The assurance constraint, Section 7.2 +# The assurance constraint, Appendix A.4 of -02 (Section 7.2 of -01) # -------------------------------------------------------------------------- def assurance_holds(price: str | float, bond: str | float, q_min: str | float, @@ -284,10 +291,10 @@ def assurance_holds(price: str | float, bond: str | float, q_min: str | float, # -------------------------------------------------------------------------- -# JWS General JSON Serialization with a detached payload, Section 13.1 +# JWS General JSON Serialization with a detached payload, Section 14.1 # -------------------------------------------------------------------------- -ALLOWED_ALGS = ("EdDSA", "ES256", "ES384") +ALLOWED_ALGS = ("Ed25519", "ES256", "ES384") def b64u(b: bytes) -> str: @@ -314,7 +321,7 @@ def signing_input(protected_b64: str, obj: dict) -> bytes: @dataclass class Key: - """A party key. `kid` is the URI a verifier resolves, per Section 13.1.1.""" + """A party key. `kid` is the URI a verifier resolves, per Section 14.1.1.""" kid: str alg: str private: Any = None @@ -327,14 +334,14 @@ def from_seed(cls, kid: str, seed: bytes) -> "Key": if not HAVE_CRYPTO: raise RuntimeError("signing needs the `cryptography` package") sk = Ed25519PrivateKey.from_private_bytes(seed) - return cls(kid=kid, alg="EdDSA", private=sk, public=sk.public_key()) + return cls(kid=kid, alg="Ed25519", private=sk, public=sk.public_key()) @classmethod def from_public_bytes(cls, kid: str, alg: str, raw: bytes) -> "Key": """A verify-only key from the raw public bytes public_bytes() emits.""" if not HAVE_CRYPTO: raise RuntimeError("verification needs the `cryptography` package") - if alg == "EdDSA": + if alg == "Ed25519": pub = Ed25519PublicKey.from_public_bytes(raw) elif alg == "ES256": pub = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), raw) @@ -345,12 +352,12 @@ def from_public_bytes(cls, kid: str, alg: str, raw: bytes) -> "Key": return cls(kid=kid, alg=alg, private=None, public=pub) @classmethod - def generate(cls, kid: str, alg: str = "EdDSA") -> "Key": + def generate(cls, kid: str, alg: str = "Ed25519") -> "Key": if not HAVE_CRYPTO: raise RuntimeError( "the `cryptography` package is required to mint keys; " "install it with `pip install cryptography`") - if alg == "EdDSA": + if alg == "Ed25519": sk = Ed25519PrivateKey.generate() elif alg == "ES256": sk = ec.generate_private_key(ec.SECP256R1()) @@ -361,7 +368,7 @@ def generate(cls, kid: str, alg: str = "EdDSA") -> "Key": return cls(kid=kid, alg=alg, private=sk, public=sk.public_key()) def sign_bytes(self, data: bytes) -> bytes: - if self.alg == "EdDSA": + if self.alg == "Ed25519": return self.private.sign(data) curve_hash = hashes.SHA256() if self.alg == "ES256" else hashes.SHA384() der = self.private.sign(data, ec.ECDSA(curve_hash)) @@ -373,7 +380,7 @@ def sign_bytes(self, data: bytes) -> bytes: return r.to_bytes(size, "big") + s.to_bytes(size, "big") def verify_bytes(self, sig: bytes, data: bytes) -> None: - if self.alg == "EdDSA": + if self.alg == "Ed25519": self.public.verify(sig, data) return size = 32 if self.alg == "ES256" else 48 @@ -384,8 +391,8 @@ def verify_bytes(self, sig: bytes, data: bytes) -> None: # RFC 7518 fixes the encoding (raw r||s) but not which of the two valid # s values a verifier accepts. Accepting both lets anyone holding a # valid signature mint a second one over the same bytes without the - # key, and a second signature entry changes vtc_hash (Section 6). The - # low half is enforced here ahead of the text; see CHOICES C9. + # key, and a second signature entry changes vtc_hash (Section 2). The + # low half is enforced here ahead of the text; see Section 14.1. n = CURVE_ORDER[self.alg] if s == 0 or s > n // 2: raise InvalidSignature("ECDSA s is not in the low half of the curve order") @@ -407,7 +414,7 @@ def verify_bytes(self, sig: bytes, data: bytes) -> None: class KeyResolver: """Maps a `kid` to a public key. - Section 13.1.1 resolves a kid as a DID URL (DID Core, did:web) or as an + Section 14.1.1 resolves a kid as a DID URL (DID Core, did:web) or as an https URI naming a JWK Set (RFC 7517). Both are network lookups with caching and revocation semantics that a reference implementation should not fake. This resolver is an in-process registry with the same interface, @@ -428,9 +435,9 @@ def resolve(self, kid: str) -> Key | None: def public_bytes(key: "Key") -> bytes: - """Raw public key bytes, for the Section 16.11 record of what was resolved.""" + """Raw public key bytes, for the Section 17.13 record of what was resolved.""" from cryptography.hazmat.primitives import serialization - if key.alg == "EdDSA": + if key.alg == "Ed25519": return key.public.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) return key.public.public_bytes(serialization.Encoding.X962, @@ -461,8 +468,13 @@ def verify_entry(obj: dict, entry: dict, resolver: KeyResolver, if member not in protected: return False, f"protected header is missing {member}" if "kid" in entry: - # Section 13.1: a kid outside the signed header is attacker-controlled. + # Section 14.1: a kid outside the signed header is attacker-controlled. return False, "kid carried as a sibling of the protected header" + for member in ("jwk", "jku", "x5c", "x5u", "x5t", "x5t#S256", "crit"): + if member in protected: + return False, f"protected header carries {member}, which Section 14.1 forbids" + if "header" in entry: + return False, "signature entry carries an unprotected header" # RFC 8725 section 3.11 and conformance vector V-05: typ carries the full # media type so a signature minted over one object cannot be presented as # one minted over another. This was previously accepted as an argument and @@ -475,7 +487,7 @@ def verify_entry(obj: dict, entry: dict, resolver: KeyResolver, if alg not in ALLOWED_ALGS: # Rejecting `none` and everything off the allowlist is the whole point: # absent one, the attacker selects the algorithm. The reason string - # starts with "algorithm" so a caller can map it to Table 9's + # starts with "algorithm" so a caller can map it to the problem table's # algorithm-not-permitted rather than a generic signature failure. return False, f"algorithm {alg!r} is not permitted" @@ -509,8 +521,8 @@ def kid_covers(kid: str, party: str) -> bool: Section 9.1 normalization strips the fragment, so a kid of did:web:seller.example#key-1 normalizes to the party identifier itself and this is an equality test. It used to be a prefix test, which is a hole: - did:web:acme.example.evil starts with did:web:acme.example and would have - signed as its neighbour. Line 1965 of the draft requires equality of the + did:web:seller.example.evil starts with did:web:seller.example and would have + signed as its neighbour. Section 14.1 of the draft requires equality of the normalized identifier, not containment. """ return norm(kid) == norm(party) diff --git a/tools/profile.py b/tools/profile.py index 3d54132..f7ccb75 100644 --- a/tools/profile.py +++ b/tools/profile.py @@ -91,10 +91,6 @@ def admit(self, vtc: dict) -> None: bond = _d(prm["seller_bond"]) fund = _d(prm["verification_fund"]) cap = _d(prm["cap"]) - if bond > cap or fund > cap: - raise ProfileRefusal("parameters-inconsistent", - "seller_bond and verification_fund cannot exceed cap", - "A.4", cap=prm["cap"]) mode = prm["assurance"]["mode"] if mode == "open": raise ProfileRefusal("assurance-constraint-unsatisfied", @@ -111,6 +107,10 @@ def admit(self, vtc: dict) -> None: f"{_money(released_before_verdict)}", "A.4", required_bond=f"{need:f}", declared_bond=prm["seller_bond"], q_min=prm["assurance"]["q_min"], price=vtc["price"]["amount"]) + if bond > cap or fund > cap: + raise ProfileRefusal("parameters-inconsistent", + "seller_bond and verification_fund cannot exceed cap", + "A.4", cap=prm["cap"]) # -- Appendix A.3: accounts ----------------------------------------------- @staticmethod @@ -161,7 +161,7 @@ def standing() -> dict | None: if ev == "funded": emit(i, "buyer", "escrow", price, "lock") emit(i, "seller", "bond", bond0, "bond") - emit(i, "buyer", "fund", fund0, "fund") + emit(i, "seller", "fund", fund0, "fund") elif ev == "delivered": if principal_on == "delivered": emit(i, "escrow", "seller", bal["escrow"], "principal") @@ -182,11 +182,11 @@ def standing() -> dict | None: if state == "FINAL": emit(i, "escrow", "seller", bal["escrow"], "principal") emit(i, "bond", "seller", bal["bond"], "return") - emit(i, "fund", "buyer", bal["fund"], "fund-return") + emit(i, "fund", "seller", bal["fund"], "fund-return") elif state == "ABANDONED": emit(i, "escrow", "buyer", bal["escrow"], "reverse") emit(i, "bond", "seller", bal["bond"], "return") - emit(i, "fund", "buyer", bal["fund"], "fund-return") + emit(i, "fund", "seller", bal["fund"], "fund-return") else: # SETTLED, five ranks st = standing() upheld = bool(e.get("challenge_upheld")) and st is not None and "answers" in st @@ -214,7 +214,7 @@ def standing() -> dict | None: min(bal["bond"], room), "remainder") # 5 # anything the cap kept in the bond is not this profile's to move emit(i, "bond", "seller", bal["bond"], "return") - emit(i, "fund", "buyer", bal["fund"], "fund-return") + emit(i, "fund", "seller", bal["fund"], "fund-return") return out def step(self, vtc: dict, trace: list[dict]) -> list[dict]: @@ -247,6 +247,15 @@ def vectors(self) -> list[dict]: def reproduces(self) -> tuple[bool, str]: for v in self.vectors(): + if "admission" in v: + try: + self.admit(v["contract"]) + if not v["admission"].get("admitted"): + return False, f"vector {v['name']}: admitted, bundle says refused" + except ProfileRefusal as exc: + if v["admission"].get("refused") != exc.kind: + return False, f"vector {v['name']}: refused as {exc.kind}" + continue got = self.schedule(v["contract"], v["trace"]) if got != v["transfers"]: return False, f"vector {v['name']}: schedule differs from the bundle" @@ -361,8 +370,8 @@ def _trace_delivery_first() -> list[dict]: def build_vectors(profile: BondedRestitution) -> list[dict]: cases = [ - ("FINAL: PASS, window closes, Figure 1", _contract(), _trace_final()), - ("SETTLED on an upheld Challenge, Figure 5", _contract(), _trace_overturned()), + ("FINAL: PASS, window closes (the verdict-first path)", _contract(), _trace_final()), + ("SETTLED on an upheld Challenge (the dispute path)", _contract(), _trace_overturned()), ("SETTLED: the Verifier records FAIL", _contract(), _trace_settled_by_verifier()), ("ABANDONED: deadline with no Delivery", _contract(), _trace_abandoned()), ("SETTLED on an upheld Challenge, basis price", @@ -377,7 +386,23 @@ def build_vectors(profile: BondedRestitution) -> list[dict]: transfers = profile.schedule(contract, trace) ok, why = profile.check(contract, transfers, terminal=True) assert ok, f"{name}: {why}" - out.append({"name": name, "contract": contract, "trace": trace, "transfers": transfers}) + out.append({"name": name, "contract": contract, "trace": trace, "transfers": transfers, + "accounts": {"internal": list(INTERNAL)}}) + q = {"mode": "certain", "q_min": 0.9091} + admission = [ + ("admission: refused, seller_bond 17.99 at q_min 0.9091 on 180.00", + _contract(parameters={"seller_bond": "17.99", "assurance": q}), "assurance-constraint-unsatisfied"), + ("admission: admitted, seller_bond 18.00 at q_min 0.9091 on 180.00", + _contract(parameters={"seller_bond": "18.00", "assurance": q}), None), + ] + for name, contract, refused in admission: + try: + profile.admit(contract) + assert refused is None, f"{name}: admitted" + out.append({"name": name, "contract": contract, "admission": {"admitted": True}}) + except ProfileRefusal as exc: + assert refused == exc.kind, f"{name}: {exc.kind}" + out.append({"name": name, "contract": contract, "admission": {"refused": exc.kind}}) return out diff --git a/tools/validate.py b/tools/validate.py index 98581a2..9dc6cf2 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -232,6 +232,9 @@ def headers_ok(obj, typ) -> tuple[bool, str]: standing = [e for e in tr if e["event"] == "verdict"][-1] r.check(standing["outcome"] == "FAIL" and ("answers" in standing) == outcome["outcome"]["challenge_upheld"], "challenge_upheld is true exactly when the standing FAIL answers a Challenge") + ats = [e["at"] for e in tr] + r.check(ats == sorted(ats) and status["issued_at"] >= status["trace"][-1]["at"], + "trace timestamps never decrease and issued_at is not earlier than the last entry (Section 4.2)") r.check(status["state"] == "WINDOW_OPEN" and status["trace"][-1]["event"] == "window-opened", "the example Status is the window-opened moment") @@ -241,7 +244,7 @@ def headers_ok(obj, typ) -> tuple[bool, str]: if HAVE_CRYPTO: resolver = pc.KeyResolver() for role, jwk in keys.items(): - resolver.register(pc.Key.from_public_bytes(jwk["kid"], "EdDSA", pc.b64u_decode(jwk["x"]))) + resolver.register(pc.Key.from_public_bytes(jwk["kid"], "Ed25519", pc.b64u_decode(jwk["x"]))) for name, obj, typ, required in ( ("vtc", vtc, "vtc", [parties["buyer"], parties["seller"]]), ("delivery", delivery, "delivery", [parties["seller"]]), @@ -268,14 +271,16 @@ def headers_ok(obj, typ) -> tuple[bool, str]: def tup(ts): return [(t["event"], t["from"], t["to"], t["amount"], t["code"]) for t in ts] final_printed = [(1, "buyer", "escrow", "180.00", "lock"), (1, "seller", "bond", "18.00", "bond"), - (1, "buyer", "fund", "0.50", "fund"), (3, "escrow", "seller", "180.00", "principal"), - (7, "bond", "seller", "18.00", "return"), (7, "fund", "buyer", "0.50", "fund-return")] + (1, "seller", "fund", "0.50", "fund"), (3, "escrow", "seller", "180.00", "principal"), + (7, "bond", "seller", "18.00", "return"), (7, "fund", "seller", "0.50", "fund-return")] overturned_printed = [(1, "buyer", "escrow", "180.00", "lock"), (1, "seller", "bond", "18.00", "bond"), - (1, "buyer", "fund", "0.50", "fund"), (3, "escrow", "seller", "180.00", "principal"), + (1, "seller", "fund", "0.50", "fund"), (3, "escrow", "seller", "180.00", "principal"), (8, "fund", "challenger:did:web:watch.example#k1", "0.50", "costs"), (8, "bond", "buyer", "18.00", "restitution")] - r.check(tup(vecs[0]["transfers"]) == final_printed, "Appendix A.6, the FINAL list, is vector 1 verbatim") - r.check(tup(vecs[1]["transfers"]) == overturned_printed, "Appendix A.6, the overturned list, is vector 2 verbatim") + by_name = lambda prefix: next(v for v in vecs if v["name"].startswith(prefix)) + r.check(tup(by_name("FINAL: PASS")["transfers"]) == final_printed, "Appendix A.6, the FINAL list, is the bundle's verdict-first vector verbatim") + r.check(tup(by_name("SETTLED on an upheld Challenge (the")["transfers"]) == overturned_printed, "Appendix A.6, the overturned list, is the bundle's dispute-path vector verbatim") + r.check(sum("admission" in v for v in vecs) == 2 and len(vecs) == 9, "the bundle carries nine vectors, two of them admission vectors") r.check(outcome["terms_result"]["transfers"] == prof.schedule(vtc, outcome["trace"]), "outcome.terms_result.transfers is the schedule over the example trace") ok, why = prof.check(vtc, outcome["terms_result"]["transfers"], terminal=True) @@ -326,7 +331,7 @@ def admits(bond: str, q: float) -> bool: "581A0DB248B0A77AECEC196ACCC52973", 16), "P-384 group order is the SEC 2 value") if not HAVE_CRYPTO: - for _ in range(22): + for _ in range(26): r.skip("negative vector") return r.done() @@ -392,7 +397,7 @@ def kid_outside() -> dict: lambda: fac.propose(cosign(fresh | {"parties": dict(parties, seller=parties["buyer"] + "/")}, kb, ks)), ("parties-not-distinct",)) refused("V-08 two buyer signatures, no seller", lambda: fac.propose(cosign(fresh, kb, kb)), - ("signature-missing", "signature-invalid", "unexpected-signer")) + ("unexpected-signer",)) refused("V-09 window_seconds 0", lambda: fac.propose(cosign(fresh | {"challenge": dict(vtc["challenge"], window_seconds=0)}, kb, ks)), ("schema-invalid",)) @@ -409,8 +414,20 @@ def kid_outside() -> dict: ("terms-parameters-invalid",)) case_seller = parties["buyer"].replace("procure-1", "Procure-1") kcase = resolver.register(pc.Key.generate(case_seller + "#k1")) - code, _ = fac.propose(cosign(fresh | {"id": "vtc_case01", "parties": dict(parties, seller=case_seller)}, kb, kcase)) + try: + code, _ = fac.propose(cosign(fresh | {"id": "vtc_case01", "parties": dict(parties, seller=case_seller)}, kb, kcase)) + except F.Refuse as exc: + code = exc.kind r.check(code == 201, "V-19 buyer and seller differing only in did:web path case are accepted as distinct") + forbidden = {"jwk": {"kty": "OKP"}, "jku": "https://keys.example/jwks", "x5c": ["MIIB"], "x5u": "https://keys.example/c.pem", + "x5t": "abc", "x5t#S256": "abc", "crit": ["b64"]} + bad = [] + for member, value in forbidden.items(): + try: + fac.propose(with_header(vtc, **{member: value})); bad.append(member + " accepted") + except F.Refuse as exc: + if exc.kind != "signature-invalid": bad.append(f"{member}: {exc.kind}") + r.check(not bad, "V-26 a protected header carrying jwk, jku, x5c, x5u, x5t, x5t#S256 or crit is refused as signature-invalid", "; ".join(bad)) refused("V-20 undefined member", lambda: fac.propose(cosign(fresh | {"bonus": True}, kb, ks)), ("schema-invalid",)) refused("V-21 signatures out of order", lambda: fac.propose(vtc | {"signatures": list(reversed(vtc["signatures"]))}), ("signatures-unordered",)) @@ -433,9 +450,11 @@ def kid_outside() -> dict: # The example contract, then the Delivery-level and Verdict-level vectors code, _ = fac.propose(vtc) r.check(code == 201, "V-01 the example contract is accepted by the reference Facilitator") + before = len(fac.get_status(vtc["id"])[1]["trace"]) refused("V-14 Delivery without evidence", lambda: fac.submit_delivery(resign({k: v for k, v in delivery.items() if k != "evidence"}, ks, MEDIA["delivery"])), ("evidence-nonconformant",)) + r.check(len(fac.get_status(vtc["id"])[1]["trace"]) == before, "V-14: the refused Delivery left no entry in the trace") code, _ = fac.submit_delivery(delivery) r.check(code == 202, "the example Delivery is accepted after the nonconformant one was refused") refused("V-17 Verdict signed by the seller", lambda: fac.record_verdict(resign(verdict, ks, MEDIA["verdict"])),