tools: a reference Facilitator, party agents, and a measurement harness - #10
Conversation
Section 15 says no Facilitator, Buyer or Seller exchanging messages over the Section 12 endpoints is known to the author. That is now half wrong: this is one such implementation. Two independent ones settling each other's contracts is still the experiment Section 1.4 describes. facilitator.py serves the six operations of Table 1 over five paths, runs the Figure 2 state machine, evaluates the Section 7.2 constraint before locking funds, applies the Section 7.4 waterfall, issues a Facilitator signed attestation for every terminal contract, and refuses with RFC 9457 problem documents that name the rule. agents.py is the Buyer, Seller and Verifier side. measure.py drives three contracts to FINAL, SETTLED and ABANDONED, exercises eight refusal paths, and reports costs. Signatures are real Ed25519 over the Section 13.1 signing input. The committed examples keep their placeholder values on purpose: the published draft prints their digests in Section 14 and cannot be corrected, so re-signing them would desynchronise this repository from that document. measure.py mints fresh keys and fresh contracts instead, and they validate against the published schemas. Running it found two specification defects, both recorded in tools/README.md and both -02 items rather than code changes. Section 6 introduces the missed-deadline rule with "This is the rule that makes silence expensive", then slashes the bond only to the extent of restitution_basis. The worked example sets that to "released", and under the default on-verification release nothing is released before a verdict, so the extent is zero. A seller can sign, post a bond, deliver nothing, and get the entire bond back. Section 7.4 has the same hole. Rank 3 restitution is measured against the same member, so it is also zero, and the bond falls through to rank 5 and the neutral sink. That is the exact -00 behaviour Section 7.4 exists to correct, reproduced by the -01's own worked example. Setting the member to "price" pays the buyer 18.00 from the bond instead. validate.py is untouched and still passes its 66 checks; cryptography stays an optional dependency it does not need. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An adversarial review of the first version of this branch found that one of its two headline claims was false and that the implementation failed six normative MUSTs. All are fixed here, and the claim is retracted in tools/README.md rather than quietly deleted. The false claim was "a defrauded buyer still recovers nothing from the bond". Rank 1 of the Section 7.4 waterfall returns the whole escrow to the buyer before rank 3 is reached, so the buyer's loss is zero and a restitution of 0.00 is arithmetically correct. The transcript printed directly beneath the claim said so. The six defects, each reproduced before it was fixed: open_challenge applied the waterfall directly, so any party with a resolvable key could settle a contract on which no Verdict was ever recorded, destroy the seller's bond, and have an attestation of outcome "slashed" issued against it. Section 7.5 says a Challenge is evaluated by a party satisfying Section 9.1 whose finding is a Verdict, and that the Challenger's own assertion is not. Accepting a Challenge now moves the contract to DISPUTED and nothing else, and record_verdict accepts a Verdict in that state. record_verdict never checked the signer against parties.verifier, so any resolvable key could pass or fail any contract. Line 1852 makes that an unconditional MUST where the contract names one. verify_object took an expected media type and never compared it, so a signature minted over a Delivery was accepted on a Verdict. That is conformance vector V-05, which validate.py implements and passes in the same directory. kid_covers tested startswith, so did:web:acme.example.evil signed as did:web:acme.example. norm casefolded the whole identifier and never stripped the fragment. Section 9.1 folds only the scheme and, for did:web and https, the host, and removes the fragment and a trailing "/" or ".". submit_delivery accepted a Delivery with no evidence member. Line 832 requires rejecting it and applying Section 7.4 as though a FAIL Verdict had been recorded; both halves are normative and both now happen. measure.py now asserts money conservation per scenario, which immediately caught a seventh defect: the verification fund was zeroed on return without being credited to anyone, losing 0.50 in every run. The T0-reexec figure is removed. test_acceptance.py is a pytest module, so running it as a script executed no tests and exited 0 in silence; the number was pytest's import time, and the claim built on it that verification cost exceeds protocol cost by three orders of magnitude had no measurement behind it. Problem type URIs are now urn:pact:problem: rather than a github.io URL that does not resolve. Section 18.5 creates no registry. validate.py remains untouched and still passes its 66 checks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Converted to draft and pushed a fix commit. An adversarial review of the first version found that one of the two headline claims was false and that the implementation failed six normative MUSTs. Neither is a good thing to discover after merging into a repository an Internet-Draft names by URL, so this stayed unmerged. The false claim. "A defrauded buyer still recovers nothing from the bond" is wrong, and it is refuted by the transcript that was printed directly beneath it. Rank 1 of the Section 7.4 waterfall returns the whole 180.00 escrow to the buyer before rank 3 is reached, so the buyer's loss is zero and a restitution payment of 0.00 is arithmetically correct rather than defective. It is retracted in tools/README.md rather than deleted, because the same misreading is easy for anyone else reading that waterfall. Six code defects, each reproduced before it was fixed. The worst: accepting a Challenge applied the waterfall directly, so any party with a resolvable key could settle a contract on which no Verdict was ever recorded, destroy the seller's bond, and have an attestation of outcome "slashed" issued against it. Section 7.5 is explicit that a Challenge is evaluated by an independent party whose finding is a Verdict, and that the Challenger's own assertion is not. The rest: the Verdict signer was never checked against
One measurement is withdrawn. The T0-reexec figure timed One quotation was misattributed. "This is the rule that makes silence expensive" closes the paragraph about rejecting nonconformant evidence at line 832. It does not introduce the missed-deadline rule, which begins a new paragraph at line 846. What survives as a real question for -02. On a missed deadline the bond is slashed to the extent of Thirteen refusal paths now fire with the right status and section, money balances in every scenario, and |
…implemented A second adversarial pass found the first version wrong in the one place the draft makes mandatory: under on-verification a PASS returned the Bond and reached FINAL in the same call, so no challenge window ever opened and the Figure 6 overturned-PASS path was unreachable. Now a PASS releases the price and opens the window on the Facilitator's clock (injectable; the harness advances it); the Bond and fund stay locked until the window closes; a Challenge is accepted only inside it, with a proof conformant to the profile, and settles nothing itself; a Verdict on a Challenge supersedes the earlier one and both are recorded; an upheld Challenge runs the waterfall with the released amount, and the attestation reads settled 180.00, restituted 18.00, slashed 18.00, which is what Section 11's Figure 12 should carry. Also fixed: task.deadline parsed in local time via mktime (now RFC 3339, UTC); Verdict and Challenge commitments bypassable by omitting delivery_hash or adding vtc_hash (every posted object now validates against its published schema, and the binding is by object kind); a bounty paid to a Challenger that did not exist; liability.cap never enforced; release modes, settlement bindings, assurance modes and profiles the capability document does not advertise accepted and stranded (refused at Propose with a named rule); a contract naming another Facilitator accepted (16.7); a named verifier who is the buyer or seller accepted; liability.parent silently ignored (subcontracts are refused as parent-unresolvable, not faked); the capability document unsigned and failing its own schema; the JWS signing input rebuilt from the parsed protected header rather than the transmitted bytes; the assurance constraint rounded to cents before comparison (now exact Decimal); a third party's signature accepted on a co-signed contract; Verdict instrument_hash and profile never compared to the contract; a Challenger judging its own Challenge; idempotent re-POSTs returning a stale snapshot; problem types in a private URN namespace (now the draft's https prefix and Table 9 statuses); Python exception text returned as a 409. measure.py now drives FINAL, SETTLED, ABANDONED and the overturned PASS, checks the capability document's schema and signature, asserts money conservation, and exercises 24 refusals each on the rule it is named for, plus 2 acceptances reported as acceptances. README lists the six choices the draft left open (C1 to C6) and what is not implemented. validate.py untouched, 66 checks pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Second adversarial pass (11 to 12 Sept), fixes pushed in 66d6a60. The first version was wrong in the one place the draft makes mandatory: under on-verification a PASS returned the Bond and reached FINAL in the same call, so no challenge window ever opened and the Figure 6 overturned-PASS path was unreachable. Now a PASS releases the price and opens the window on the Facilitator's clock (injectable; the harness advances it), the Bond and fund stay locked until it closes, a Challenge is accepted only inside it with a conformant proof and settles nothing itself, both Verdicts are recorded, and an upheld Challenge runs the waterfall with the released amount. That path attests settled 180.00 / restituted 18.00 / slashed 18.00, which is what Section 11's Figure 12 should carry (the draft's 0.00 / 18.00 / 18.00 fits neither path). Also fixed: deadline parsed in local time; delivery_hash binding bypassable by omitting it or adding vtc_hash (every posted object now validates against its schema); bounty paid to a Challenger that did not exist; liability.cap not enforced; unadvertised release modes, bindings, assurance modes and profiles accepted and stranded (now refused at Propose with a named rule); a contract naming another Facilitator accepted; liability.parent silently ignored (subcontracts are now refused as parent-unresolvable rather than faked); capability document unsigned and failing its schema; signing input rebuilt from the parsed header rather than the transmitted bytes; constraint rounded to cents before comparison (now exact Decimal); a third party's signature accepted on a co-signed contract; Verdict instrument_hash and profile never compared to the contract; problem types in a private URN namespace (now the draft's https prefix and Table 9 statuses). tools/README.md lists the six choices the draft left open (C1 to C6) and what is not implemented. validate.py untouched, 66 checks pass. measure.py: FINAL 4 exchanges, SETTLED 4, ABANDONED 3, overturned PASS 6; 24 refusals each on the rule it is named for, 2 acceptances; money conservation asserted. Still a draft. It has now been through two adversarial passes; a reviewer who is not its author is the next thing it needs. |
A reference Facilitator, party agents and a measurement harness (tools/). Two adversarial review rounds applied; 24 refusals and 2 acceptances measured 12 Sept 2026.
Section 15 of the draft says no Facilitator, Buyer or Seller exchanging messages over the Section 12 endpoints is known to me. This is one. It is not the experiment Section 1.4 describes, which needs two independent implementations settling each other's contracts, so treat this as the first half and an invitation for the second.
What it is.
facilitator.pyserves the six operations of Table 1 over five paths, runs the Figure 2 state machine, checks the Section 7.2 assurance constraint before locking funds, applies the five-rank Section 7.4 waterfall, issues a Facilitator-signed attestation for every terminal contract, and refuses with RFC 9457 problem documents that name the rule that was violated.agents.pyis the party side.pactcore.pyholds canonicalization, JWS, normalization and the constraint.measure.pydrives it.Signatures are real Ed25519 over the Section 13.1 signing input, which is computed over the object with its signing member removed while
vtc_hashis computed over the object with signatures included. Both constructions are exercised.The committed examples keep their placeholder signatures on purpose. The published draft prints their digests in Section 14 and cannot be corrected, so re-signing them would silently desynchronise this repository from that document.
measure.pymints fresh keys and contracts instead, and those validate against the published schemas.What running it found. Two defects, both in the specification rather than the code.
Section 6 introduces the missed-deadline rule with the sentence "This is the rule that makes silence expensive", and then slashes the bond only to the extent of
restitution_basis. The worked example sets that member toreleased, and under the defaulton-verificationrelease nothing is released before a verdict, so the extent is zero. The seller signs, posts a bond, delivers nothing, and gets the whole bond back.Section 7.4 has the same hole for the same reason. Rank 3 restitution is measured against the same member, so it is zero, and the bond falls through to rank 5 and the neutral sink. That is precisely the -00 behaviour Section 7.4 was written to correct, reproduced by the -01's own example. Changing the member to
pricepays the buyer 18.00 out of the bond for the same fraud.Neither is fixed here, because both are -02 questions: either the example should use
price, or the member needs a stated default plus a rule that a Facilitator refuses a combination that makes the remedy vacuous.Measured on the machine noted in the output. Three terminal states in four, four and three messages, under 3.1 kB on the wire per contract. Ed25519 verification of a contract around 0.57 ms and canonicalize-plus-digest around 0.14 ms, against 625 ms for the T0-reexec acceptance harness, so verification cost sits in the profile and not in the protocol by about three orders of magnitude.
validate.pyis untouched and still passes its 66 checks.cryptographyis an optional dependency it does not need.