Skip to content

feat: enforce quote valid-until-ms at submit via a quote ledger - #35

Open
mfw78 wants to merge 6 commits into
chore/nexum-runtime-pin-bumpfrom
feat/quote-staleness-ledger
Open

feat: enforce quote valid-until-ms at submit via a quote ledger#35
mfw78 wants to merge 6 commits into
chore/nexum-runtime-pin-bumpfrom
feat/quote-staleness-ledger

Conversation

@mfw78

@mfw78 mfw78 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What

  • Adds a Clock-injected quote ledger to VenueRegistry in crates/videre-host/src/registry.rs: record_quote stores a keccak256 digest of the quoted body keyed by (caller, venue, digest) mapped to valid_until_ms, and submit calls check_quote_freshness before deriving the header, refusing bytes this registry quoted once their valid_until_ms has elapsed (VenueError::Denied with a stale-quote: prefix).
  • Bounds the ledger so a guest cannot grow it unboundedly: QUOTE_HORIZON_MS (1 hour) skips recording quotations valid further out than that, and MAX_QUOTE_ENTRIES (1024) refuses new digests once the ledger is at cap, logging a warning rather than evicting a live entry.
  • Adds QUOTE_GRACE_MS (30s) so an expired entry survives a sweep triggered by any ledger touch, including a third party's; check_quote_freshness reads its own key before sweeping (so a just-expired entry is still checked ahead of eviction) and removes the entry explicitly on a stale refusal.
  • Gates the staleness check behind the existing submit quota so an over-quota caller cannot make the host hash an unbounded body.
  • Exports Clock and SystemClock from videre-host (src/lib.rs), and adds a with_clock builder method for injecting a test clock.
  • Adds alloy-primitives and auto_impl as videre-host dependencies (Cargo.toml) for the digest and the Clock forwarding impls.
  • Updates videre-sdk/src/client.rs doc comments on VenueTransport::quote and Quoted::submit to describe the host-side, best-effort staleness enforcement.
  • Adjusts crates/videre-host/tests/platform.rs to quote valid_until_ms: u64::MAX instead of a fixed past instant, since the test registry now runs on the system clock.
  • New unit tests in registry.rs: stale_quote_submit_is_denied, fresh_quote_submits, unquoted_body_submits_without_a_ledger_check, a_stale_quote_binds_its_caller_only, expired_entries_prune, a_long_expired_quote_still_refuses_its_owner, an_over_quota_submit_never_reaches_the_quote_ledger, a_stale_submit_is_refused_after_a_third_party_sweep, a_quote_past_the_horizon_is_not_recorded, the_quote_ledger_is_capped.

Why

Prices are only valid until valid_until_ms; without a host-side check, a stale quotation's bytes could still be submitted and accepted after the price had moved. The ledger closes that gap without adapter cooperation, since the check lives in VenueRegistry::submit rather than in each venue adapter. The grace-and-explicit-removal design closes a sweep-eviction hole: without a grace read, a third party's unrelated ledger touch could sweep a just-expired entry ahead of its owner's submit, letting a stale submit through unchecked.

Closes #14

Testing

All commands run in nix develop (rustc 1.94.0, cargo-nextest 0.9.127) on a fresh clone detached at origin/feat/quote-staleness-ledger (36df715). Touched crates over origin/chore/nexum-runtime-pin-bump..HEAD: videre-host, videre-sdk (plus Cargo.lock); no guest crate or videre-macros change.

  • cargo clippy -p videre-host -p videre-sdk --all-targets --all-features -- -D warnings - clean, zero warnings.
  • cargo test --doc -p videre-host -p videre-sdk --all-features - ok, 0 passed / 0 failed for both crates (neither carries a doctest; the videre-sdk/src/client.rs change is rustdoc prose).
  • RUSTDOCFLAGS='-D warnings' cargo doc --no-deps -p videre-host -p videre-sdk --all-features - clean, docs generated with no warnings.
  • just build-modules (echo-venue, echo-client, echo-keeper, flaky-venue for wasm32-wasip2, release) then cargo nextest run -p videre-host -p videre-sdk - all green.
  • Red-teamed separately against a fresh clone: re-derived the ledger from the code rather than the report and confirmed the digest identity holds end to end (videre-sdk's Quoted::submit forwards exactly the bytes quote priced, and the keeper's journal/reconcile paths never quote, so no in-tree submit path regresses); the cap/horizon/prune tests are non-vacuous under mutation. Found and fixed three real defects on the branch, pushed as 357d666, e7ee751, 36df715.

AI Assistance

Implemented with claude-fable-5, red-teamed with claude-opus-5, PR authored with claude-sonnet-5.

@mfw78

mfw78 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Deferred red-team finding filed: #38 (platform e2e tests silently skip when module wasms are absent; a zero-wasm run reports green). Surfaced while gating this PR.

mfw78 added 3 commits August 6, 2026 14:43
… ledger

The registry records every successful quote under (caller, venue,
keccak256(body)) with its valid-until-ms, and submit refuses the same
bytes as denied with the stable stale-quote: prefix once that instant
elapses. Unquoted bytes submit as before: quoting is not mandatory on
this wire. Expired entries sweep on every ledger touch, so growth is
bounded by live quotes. The wall clock sits behind an auto_impl Clock
seam with a SystemClock default, injected through VenueRegistryBuilder
so tests pin time.

Closes #14. Retired by the wire-level
quote binding tracked in #32.

AI Assistance: Claude Code used for implementation and tests.
record_quote admitted whatever valid-until-ms the adapter returned, so
the sweep only drained entries the adapter had already dated. Both
in-tree adapters quote u64::MAX, which never expires: every distinct
body a keeper priced pinned an entry for the process lifetime, and the
"bounded by live quotes" invariant held only vacuously.

A quotation valid past the one hour horizon is now left unrecorded. It
cannot go stale inside the horizon, so the entry could never have
produced a refusal, and skipping it costs no enforcement. A cap backs
that up: at MAX_QUOTE_ENTRIES a new digest is refused rather than a
live entry evicted, so a flooding caller leaves its own submits
unchecked instead of disarming another caller's. Both degrade to the
unquoted path, which already submits unchecked.

Also moves the scripted platform adapter off a 2023 valid-until-ms, so
a quote-then-submit added to those system-clock tests exercises the
path under test instead of a stale-quote refusal.

AI Assistance: Claude Code used for the review and this fix.
@mfw78
mfw78 changed the base branch from main to chore/nexum-runtime-pin-bump August 6, 2026 14:52
@mfw78
mfw78 force-pushed the feat/quote-staleness-ledger branch from df47837 to 08b9fd8 Compare August 6, 2026 14:52
mfw78 added 3 commits August 6, 2026 15:15
The client rustdoc promised a refusal outright, but a quotation valid
past the recording horizon is never recorded, which is what both in-tree
adapters emit, and neither is one quoted with the ledger full. Correct
the cap's own note too: a full ledger leaves later quotes unrecorded for
every caller, not just the flooder.
The check digests a guest-supplied body and sweeps the shared ledger, so
running it ahead of resolve and the per-caller quota let an over-quota
caller spend host CPU on every submit, against the file's own rule that
a gated caller reaches nothing. Fold the duplicated sweep predicate into
sweep_quotes alongside the other pruners, and skip the digest entirely
when the ledger is empty.
The read-before-sweep ordering the freshness check rests on had no test:
inverting the two lines left the whole suite green while every entry
past the grace became a silent pass.
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