Skip to content

Add ADR-0005 client-side earned reward eligibility gate - #2

Closed
grumbach wants to merge 12 commits into
adr-0003-quote-commitment-resolvefrom
adr-0005-earned-reward-eligibility
Closed

Add ADR-0005 client-side earned reward eligibility gate#2
grumbach wants to merge 12 commits into
adr-0003-quote-commitment-resolvefrom
adr-0005-earned-reward-eligibility

Conversation

@grumbach

@grumbach grumbach commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Base note. Opened against the fork's adr-0003-quote-commitment-resolve branch (the head of ant-client#126, the ADR-0004 work this stacks on) so the diff shows the ADR-0005 changes only. Retarget to WithAutonomi:main once WithAutonomi#126 merges.

What

The client side of ADR-0005: earned reward eligibility, and the piece that actually decides who gets paid. The gate sits at quote collection, so payee selection and payment verification are untouched: a quoter is payable when more than half of the other responders who know it vouch for a clean audited week at the quoted size.

Depends on the ADR-0005 wire types (ant-protocol#19) and node tally (ant-node#169).

Changes

  • eligibility.rs (new) — aggregate the quorum of signed audit reports into a two-tier decision:
    • Tier 1 (size) — a majority vouch for a clean week at the quoted size. Full eligibility.
    • Tier 2 (dues) — if too few clear tier 1, relax the size requirement to keep the network paying under fast network-wide growth. Safe because ADR-0004 independently forces a current-size proof at payment, so a node still can't be paid for a size it doesn't hold. Both tiers exclude fresh identities (no dues) and caught cheaters (fenced/convicted).
    • The eligibility map is keyed by (peer, quoted size) so one peer's small size-eligible quote can never leak size-tier admission onto its larger quote. If neither tier fills the need, it falls back to today's rules and logs the degraded mode; the network never stalls.
  • merkle.rs / quote.rs — carry the client report_nonce on quote requests, collect the returned audit reports, and apply the gate at pool / close-group composition.
  • Tests — unit coverage for the majority bar, both tiers, the (peer,size) keying, and the fresh/convicted exclusions; plus an end-to-end suite: mixed pool under enforcement, fenced/unproven substitution, and the fast-growth dues fallback.

Dependency pins (revert before merge)

Cargo.toml temporarily pins ant-protocol (ant-protocol#19) and ant-node (ant-node#169) to their ADR-0005 PR branches so CI can build the coordinated change. These revert to published version bumps once the wire types and node tally are released, same as the ADR-0004 cutover.

Follow-up

Wiring --test e2e_adr0005 into .github/workflows/ci.yml is left out of this PR (token lacks the workflow scope); it should be added in a small follow-up so the new e2e suite runs in CI. It passes locally.

Testing

cargo test -p ant-core --features test-utils --lib (426 pass, incl. 14 eligibility unit tests) and the ADR-0005 e2e suite (6 tests) pass locally against the ant-protocol#19 / ant-node#169 branches; cargo clippy --all-targets --features test-utils -D warnings clean; cargo fmt --check clean.

grumbach added 12 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.
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
The client decides who is payable. This adds the eligibility gate at
quote collection (payee selection and payment verification untouched): a
quoter is payable when more than half the other responders who know it
vouch for a clean audited week at the quoted size.

- eligibility: aggregate the quorum of signed audit reports into a
  two-tier decision. Tier 1 (size) needs a vouched clean week AT the
  quoted size; tier 2 (dues) relaxes size to keep the network paying
  under fast growth, and is safe because ADR-0004 independently forces a
  current-size proof at payment. Both tiers still exclude fresh
  identities (no dues) and caught cheaters (fenced/convicted). The
  eligibility map is keyed by (peer, quoted size) so one peer's small
  quote can never leak size-tier admission onto its larger quote. If too
  few are eligible, it prefers the ones with the most dues, then falls
  back to today's rules; the network never stalls.
- merkle / quote: carry the client report_nonce on quote requests,
  collect the returned audit reports, and apply the gate at pool /
  close-group composition.
- tests: unit coverage for the bar, both tiers, and the exclusions, plus
  an end-to-end suite (mixed pool under enforcement, fenced/unproven
  substitution, and fast-growth dues fallback).
- Cargo: temporarily pin ant-protocol and ant-node to the ADR-0005 PR
  branches carrying the wire types and node tally; revert to published
  bumps once released.

Based on the ADR-0004 client work in WithAutonomi#126.
@jacderida
jacderida force-pushed the adr-0003-quote-commitment-resolve branch from 872b29b to 059cc0a Compare July 9, 2026 16:58
@grumbach

grumbach commented Jul 9, 2026

Copy link
Copy Markdown
Owner Author

Superseded by WithAutonomi#151 — restaged onto WithAutonomi/ant-client main (the ADR-0004 base branch this was stacked on is now merged into main), with the test seams removed and the deps pinned to immutable revs.

@grumbach grumbach closed this Jul 9, 2026
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