Skip to content

feat(antd): opt-in signed-quote exposure + stateless VerifyQuotes - #242

Open
Nic-dorman wants to merge 4 commits into
mainfrom
feat/signed-quote-exposure
Open

feat(antd): opt-in signed-quote exposure + stateless VerifyQuotes#242
Nic-dorman wants to merge 4 commits into
mainfrom
feat/signed-quote-exposure

Conversation

@Nic-dorman

Copy link
Copy Markdown
Member

Summary

Hosted-payments fraud control (Linear V2-854): in hosted mode the party that pays is not the party that collected the quotes — the payer receives payments[] triples from a customer-controlled instance and must not trust them (a fabricated triple could name any address and amount). This PR gives the payer the means to verify a batch offline before settling it, in two halves:

1. Opt-in signed-quote exposure (include_signed_quotes)

All three prepare endpoints (upload / data / chunk, REST + gRPC) accept the flag. Wave-batch responses then carry signed_quotes[] — one entry per payments[] triple with the full signed PaymentQuote and its ADR-0004 commitment sidecar as opaque base64(msgpack) bytes (only antd ever parses them):

  • Entries are restricted to the payment intent's paid quote set — the network quotes the whole close group (~7 peers/chunk) but single-quote payments (V2-619) pay only the median, so signed_quotes[] is guaranteed 1:1 with payments[].
  • Sidecars (ant-core's compacted, non-index-aligned vector) are matched to quotes by commitment_hash(sidecar) == quote.commitment_pin at emit time, so each entry is self-contained.
  • Default-off: ~5–6 KB per quote + up to 8 KB per sidecar; existing consumers see zero change.
  • Merkle prepares never populate it — candidate exposure is blocked upstream (private candidate pools; resolved commitments deliberately discarded, regression-test-enforced) and is specced on V2-934.

2. Stateless offline verification (POST /v1/verify/quotes + gRPC VerifyService)

A pure function of the request — no network, wallet, or session state — run by "the party about to pay" on its own antd. Per entry (port of ant-core's client-side quote_commitment_binding_is_valid, with the peer identity derived from the quote's own pub_key):

  1. quote_hash == hash(signed_quote) — the triple is tied to a concrete quote
  2. ML-DSA-65 signature over the paid fields
  3. Paid-fields equality: rewards_address exact, amount == 3× signed price (the single-node payment multiplier — mirrored as a documented local const since ant-core's is pub(crate))
  4. ADR-0004 resolve-before-pay binding: sidecar present for pinned quotes, signed under the quote's key, commitment_hash == pin, count equality, and price == calculate_price(count) by exact recomputation (baseline == calculate_price(0))

Verdicts carry the fields caller-side policy needs (timestamp, content XorName, price, rewards address, key count, pinned). Policy itself (expiry, replay ledgers, chunk-set equality, count caps) deliberately stays caller-side. CPU-bound work runs on spawn_blocking; 1024-entry cap.

Dependencies

  • New direct dep ant-protocol = "=2.3.2" — pinned exactly to the already-locked transitive version so PaymentQuote/StorageCommitment can't fork in the graph. All PQC verification routed through its re-exports (no new saorsa deps).
  • xor_name as dev-dependency (test quote construction).
  • ant-core pin unchanged (ant-cli-v0.3.3). No ant-client changes involved.

Testing

  • 58 Rust tests (8 new: genuinely-signed ML-DSA quotes + commitments, tamper cases, builder association); antd-go build/vet/tests incl. new httptest coverage; clippy + fmt clean.
  • Live devnet smoke (24-node local devnet + anvil): prepare with the flag → 1:1 signed quotes → VerifyQuotes passes on real network quotes; inflated amount, redirected payee, and fabricated quote_hash each rejected with the named rule. Two real-network corrections came out of this (paid-set filter; 3× multiplier) — both covered by unit tests now.

Docs

docs/external-signer-flow.md (exposure + verification recipe + policy split), antd/openapi.yaml (new path + 5 schemas), proto comments throughout.

Release note

Version bump left out per house convention (separate chore/bump-* PR at release time — docs reference the feature as "antd 0.13.0").

🤖 Generated with Claude Code

Nic-dorman and others added 3 commits August 24, 2026 10:49
…ST + gRPC)

Hosted payments fraud control (V2-854 work items 1 + 1b): in hosted mode the
party that pays is not the party that collected the quotes, so the payer must
be able to verify a payment batch offline before settling it.

Exposure: `include_signed_quotes` on the three prepare endpoints. Wave-batch
responses carry `signed_quotes[]` — one entry per payments[] triple with the
full signed PaymentQuote and its ADR-0004 commitment sidecar as opaque
base64(msgpack) bytes (sidecars matched to quotes by commitment_hash == pin;
entries restricted to the payment intent's paid quote set, since the network
quotes the whole close group but single-quote payments pay only the median).
Default-off; existing consumers see no change. Merkle candidate exposure is
blocked upstream (private candidate pools, commitments deliberately
discarded) and rides V2-934.

Verification: POST /v1/verify/quotes + gRPC VerifyService. Stateless offline
port of ant-core's quote_commitment_binding_is_valid, run by the party about
to pay on its own antd: quote-hash recomputation, ML-DSA-65 signature,
paid-fields equality (amount == 3x the signed price — the single-node payment
multiplier), and the ADR-0004 binding with exact on-curve pricing. Verdicts
carry the extracted fields caller-side policy needs (timestamp, content,
price, rewards address, key count, pinned).

New direct dep ant-protocol = "=2.3.2", pinned to the locked transitive
version; all PQC verification routed through its re-exports.

Verified against a live local devnet: real-quote batch verifies; inflated
amount, redirected payee, and fabricated quote_hash are all rejected with
named rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Go client surface for the V2-854 daemon additions: PrepareOptions with
IncludeSignedQuotes (PrepareUploadWithOptions / PrepareDataUploadWithOptions /
PrepareChunkUploadWithOptions), SignedQuoteEntry parsing on both prepare
results, and VerifyQuotes with per-entry verdict models. Quote and sidecar
blobs stay opaque — the client relays them, only antd parses them.

Generated protobuf refresh for the new fields + verify.proto service.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generated PrepareChunkRequest/PrepareFileUploadRequest/
PrepareDataUploadRequest structs gained include_signed_quotes and the
prepare responses gained signed_quotes; antd-rust's exhaustive struct
literals must name them. Requests default to false (no behaviour change);
the antd-rust client surface for the new options/endpoint can follow
separately if wanted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman force-pushed the feat/signed-quote-exposure branch from bc14d47 to 419c57c Compare August 24, 2026 10:23
Newer stable clippy (CI's @stable moved past the last antd-rust run) flags
every tonic-generated client method's Result<_, tonic::Status> as
result_large_err — 24 hits across all services, none actionable in
generated code. Scoped allow on the include_proto module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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