Skip to content

feat(client): resolve-before-pay for commitment-bound quotes (ADR-0004)#126

Open
grumbach wants to merge 11 commits into
WithAutonomi:mainfrom
grumbach:adr-0003-quote-commitment-resolve
Open

feat(client): resolve-before-pay for commitment-bound quotes (ADR-0004)#126
grumbach wants to merge 11 commits into
WithAutonomi:mainfrom
grumbach:adr-0003-quote-commitment-resolve

Conversation

@grumbach

@grumbach grumbach commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

ADR-0004: commitment-bound quote pricing — client side.

Implements "the client pays nothing it cannot resolve". Before paying, the client runs the full binding check on every quote and candidate, so a node that overstates its storage to inflate its price is dropped before any on-chain payment instead of after.

What

For each single-node quote and each merkle candidate, the client now:

  • verifies the quote's own ML-DSA-65 signature and that it is for the requested content;
  • checks the (committed_key_count, commitment_pin) shape, the count cap, and that price == calculate_price(count) by exact recomputation;
  • for a bound quote, fully resolves the shipped commitment: parse it, check it is bound to the quoting peer, verify its signature, that it hashes to the quote's pin, and that its key_count matches the claimed count.

Anything unresolvable, withheld, off-curve, or contradictory is dropped before payment — exactly as the storer would reject it. The verified commitments are then forwarded as sidecars in the PUT bundle (single-node and merkle) so storers cross-check synchronously; sidecar blobs are size-capped before parsing. Pricing and commitment verification come from ant-protocol, so client and node never disagree.

Tests

Adds an e2e test (real QUIC nodes + Anvil) proving bound quotes are shipped, priced, fully resolve, and round-trip; the off-curve / forged-commitment / signature / pin / count rejection paths are covered in unit tests.

Release ordering

Part of the coordinated ADR-0004 cutover — publish order evmlib 0.9.0 → ant-protocol 2.3.0 → ant-node / ant-client. Depends on the shared layer (WithAutonomi/ant-protocol#15; evmlib WithAutonomi/evmlib#11 merged, pending publish) and on the node side (WithAutonomi/ant-node#149). Builds today via committed, temporary [patch.crates-io] git deps plus a temporary git dep on ant-node; both are stripped and repinned to the published crates at cutover.

@grumbach grumbach force-pushed the adr-0003-quote-commitment-resolve branch 3 times, most recently from 51e1451 to 9dadd24 Compare July 1, 2026 13:51
@grumbach grumbach changed the title feat(client): resolve-before-pay for commitment-bound quotes (ADR-0003) feat(client): resolve-before-pay for commitment-bound quotes (ADR-0004) Jul 3, 2026
grumbach added 8 commits July 3, 2026 18:40
Implement the client half of ADR-0003: the client pays nothing it cannot
resolve. Before paying, it runs the full binding check on every quote and
candidate, so a node that overstates its storage to inflate its price is
dropped before any on-chain payment instead of after.

For each single-node quote and each merkle candidate, the client now:
- verifies the quote's own ML-DSA-65 signature and that it is for the
  requested content;
- checks the (committed_key_count, commitment_pin) shape, the count cap,
  and that price == calculate_price(count) by exact recomputation; and
- for a bound quote, fully resolves the shipped commitment: parse it,
  check it is bound to the quoting peer, verify its signature, that it
  hashes to the quote's pin, and that its key_count matches the claimed
  count.
Anything unresolvable, withheld, off-curve, or contradictory is dropped
before payment, exactly as the storer would reject it.

The verified commitments are then forwarded as sidecars in the PUT bundle
(single-node and merkle) so storers cross-check synchronously. Sidecar
blobs are size-capped before parsing. Pricing and the commitment
verification come from ant-protocol, so client and node never disagree.

Adds an e2e test (real QUIC nodes + Anvil) proving bound quotes are
shipped, priced, fully resolve, and round-trip; and the off-curve /
forged-commitment rejection is covered in unit tests.

Depends on the evmlib, ant-protocol, and ant-node ADR-0003 releases; will
not build standalone until those publish.
[patch.crates-io] points evmlib/ant-protocol at their PR branches; ant-core
points the ant-node dep (runtime + dev) at the matching ant-node ADR-0004
branch (published versions version-collide with the patch). At release: revert
ant-node to a published version, drop the patch block, bump ant-protocol pin.
…ote flow; renumber ADR-0003 -> ADR-0004

StoreQuote carries the commitment sidecar end-to-end (resolve-before-pay);
get_store_quotes family + median helpers + downstream consumers updated.
The renamed ADR-0004 e2e target was auto-discovered but never listed in the CI
E2E command, so ant-client could go green without exercising the main ADR-0004
end-to-end coverage. Add --test e2e_adr0004.
Point the ant-node / ant-protocol / evmlib git-branch deps at the freshly
clean-rebased ADR-0004 revisions (ant-node ff7a547 — the audit-code merge
against current main resolved correctly; ant-protocol 3a4eb638 = 2.3.0;
evmlib a7ac7e26 = 0.9.0). Recovers the workspace build now that ant-node's
branch compiles again.
Add negative unit tests for the three resolve-before-pay commitment
sub-checks that previously had no coverage (deleting any of them still
passed all tests): invalid commitment signature, commitment that does
not hash to the quote's pin, and quote count disagreeing with the
committed key_count. Each constructs a validly-signed, peer-bound
commitment and breaks exactly one field to isolate its check.

Rename binding_rejects_garbage_and_wrong_pin_commitment to
binding_rejects_unparseable_and_peer_unbound_commitment to reflect what
it actually exercises (the 'wrong pin' case failed peer-binding first),
and fix the stale 'adr-0003' e2e test payload strings.
@grumbach grumbach force-pushed the adr-0003-quote-commitment-resolve branch from 9dadd24 to 0414f39 Compare July 3, 2026 16:49
grumbach added 3 commits July 3, 2026 18:56
Once nodes rotate their first storage commitment, every merkle candidate
is bound and finalize_merkle_batch copied all 16 winner-pool commitment
sidecars into EVERY per-chunk proof, growing it from ~128 KB to ~342 KB,
past the storer's 256 KB payment-proof cap. Every merkle chunk PUT was
rejected AFTER the on-chain payment, so forced-merkle uploads burned
their payment and then failed every store attempt (DEV-01, 2026-07-06
19:38 UTC: both forced-merkle clients flipped to 100% failure the moment
855 nodes rotated their first commitment; single-node payments were
unaffected).

The sidecars are not needed in the bundle: the client fully resolves
every candidate's shipped commitment before paying, and the storer's
cross-check is best-effort with a gossip-cache / GetCommitmentByPin
fallback. Drop the forwarding and the dead plumbing that threaded the
sidecar map from pool collection into finalization.

Receipts cached by pre-fix clients still carry the fat proofs, so a
resume would replay the rejection: strip sidecars from cached merkle
receipts on load, with an atomic (create_new tmp + fsync + rename)
write-back that can never truncate the only copy of a paid receipt.

New coverage:
- e2e: forced-merkle upload against 35 committed nodes; failed with the
  exact production signature before this fix, passes after, against the
  unchanged 256 KB storer cap
- unit: finalize_merkle_batch ships no sidecars (bound candidates)
- unit: cached receipt strip + idempotency + non-merkle passthrough
- unit: atomic overwrite lands and leaves no tmp sibling
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant