Skip to content

Receiver-side price floor for single-node store admissions#176

Open
grumbach wants to merge 2 commits into
WithAutonomi:mainfrom
grumbach:single-node-price-floor
Open

Receiver-side price floor for single-node store admissions#176
grumbach wants to merge 2 commits into
WithAutonomi:mainfrom
grumbach:single-node-price-floor

Conversation

@grumbach

Copy link
Copy Markdown
Collaborator

Summary

Node-side companion to ant-client #150 (one-quote uploads). ADR-0004 gives every quote a price ceiling (price = f(committed key count)); removing the old local floor (b52f4577) left no revenue floor. Since the verifier accepts 1..=7 quote proofs and fully validates only the median candidate, a modified client can fetch quotes from the whole neighbourhood and settle only the cheapest valid one. The flexible bundles from #136 were safe while the old floor stood; the combination is the hole, and one-quote clients make it routine.

This PR restores the floor on the receiver, priced from the receiver's own live commitment:

settled_amount >= 3 x tolerance% x calculate_price(local committed responsible key count)

alongside the existing settled >= 3 x median check. The floor compares the settled amount, so an honest client may overpay a cheap quote to clear stricter receivers.

Design points

  • Same pricing basis as our own quotes. The floor reads the commitment via a new non-mutating CommitmentSource::current_binding_snapshot() — the exact source QuoteGenerator prices from, wired in AntProtocol::attach_commitment_source. Never current_chunks() (the unlike-counts comparison that false-rejected honest quotes under the pre-ADR-0004 floor), and never current_binding_for_quote() (which would extend commitment answerability as a side effect).
  • No commitment = baseline floor. A fresh/retired/restarting receiver (or one with nothing attached) prices the floor at calculate_price(0), making it vacuous rather than an outage mode.
  • Shadow-first rollout. Default is observe-only: one telemetry line per evaluated admission (target ant_node::payment::price_floor) with paid price, settled amount, local count/price, and the would-reject verdict. Enforcement is a per-node canary opt-in via ANT_PRICE_FLOOR_ENFORCE=1 (tolerance override ANT_PRICE_FLOOR_TOLERANCE_PERCENT, default 50%); unsetting it is the kill switch. Clients need 4-of-20 stores, so canary enforcement cannot fail honest uploads while telemetry calibrates the tolerance.
  • Scoped to fresh economics only. New VerificationContext::FreshReplication splits fan-out from direct PUT — verified identically everywhere (store-strength cache, cross-check, first-audit) so behaviour is unchanged; the floor applies to both (a single cheap accepting node must not fan a below-floor proof around everyone else's floor), and never to paid-list admission, repair, or cache hits. No historical receipt is repriced. Merkle proofs untouched.
  • Economic decision only. A floor rejection is a payment decline; it feeds no trust/misbehaviour scoring.

Tests

  • Cheapest-of-K adversarial gate: a 3x-baseline settlement against a fuller receiver is rejected under enforcement, on both direct PUT and fresh replication.
  • Exactly-at-floor overpayment accepted; shadow mode never rejects; no-commitment receivers stay vacuous; paid-list admission never repriced.
  • Full lib suite (692 tests), clippy (all targets + -D warnings), fmt, and docs clean.

Rollout

  1. Ship in shadow mode fleet-wide, watch price_floor telemetry for the settled/local-price ratio distribution.
  2. Enable enforcement on a few canary nodes, confirm zero honest rejections.
  3. Widen enforcement ahead of the one-quote client becoming the common path; tune tolerance from data.
  4. Follow-up: companion ADR documenting the floor as its own economic policy (it amends ADR-0004's "a node may always charge less").

…ions

ADR-0004 binds every quote's price to its issuer's committed key count — a
price CEILING. Removing the old local floor (b52f457) left no revenue floor
at all: the node verifier accepts 1..=7 quote proofs and fully validates only
the median-priced candidate, so a modified client can fetch quotes from the
whole neighbourhood, settle only the cheapest valid one 3x, and every receiver
accepts. The flexible-bundle change (WithAutonomi#136) was safe while the old floor stood;
their combination is the hole, and the one-quote client (ant-client WithAutonomi#150)
makes that path routine.

Add the floor half, priced from the SAME live commitment the receiver's own
QuoteGenerator uses (committed responsible key count via a new non-mutating
CommitmentSource::current_binding_snapshot — never current_chunks(), whose
divergence from commitment counts is what false-rejected honest quotes under
the pre-ADR-0004 floor; and never current_binding_for_quote(), which would
extend answerability). Requirement on single-node store admissions:

    settled >= 3 x tolerance% x calculate_price(local committed key count)

alongside the existing settled >= 3 x median check, i.e. an honest client may
overpay a cheap quote to clear stricter receivers. No live commitment (fresh
node, retired, restart window, unwired) prices the floor at baseline — vacuous,
never an outage mode.

Shadow-first rollout: default is observe-only with one telemetry line per
evaluated admission (target ant_node::payment::price_floor) recording paid
price, settled amount, local count/price, and the would-reject verdict.
Enforcement is per-node via ANT_PRICE_FLOOR_ENFORCE=1 for canary rollout
(tolerance override: ANT_PRICE_FLOOR_TOLERANCE_PERCENT, default 50); unsetting
it is the kill switch. Clients need 4-of-20 stores, so canary enforcement
cannot fail honest uploads while telemetry calibrates the tolerance. A floor
rejection is an economic admission decision only, never trust evidence.

Scope: split VerificationContext::FreshReplication out of ClientPut — verified
identically everywhere (store-strength cache, cross-check, first-audit) so the
floor's telemetry can distinguish direct ingress from fan-out and the fan-out
policy stays tunable without touching direct PUTs. The floor applies to both
(one cheap accepting node must not fan a below-floor proof around everyone
else's floor) and NEVER to paid-list admission, repair, or cache hits — no
historical receipt is repriced. Merkle proofs are untouched.

Tests: cheapest-of-K settlement rejected under enforcement (direct PUT and
fresh replication), exactly-at-floor overpayment accepted, shadow mode never
rejects, no-commitment receivers stay baseline-vacuous, paid-list admission
never repriced.
Copilot AI review requested due to automatic review settings July 14, 2026 05:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a receiver-side “price floor” policy to single-node payment verification so a node can reject (or shadow-log) underpaid store admissions even when a client settles only the cheapest valid quote from a bundle.

Changes:

  • Introduces PriceFloorConfig (shadow by default; env-controlled enforcement + tolerance) and applies it during single-node proof verification for store-admission contexts.
  • Splits VerificationContext with a new FreshReplication variant and unifies cache/admission semantics via is_store_admission().
  • Adds CommitmentSource::current_binding_snapshot() and wires the same commitment source into both quoting and the verifier’s price-floor computation; adds unit/e2e config wiring and tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/e2e/testnet.rs Adds default price_floor config into e2e testnet verifier construction.
tests/e2e/data_types/chunk.rs Wires PriceFloorConfig::default() into verifier config used by chunk e2e tests.
src/storage/handler.rs Forwards the attached commitment source to the payment verifier (in addition to the quote generator).
src/replication/mod.rs Uses VerificationContext::FreshReplication for fresh-offer payment verification and updates the associated test.
src/replication/commitment_state.rs Implements current_binding_snapshot() for ResponderCommitmentState without mutating answerability.
src/payment/verifier.rs Implements PriceFloorConfig, adds store-admission context predicate, enforces/logs receiver-side floor after settlement verification, and adds targeted tests.
src/payment/quote.rs Extends CommitmentSource trait with current_binding_snapshot() and updates test implementation.
src/payment/mod.rs Re-exports PriceFloorConfig and the env var names for external configuration.
src/node.rs Initializes verifier price_floor from env and logs when enforcement is enabled.
src/devnet.rs Sets default price_floor config for devnet verifier construction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/payment/verifier.rs
Comment on lines +1106 to +1110
let local_key_count = source
.and_then(|s| s.current_binding_snapshot())
.map_or(0u32, |binding| binding.key_count);
let local_price = calculate_price(usize::try_from(local_key_count).unwrap_or(usize::MAX));

The floor is its own economic policy, not part of ADR-0004's ceiling: it
amends "a node may always charge less" to "a receiver may refuse a payment
that settles below its own commitment-priced floor". Records the pricing
basis (committed responsible count via non-mutating snapshot), the scope
(store admissions only, no historical repricing), the shadow-first rollout
with per-node canary enforcement, and the validation gates.
Copilot AI review requested due to automatic review settings July 14, 2026 05:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

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