Skip to content

feat: full-node shunning — client implementation (ADR-0002)#134

Merged
jacderida merged 8 commits into
rc-2026.6.4from
feat/full-node-shunning-client
Jun 29, 2026
Merged

feat: full-node shunning — client implementation (ADR-0002)#134
jacderida merged 8 commits into
rc-2026.6.4from
feat/full-node-shunning-client

Conversation

@mickvandijke

@mickvandijke mickvandijke commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Client-side implementation of ADR-0002 (extended PUT fallback for full-node shunning). Stacked on the ADR PR #133 (base: docs/full-node-shunning-adr), so this diff is the implementation + tests only.

  • feat(client): classify node rejections (full StorageFailed / price-floor Payment / PaymentRequired / transport) and fall back past full or over-priced close-group members, reusing the same proof (no re-quote / re-pay).
  • refactor(client): unify chunk PUT targets on the witnessed K-closest set — one witnessed query returns the closest 20 (graceful fallback to the close-group width on sparse networks); quote/pay the quorum-witnessed consensus close group (CLOSE_GROUP_SIZE, sorted by XOR — intentionally not strictly the raw initial closest-7; see the ADR-0002 quote-authority invariant; payment byte-for-byte unchanged); put to all 20; the redundant local close_group_peers query and the on-demand extended-fetch are removed.

Review follow-ups (a1f0f8f):

  • fix: an all-PaymentRequired shortfall is now surfaced as an application error (not InsufficientPeers), so it no longer pushes the store AIMD limiter down as a false capacity signal; decision extracted into put_shortfall_error() with a unit test.
  • ci: added --features test-utils to the e2e step so the gated fallback test actually runs (verified passing with test-utils alone).
  • docs: reworded the fallback test to state precisely what it proves (single-proof reuse + fallback to genuine next-closest peers + quorum, failures driven by unreachable peers); a live node returning StorageFailed needs a node-side capacity hook the MiniTestnet lacks (follow-up). Stated the quote-authority invariant in ADR-0002.

Tests: e2e fallback proof + classification & shortfall unit tests; validated end-to-end across e2e_chunk / e2e_merkle / e2e_file (every upload path) on a live MiniTestnet; 240 client unit tests pass; clippy --all-targets --all-features -D warnings + fmt clean.

🤖 Generated with Claude Code

mickvandijke and others added 8 commits June 26, 2026 12:11
Client-side role in the full-node-shunning plan: classify node rejections (full vs price-floor vs transport) instead of flattening to RemotePut, and extend put fallback beyond the quoted close group to next-closest peers within the K=20 issuer-closeness window, reusing the same ProofOfPayment (no re-quote, no re-pay). Quorum stays at 4; reads rely on close-group convergence (see saorsa-node ADR-0003).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements ADR-0002. On a PUT shortfall the client now falls back beyond the quoted close group to the chunk's next-closest peers within a 20-wide neighbourhood (PUT_FALLBACK_WIDTH, mirroring the node's K_BUCKET_SIZE issuer-closeness window), reusing the same ProofOfPayment — no re-quote, no re-pay. Rejections are classified (StorageFailed=full, PaymentFailed=price-floor, transport) to surface a legible aggregate error and keep the store AIMD limiter honest. Quorum stays at CLOSE_GROUP_MAJORITY. Adds a unit test for the classification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a test-utils feature and a Client::chunk_put_with_dead_initial_peers seam that pays normally then stores with CLOSE_GROUP_MAJORITY unreachable initial peers, so every initial send fails and quorum can only be reached via the ADR-0002 extended fallback fetching the chunk's real next-closest peers and reusing the same proof. The e2e_chunk test asserts the store succeeds and the chunk is retrievable. Runs on the in-process MiniTestnet (cargo test --features devnet,test-utils).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per design discussion: the single-node witnessed query now asks for the closest PUT_TARGET_WIDTH (K=20) peers directly, with a graceful fallback to the close-group width on sparse networks. The witnessed quote/quorum/payment is scoped to the closest CLOSE_GROUP_SIZE (7) via a 7-scoped transcript, so payment semantics are byte-for-byte unchanged. StoreQuotePlan.put_peers now carries the 20 directly-contacted put-targets, which the single-chunk and file Single paths already forward. The merkle path's put-targets widen via the new put_target_peers helper (closest 20). chunk_put_to_close_group drops the on-demand extended-fetch entirely and just walks the supplied K-closest set, removing the redundant second DHT query. Validated: 26 quote unit tests, 238 client unit tests, e2e_chunk (10), e2e_merkle (4), e2e_file (8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A node's `ChunkPutResponse::PaymentRequired` surfaces client-side as
`Error::Payment`, which `classify_put_failure` previously bucketed as a
transport failure. That let an application-level payment decline count
toward the transport tally, wrongly forcing the capacity-signal path and
pushing the store AIMD limiter down (contrary to ADR-0002 / V2-468).

Classify `Error::Payment` as `PriceFloor`, matching the price-floor
`PaymentFailed` rejection, so a mixed shortfall surfaces the
representative `RemotePut` instead of `InsufficientPeers`. Extends the
classifier unit test to cover the mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uery

The single-node quote path widens the witnessed lookup to PUT_TARGET_WIDTH
and relies on scope_witnessed_to_close_group to keep payment semantics
byte-for-byte identical to a CLOSE_GROUP_SIZE-wide query. That invariant
had no test.

Add a unit test that scopes a wide transcript (with out-of-close-group
responder views) and asserts target preservation, close-group truncation,
verbatim view contents, and identical missing-views/quorum/consensus math
versus a hand-built native close-group transcript.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alls

Addresses PR #134 review.

1. (bug) chunk_put_to_close_group only captured Error::RemotePut as the representative shortfall error, so an all-PaymentRequired (Error::Payment) shortfall fell through to InsufficientPeers — classified NetworkError — and wrongly pushed the store AIMD limiter down as a capacity signal. Now the first application-level decline (RemotePut OR Payment) is captured, and the shortfall decision is extracted into put_shortfall_error() with a unit test: all-application shortfall surfaces the app error (ApplicationError), only a transport-tainted shortfall is InsufficientPeers.

2. (ci) The e2e fallback test was behind #[cfg(feature = test-utils)] but CI ran e2e without it, so it never ran. Added --features test-utils to the e2e_chunk CI step (ant-node is a dev-dep, so no devnet needed); verified the test runs and passes with test-utils alone.

3. (test honesty) Reworded the fallback test doc to state exactly what it proves — fallback to genuine next-closest peers on a single reused proof (no re-quote/re-pay), reaching quorum, with failures driven by unreachable peers — and that a live node returning StorageFailed needs a node-side capacity hook the MiniTestnet lacks (follow-up); the StorageFailed/price-floor classification is covered by the classify_put_failure unit test.

4. (doc) Stated the SNP quote authority invariant in ADR-0002: the quoted/paid set is the quorum-witnessed consensus close group (sorted by XOR, capped at CLOSE_GROUP_SIZE), intentionally not strictly the querying node's raw initial closest list; the widened query only enlarges the put set and responder pool, payment is byte-for-byte unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mickvandijke
mickvandijke changed the base branch from docs/full-node-shunning-adr to main June 29, 2026 16:36
@jacderida
jacderida changed the base branch from main to rc-2026.6.4 June 29, 2026 22:17
@jacderida
jacderida merged commit e02b6ec into rc-2026.6.4 Jun 29, 2026
1 check passed
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.

2 participants