feat(client): resolve-before-pay for commitment-bound quotes (ADR-0004)#126
Conversation
51e1451 to
9dadd24
Compare
9dadd24 to
0414f39
Compare
Testnet analysis — DEV-01 (5h20m, 58 nodes ×15, 30% NAT) vs release 0.2.10 / node 0.14.2 baselineRan this client (paired with ant-node #149) on a staging testnet against an identical-topology baseline over a matched 5h20m window. Clean result — 0 upload failures (2,977 uploads), 0 download failures (662). Client-side chunk retry amplification = 1.000 exactly: all 98,866 stored chunks succeeded on the first attempt ( Worth noting: uploads ran +26–60% slower than the baseline at every file size (e.g. 1GB 179s→255s, 500MB 105s→168s) and downloads +13–26% slower. Store-latency analysis pins the slowdown on the node side (put_rpc p95/p99 rising as node disk fills), not the client — client store p50 stayed flat (~3s) with retry-amp flat at 1.0. No client-side blockers found. |
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.
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
Strip the temporary [patch.crates-io] git branches and repin ant-node from the grumbach ADR-0004 branch to the published 0.14.3, now that the coordinated ADR-0004 cutover crates are published in order evmlib 0.9.0 -> ant-protocol 2.3.0 -> ant-node 0.14.3. ant-protocol was already pinned to 2.3.0. Lockfile regenerated to registry sources; the workspace resolves a single saorsa-core 0.26.2 and `cargo check` builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
872b29b to
059cc0a
Compare
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:
(committed_key_count, commitment_pin)shape, the count cap, and thatprice == calculate_price(count)by exact recomputation;key_countmatches 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 onant-node; both are stripped and repinned to the published crates at cutover.