Skip to content

feat(chunk): declare a settlement version on quote requests - #23

Open
grumbach wants to merge 5 commits into
WithAutonomi:mainfrom
grumbach:settlement-version-quote-gate
Open

feat(chunk): declare a settlement version on quote requests#23
grumbach wants to merge 5 commits into
WithAutonomi:mainfrom
grumbach:settlement-version-quote-gate

Conversation

@grumbach

@grumbach grumbach commented Aug 13, 2026

Copy link
Copy Markdown
Member

Linear issue

V2-975 — https://linear.app/autonominetwork/issue/V2-975/refuse-to-quote-clients-that-cannot-settle-correctly-instead-of

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

Wire types and the payment-admission path. T3 even though this crate only supplies the vocabulary and enforces nothing, because the vocabulary is the wire contract.

Compatibility

  • Wire: additive, no cutover. Two ChunkMessageBody variants and one ProtocolError variant are appended. Every existing discriminant keeps its value, so a peer built against 2.3.x decodes current traffic unchanged. A peer that does not know the new variants rejects them as an unknown discriminant rather than misreading a variant it does know. CHUNK_PROTOCOL_ID is unchanged at autonomi.ant.chunk.v1. Consequence to plan for: a client sending V2 gets no answer from a node built on 2.3.x, so nodes must be upgraded before clients start sending V2. The client PR handles the mixed fleet with a per-peer fallback.
  • Storage: none. No stored-data format is touched.
  • API: additive only. New public items are CURRENT_SETTLEMENT_VERSION, MIN_SUPPORTED_SETTLEMENT_VERSION, settlement_version_is_supported, client_update_required_message, ChunkQuoteRequestV2, MerkleCandidateQuoteRequestV2, and the two enum variants. Nothing is renamed, removed, or retyped. Both enums were already #[non_exhaustive], so downstream match arms are unaffected.

Semver impact

  • breaking
  • feature
  • fix

No version bump is taken in this PR; the release train owns that. Minor per the crate guide: "Add a new ChunkMessageBody variant (existing variants unchanged): minor".

Test evidence

cargo test --lib — 23 chunk tests pass, 0 failed. cargo clippy --all-targets --all-features -- -D warnings clean. cargo fmt --all clean. cargo doc builds.

Five tests added, one of which is load-bearing:

  • appending_v2_variants_leaves_existing_discriminants_untouched — encodes each existing message body and asserts its wire discriminant byte (PutRequest 0, GetRequest 2, QuoteRequest 4, MerkleCandidateQuoteRequest 6), then asserts the new variants take 8 and 9. This is the test the whole incremental-rollout design rests on: postcard assigns discriminants by declaration order, so inserting a variant anywhere but the end would make every older peer silently misread every message from a newer one. That property was previously assumed; it is now enforced.
  • client_update_required_is_appended_to_protocol_error — same guarantee for ProtocolError, which rides inside quote responses.
  • v2_quote_request_round_trips_with_the_settlement_version and v2_merkle_candidate_request_round_trips_with_the_settlement_version — encode/decode of both new requests.
  • settlement_gate_accepts_current_and_refuses_anything_older — including that a future settlement version is accepted, so an old node cannot veto a newer rule set it has never heard of.
  • update_required_message_tells_the_user_how_to_fix_it — asserts the wording carries ant update, "too old", and "nothing was charged". The wording is the deliverable for an end user, so it is pinned rather than left to drift.

No behavioural test here beyond encoding: this crate defines the vocabulary and enforces nothing. Enforcement evidence lives on the ant-node PR.

New dependency

none

ADR

ADR-0010: Settlement version and pre-payment compatibility (Proposed) — added on the ant-node branch in response to review.

https://github.com/WithAutonomi/ant-node/blob/settlement-version-quote-gate/docs/adr/ADR-0010-settlement-version-and-pre-payment-compatibility.md

Records the inclusive MIN..=CURRENT range and why the upper bound is load-bearing, the two refusal directions and why they must stay distinct during a client-first rollout, the unversioned retry as a bounded downgrade path with a compile-time cutover rule, and the residual that merkle storers are not exactly the peers a client quotes.

Builds on ADR-0008, whose re-open trigger ("a rise in refused batch uploads after the boundary") is what fired here.

Release readiness: coordinated set, do not merge alone

This crate is inert by itself: the new variants are never constructed unless a client sends them and a node acts on them, which are separate PRs. But merging publishes it, and publication is what unblocks the other two, so it should not land until the coordinated set is fleet-ready.

The open gates live on ant-node #204 and ant-client #171 and are enumerated there: mixed-version proof over a real connection, deployment ordering (nodes before clients, which has live latency impact if reversed), observability of the adoption counter, NAT/canary, rollback rehearsal and fleet safety.

Approved on the code, not a statement that the change set is production ready.

Mitigation / rollback

Revert the commit. Nothing in this crate changes behaviour on its own: the new variants are never constructed unless a client chooses to send them and a node chooses to act on them, and both of those are separate PRs. Until the ant-client PR ships, this release is inert on the wire.

ADR-0008 raised the merkle settlement multiplier to 3x in client code and
enforced it in node code, but changed no wire type. Nothing tied the two
together, so a client built before that change kept collecting quotes, paid
1x on-chain, and had the upload refused by every storer only after the
payment had settled. Merkle receipts are not refundable, so each occurrence
destroyed the user's money with no diagnosis beyond a paid/expected mismatch
in node logs.

Add a settlement version to the quote request so a storer can refuse to
quote a client it knows cannot pay correctly, before that client spends
anything. It is tracked separately from PROTOCOL_VERSION deliberately: that
constant describes what a peer can parse, this one describes how a client
turns a signed quote into an on-chain payment, and the two move
independently.

The change is additive. ChunkQuoteRequestV2 and
MerkleCandidateQuoteRequestV2 arrive as appended ChunkMessageBody variants
rather than fields on the existing structs, because ChunkMessage is
postcard-encoded and postcard is not self-describing: a new field silently
changes how every existing peer reads the message, while a new variant is
rejected cleanly as an unknown discriminant. ProtocolError gains an appended
ClientUpdateRequired variant carrying an upgrade instruction written for an
end user rather than an operator, including that nothing was charged.

A regression test pins the wire discriminants of the existing message and
error variants. The append-only property is what makes this safe to deploy
incrementally, and it holds only while declaration order is preserved, so it
is now enforced rather than assumed.

Enforcement policy lives in ant-node. This crate supplies the vocabulary.

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 19845c8c20e1a3505cfbfc446b7e2c26bf5b0726 as part of the coordinated protocol/node/client change.

Blocking concern:

  • settlement_version_is_supported accepts every version greater than or equal to MIN_SUPPORTED_SETTLEMENT_VERSION (src/chunk.rs:69-76). That does not preserve the stated pre-payment safety property for a future settlement change. A v1 node cannot know whether a v2 client will interpret its quote or construct payment in a way the v1 verifier accepts. Serving the quote therefore allows payment before compatibility is established—the failure this mechanism is intended to prevent. Please define an explicit supported range/capability (normally MIN <= version <= CURRENT) or document and test a mechanism that proves a future-version client cannot burn payment against an older node.

Release gates also remain open: the Security Audit job fails on RUSTSEC-2026-0220 (ruint 1.19.0; fixed in 1.20.0), although that dependency is unchanged from main. Local cargo test --all-targets passed 85/85 tests.

This is T3 wire/payment admission, so I would not merge the coordinated set until the compatibility policy is captured in the superseding ADR and exercised on a mixed-version dev testnet.

Review raised that accepting every version at or above the minimum does not
give the pre-payment guarantee this mechanism exists for. A node cannot know
whether a client settling under rules it has never seen will construct a
payment its verifier accepts, and by the time the verifier says no, the
client has settled on-chain and cannot be refunded.

The earlier reasoning held only for settlement changes that raise what is
paid. ADR-0008's 3x cleared an old node's 1x minimum, so old nodes accepted
new clients for free, and that case was mistaken for the general one. A
change that redefines the median rule, or which field the contract pays
from, breaks it.

Replace settlement_version_is_supported with settlement_compatibility,
returning Compatible, ClientTooOld or NodeTooOld over an inclusive
MIN..=CURRENT range.

The two refusals stay distinct on the wire because they need opposite
handling. ClientUpdateRequired is terminal and the user must upgrade.
StorerUpdateRequired says nothing about the client, which should use a
different storer and tell its user nothing. Collapsing them would either
tell up-to-date users to upgrade or strand new clients whenever the node
fleet lags, and a lagging fleet is the normal state during the client-first
rollout ADR-0008 prescribes.

StorerUpdateRequired is appended, so existing discriminants are unchanged and
the pinning test covers it.
Lockfile only, semver-compatible. The advisory predates this branch, but it
fails the Security Audit release gate, so clearing it here avoids blocking
the coordinated change on an unrelated pre-existing finding.

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review of exact head 439ed83d70ab3bbd14c1031c2e319f793141ea5a across the coordinated protocol/node/client set.

The blocking protocol concern from my previous review is resolved. settlement_compatibility() now bounds both ends of MIN..=CURRENT; a future client version is classified as NodeTooOld, and the two refusal directions have distinct append-only wire variants. Existing postcard discriminants remain pinned by tests.

The inherited ruint advisory is also fixed, local cargo test --lib --locked passed 87/87, and all 11 checks are green, including security audit and publish dry-run.

No protocol-code blocker remains. The PR body still describes the superseded behaviour in places (future versions accepted and stale test names); please refresh that before merge so the review record matches the implementation. Publication of 2.4.0 must precede repinning the coordinated node/client PRs.

Versioning is the release train's call, driven by the PR's Semver impact
box, not something a feature PR should pre-empt. Restores version 2.3.2 and
retitles the changelog entry as Unreleased, since the number is not known at
PR time.

No functional change.
The regression test that makes the append-only rollout safe covered only
request bodies 0/2/4/6 and the endpoints of ProtocolError. Reordering a
response variant, or any error variant between 1 and 6, would have passed
while breaking every peer built against the current release.

Responses read the same discriminant space as requests, so pin all of them.

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head e97901694cba5cad84da0b99ee8af02a791df2cf. The incremental changes complete the wire regression coverage by pinning the pre-existing response and ProtocolError discriminants, while correctly leaving the crate version bump to the release train. Bounded settlement compatibility and the two typed refusal directions remain sound. Local verification: cargo test --lib --locked passed 87/87 and formatting passed; all 11 GitHub checks are green. Approved.

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