Skip to content

fix(replication): cap first-audit pending admission at the launch budget#183

Open
grumbach wants to merge 21 commits into
WithAutonomi:mainfrom
grumbach:fix/first-audit-admission-cap
Open

fix(replication): cap first-audit pending admission at the launch budget#183
grumbach wants to merge 21 commits into
WithAutonomi:mainfrom
grumbach:fix/first-audit-admission-cap

Conversation

@grumbach

@grumbach grumbach commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #177 (stacked on its head; see merge-order note below). Sizes first-audit pending admission to what the launch budget can actually audit.

#177 bounded launches (token bucket) and expired dead entries eagerly, but admission was still unbounded relative to the budget: pending shared the 4096-entry commitment-cache cap while the token bucket can launch at most ~31 audits inside one effective answerability window. On the 2026-07-17 DEV-02 staging run this meant ~113 admitted nominations per node-hour against ~12 launches: over 99% of admitted work could never launch, pending sat at ~331-431k fleet-wide, and the oldest-pending age tracked the age of the deployment itself — an aging backlog whose steady state is mass expiry.

Changes

  • pending is capped at FIRST_AUDIT_PENDING_CAP, derived from the strict reserve-time launch horizon (burst at age zero plus every refill that stays answerable through max jitter + send slack; currently 31). The unit test simulates the shipped predicate instant-by-instant and must agree with the derivation. The dedup set keeps the 4096 bound.
  • Overflow follows two rules, in order (review feedback):
    1. Reservation before destructive overflow — an arrival that would displace a different peer first gives the queue a launch opportunity; with a token free, one entry moves into the reservation and admission proceeds with no eviction, so an ingress burst cannot flush eligible work past a ready token.
    2. Random-victim displacement — when no launch is possible, a uniformly random incumbent is displaced, counted as capacity_evicted. Per-nomination eviction probability of a specific target is 1/cap regardless of arrival order: suppressing it with 95% confidence costs ~95 distinct-peer paid nominations and is never certain (deterministic keep-newest LRU, flagged in review, allowed a certain flush with 32).
  • The summary line gains pending_cap and reserved (the single reservation held outside the queue), so schedulable occupancy pending + reserved is directly computable in Elasticsearch.
  • ADR-0004 Amendment 4 documents the admission sizing, the two overflow rules, and the eviction-cost math.

Evidence

  • 745 lib tests pass. Regressions include: ordered-flood non-determinism at the drain/reserve boundary (pending_cap < 60 <= FIRST_AUDIT_DRAIN_BATCH, 100-trial statistical bounds with miss odds < 1e-20), reserve-before-eviction at capacity, displaced-peer re-nomination, cap-vs-predicate simulation.
  • Expected staging signals: pending ≤ 31 with oldest_pending_quote_age_ms far below the answerability window (queue drains instead of aging), overflow visible as capacity_evicted growth at admission time instead of outside_answerability_window mass expiry hours later.

Semver

Patch: no protocol or config-surface change; two additive fields in the summary log line.

Tracking: V2-738

Copilot AI review requested due to automatic review settings July 23, 2026 04:19
@grumbach

grumbach commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Merge order: this PR is stacked on #177 and must be merged AFTER it. Until #177 lands, this diff shows #177's commits too; the admission-cap change itself is the top two commits (4309dac + review fixes in 3462edf).

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

This PR tightens ADR-0004’s monetized first-audit pipeline so the pending admission queue is sized to what the launch budget can realistically drain within the answerability window, preventing an ever-aging backlog under sustained nomination load. It also expands observability (audit outcome/drops counters, scheduler summary fields) and updates payment-verifier nomination behavior and ADR documentation.

Changes:

  • Cap first-audit pending admission at FIRST_AUDIT_PENDING_CAP derived from the launch budget + effective answerability window, with LRU displacement counted as capacity_evicted.
  • Add cumulative audit outcome/drop accounting and emit it alongside the existing replication traffic summary.
  • Update payment verification to nominate only the actually-paid candidate(s), add settlement payee-prefix checking, and add an opt-in A/B e2e workload driver plus ADR amendments.

Reviewed changes

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

Show a summary per file
File Description
tests/e2e/mod.rs Registers the new opt-in A/B workload module.
tests/e2e/first_audit_ab.rs Adds an environment-gated A/B workload driver for production-shaped paid uploads + first-audit behavior comparison.
src/replication/protocol.rs Adds process-global audit pass/fail-by-reason and responder-drop counters + summary log.
src/replication/mod.rs Implements the capped pending scheduler + reservation/promotion flow, adds new scheduler observability, and wires audit outcome/drop recording.
src/replication/config.rs Documents and centralizes first-audit limiter constants (launch interval, burst, inflight, jitter, ingress capacity, etc.).
src/payment/verifier.rs Narrows deterministic first-audit nomination to paid candidates, adds settlement redirect rejection, and updates test hooks accordingly.
docs/adr/ADR-0004-commitment-bound-quote-pricing.md Adds ADR-0004 Amendments 2–4, including the pending admission sizing rationale and semantics.

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

Comment thread src/payment/verifier.rs
Comment on lines +966 to +976
if let Some(recorded) = recorded_rewards_prefix {
let expected = Self::rewards_address_prefix(&candidate.quote.rewards_address);
if recorded != expected {
return Err(Error::Payment(format!(
"Median-priced quote settlement for peer {:?} was redirected: recorded rewards address prefix {} does not match the quote's {}",
candidate.encoded_peer_id,
hex::encode(recorded),
hex::encode(expected)
)));
}
}
grumbach added 18 commits July 23, 2026 13:34
…mendment 2)

Production v0.14.3 showed the deterministic first-audit path amplifying
fleet-wide: launches scaled as uploads x pinned-quotes-per-proof x
verifying-storers, with hourly pin rotation defeating dedup and no
aggregate launch bound. The overflow tripped responder admission caps and
was recorded as peer Timeouts (545k/24h), degrading downloads ~31%.

Payments now NOMINATE pins; the clock LAUNCHES them:

- verifier: nominate only the settlement-verified paid candidate
  (single-node median issuer; merkle paid_node_addresses indices), not
  every quote in the bundle / candidate in the pool
- drainer: launch limiter with a token bucket (burst 2, one launch per
  5 min), an in-flight cap (2), and 0-30s launch jitter so one chunk's
  storers do not challenge the paid peer simultaneously
- per-peer re-audit window (2h) that survives pin rotation, with a >1.5x
  committed-count jump override so inflated sidecar-only pins are still
  re-nominated immediately
- budget deferrals are penalty-free and consume nothing; newest-monetized
  pins get budget first
- scheduler summary gains rate_deferred_attempts / window_deduped /
  tokens; outcome labels stay append-only vs WithAutonomi#173
- ADR-0004 Amendment 2 documents first audits as bounded best-effort
  sampling and restates the coverage guarantee
- tests/e2e/first_audit_ab.rs: env-gated production-shaped A/B workload
  driver (real Anvil settlement, quote sidecars, median payment,
  close-group PUTs, periodic rotation) for baseline-vs-fix comparisons
…limiter accounting

Review-round fixes for the first-audit scheduler change:

- Settlement-redirect rejection: completedPayments stores whatever
  (rewardsAddress, amount) the payer supplied for a quote hash, so the
  amount-only check accepted a payment the client redirected to its own
  wallet while the median issuer was treated (and first-audited) as paid.
  The verifier now compares the vault's bytes16 payee prefix against the
  quote's rewards address; validated against the real contract via the
  Anvil workload (honest pay_for_quotes settlements still verify).
- Drop-guarded in-flight slot: a panicking or cancelled first-audit task
  can no longer leak an in-flight slot and wedge the cap shut.
- Correct the launch-rate wording (sustained 12/h plus at most the burst
  of 2 in the first hour) in config docs and ADR-0004 Amendment 2.
- A/B workload driver only runs when FIRST_AUDIT_AB=1 exactly.
…fter jitter

Final review-round hardenings against the production-incident context:

- The verifier-to-drainer monetized-pin channel is now bounded
  (FIRST_AUDIT_INGRESS_CAPACITY = 1024); producers try_send and drop on
  full. Every stage of the first-audit pipeline is now capacity-limited:
  ingress queue, pending LRU, and launch-rate token bucket. A dropped
  nomination is penalty-free and the peer stays covered by the lottery.
- The A1 answerability screen runs again after the 0-30s launch jitter,
  so a pin can never be challenged outside its window regardless of how
  deferral time, jitter, and the skew margin compose. No false-conviction
  window remains by construction.
- ADR-0004 Amendment 2 documents the accepted residuals: the
  budget-exhaustion starvation economics (bounded by real settled payment
  costs, lottery backstop) and the PRE-EXISTING vault-level
  completedPayments overwrite race (already defeats the amount check with
  a 1-wei payment on all node versions; needs a contract-side fix,
  tracked as follow-up).
Anti-starvation lane for the launch budget: passes alternate between
newest-first (freshest answerability windows) and oldest-first, so a
stream of fresh settled decoy nominations cannot keep an aging pin from
launching before its eligibility window closes. Suppressing a pin now
additionally requires pre-aged pending decoys at every observer, which
the oldest lane itself drains and the per-peer re-audit window blocks
from refreshing.

Also restate the ADR-0004 Amendment 2 starvation residual with accurate
economics (decoy settlements hit a whole storer cohort at once, merkle
settlements nominate every paid index, principal recycles through sybil
reward addresses, and sidecar-only pins have no lottery backstop) and
correct over-claimed lottery-coverage comments: a dropped nomination is
re-covered by the peer's next settled payment, the lottery covers only
gossiped commitments.
… per pass

The alternating newest/oldest lane flipped on every drainer pass, but most
passes spend no token (the bucket is empty between refills) and a pass is
triggered by incoming nominations as well as by the retry tick. An attacker
could therefore inject a nomination to force a barren scan, flip lane parity,
and keep every token-bearing pass on the newest lane — starving an aging pin
with nothing but fresh decoy settlements, no pre-aged decoys required.

The lane now advances only when a launch token is actually spent, so
consecutive LAUNCHES strictly alternate whatever the scan pattern in between,
and the oldest lane cannot be skipped. Adds a regression test modelling the
barren-scan attack, and restates the Amendment 2 residual: the remaining
suppression routes (pre-aged decoys at every observer, or evicting the target
from the 4096-entry pending LRU) both cost real settled payments at scale.
… per pass

The lane still advanced once per launching pass, so a full burst spent both
tokens in the same lane: the launch sequence could be newest, newest, oldest
instead of a strict alternation, and the anti-starvation argument did not
hold for burst passes.

The launch loop now pops candidates from a deque — front for the newest lane,
back for the oldest — and flips the lane at every committed launch, so
consecutive launches strictly alternate including within one burst pass.
Deferred entries carry their snapshot index and are re-inserted oldest-first,
restoring LRU recency regardless of which end they were popped from.

The regression test now models the real loop (deque + multi-token passes) and
rejects both wrong flip rules: per pass (attacker steers parity with barren
scans) and once per launching pass (a burst keeps one lane).

ADR-0004 Amendment 2 restated accordingly, and the residual no longer claims
suppression cannot be indefinite: evicting a target from an observer's
4096-entry pending LRU permanently drops that nomination, after which
coverage depends on the peer's next settled payment (or the ADR-0002 lottery
for a gossiped commitment).
…d telemetry

Prod-readiness review follow-ups (both minor/nit, no behavior change):

- Count nominations dropped at the bounded ingress channel when it is full
  (Full only, not Closed) in a process-global counter, surfaced as
  ingress_dropped in the 5-minute scheduler summary. A non-zero value is the
  rollout signal that nomination ingress is saturating — benign but the thing
  to watch when widening the rollout.
- Document that launched counts SCHEDULED audits (token spent); a pin that
  ages out at the post-jitter answerability re-screen is counted under
  outside_answerability_window and sends nothing, so actual wire challenges
  are launched minus post-jitter aborts, reconciled by the terminal counters.
…ppression

Blocking review finding (dirvine): the drainer committed durable suppression
(first_audited + the 2h per-peer window + token + launched) at LAUNCH time,
then the child slept for jitter and re-checked answerability; a quote near the
answerability boundary could obtain full 'already audited' suppression and drop
a same-count successor for two hours despite no audit ever sending.

Reworked to a drainer-owned reserve/promote/cancel scheduler (consensus design
with codex xhigh over four rounds):

- Payments NOMINATE; the drainer holds at most ONE reservation at a time
  (preserving per-launch lane alternation), consuming a token and an in-flight
  slot but stamping NO suppression.
- A jitter timer (select arm) fires at the reservation's ready_at; the
  AUTHORITATIVE answerability + shared-cooldown check-and-stamp runs then, under
  the cooldown write lock, immediately before the send. Only on that pass are
  first_audited/recent stamped, the lane flipped, and launched counted.
- A cancelled reservation (answerability lapsed during jitter, or a gossip
  audit won the cooldown) refunds the token, releases the slot, and leaves NO
  durable state — nothing to roll back. A cooldown-race requeues the event.
- A same-peer successor arriving during a reservation is retained (window
  bypass for the reserved peer), never dropped, and becomes the next
  reservation if the first cancels.
- B horizon prefilter: a nomination is only admitted if it stays answerable
  through now + max_jitter + slack, so a committed reservation can still be
  challenged at send; the authoritative check remains at promotion so A1
  holds regardless of jitter-vs-window sizing.

All scheduler state mutation stays single-threaded in the drainer; the spawned
child only runs the audit I/O and holds the moved-in in-flight slot.

Tests: horizon-prefilter boundary; answerability-cancel is state-neutral and
retains a same-count successor (the reviewer's exact hole); per-launch lane
alternation driven through reserve/resolve. 699 lib tests + cfd green; e2e
smoke on a live anvil testnet exercises reserve->promote end to end.
…ination

Codex review: resolving a reservation that loses the shared-cooldown race
requeued the reserved event unconditionally, which could overwrite a newer
same-peer successor (a count jump) already pending, losing coverage. Requeue
only when no same-peer entry is pending; the successor arrived later so it is
the newer nomination. Adds a regression test.
…granularity in test

- pending_len/tokens are used only by the scheduler summary log and unit
  tests, both absent from cargo build --release --no-default-features, so they
  tripped dead_code under RUSTFLAGS=-D warnings. Gate the lint the same way the
  crate already gates logging-only unused items.
- first_audit_horizon_prefilter_boundary used a 1ns epsilon; Windows SystemTime
  has 100ns (FILETIME) granularity, so the nanosecond step rounded to the same
  instant and the boundary assertion failed there. Use 1µs, exact on every
  platform.
…cannot erase an inflated one

Re-review blocker (dirvine): per-peer pending coalescing replaced same-peer
work by arrival order only, so a peer could suppress an inflated (audit-worthy)
sidecar-only commitment for the cost of one cheaper same-peer settlement — the
count-jump override only compares against the last AUDITED count and sidecar-
only pins have no gossip-lottery backstop.

Coalescing is now highest-count-per-peer (newest on an equal-count tie): a
strictly-lower-count nomination never displaces a higher-count pending pin, and
a suppressed lower nomination leaves the retained pin's LRU recency untouched
(so low-count floods cannot steer the lane). The same rule governs the
cooldown-race requeue of a reserved event (incoming_is_newer=false, so a
higher-count reserved event beats a lower successor), which now also counts a
capacity eviction in the funnel (previously unobserved).

ADR-0004 Amendment 2 restated (highest-count coalescing + accepted residual:
a single per-peer slot may drop the lower fallback if the retained higher pin
later ages out). Non-blocking notes documented: single-node tied-median
nominates only the first settled candidate (sidecar-only extras are best-
effort residual; gossiped extras keep the lottery); the re-audit window is
stamped at launch not outcome (Transient/silence is not auto-relaunched) — both
to be quantified in the matched staging run.

Tests: coalescing keeps highest count + exposes eviction; pending high vs later
lower; reserved high vs cooldown-race lower successor. 702 lib tests + cfd
green.
…d edge tests

Review nits (both non-blocking): add a dedicated suppressed_lower funnel
counter so a burst of attempted cheaper-pin self-erasure (a lower-count
same-peer nomination dropped so a higher-count pin survives) is visible in the
scheduler summary, on both the enqueue and cooldown-race requeue paths; add
edge tests for (a) a suppressed-lower flood leaving the retained pin's LRU
recency/lane position unchanged (peek, no push) and (b) the cooldown-race
requeue counting a different-peer capacity eviction. ADR-0004 funnel list
updated. 704 lib tests + cfd green.
…he funnel

Codex review: SuppressedLower was returned for both a strictly-lower incoming
(the attack signal) and an equal-count freshness tie, so the requeue counted a
benign tie as an attempted suppression. Add a distinct RetainedOnTie outcome;
only a strictly-lower drop increments suppressed_lower. Assert the counter
through the enqueue path in the pending-lower test.
Adds oldest_pending_quote_age_ms to the first-audit scheduler summary so the
pending-queue depth can be interpreted: a small, steady value means the queue
is draining promptly; a value climbing toward the answerability window means
pending work is expiring unaudited instead of launching. Gives the queue-age
evidence the last testnet report asked for.

Observability only: no scheduling or protocol behaviour change.
…riodic summary

The per-variant traffic counters give audit volume but not results, so
release comparisons cannot attribute CPU changes to audit work. Add
cumulative pass/fail-per-reason counters for responsible and subtree
audits plus responder-side admission-drop counters, emitted as a fourth
summary line that telegraf lifts into per-field ES/Influx data.
Expired pending nominations were only collected during reserve scans,
which the token-budget gate skips entirely under sustained load, so dead
entries squatted the bounded per-peer LRU and a dead higher-count
incumbent could permanently veto live lower-count nominations for its
peer. Enqueue now drops a dead incumbent before coalescing and the
summary tick sweeps expired entries regardless of tokens; both removals
are accounted as outside_answerability_window, so the pending count and
oldest-pending-age gauge report only launchable work.
The pre-rebase merge of main had resolved the tests-module import list to
use the qualified path for AuditFailureSummary; replaying the linear
commits resurrected the unused import.
Copilot AI review requested due to automatic review settings July 23, 2026 04:42
@grumbach
grumbach force-pushed the fix/first-audit-admission-cap branch from cbc2f5e to d1fc1dd Compare July 23, 2026 04:42

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 6 out of 7 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (3)

src/replication/protocol.rs:1105

  • These assertions use exact deltas on process-global counters, which can fail if another test increments the same counter between the load and the assert. Use a >= check so the test remains correct under parallel execution.
        let before = AUDIT_PASS[pass_slot].load(Ordering::Relaxed);
        record_audit_pass(AuditOutcomeKind::Subtree);
        assert_eq!(AUDIT_PASS[pass_slot].load(Ordering::Relaxed), before + 1);

src/replication/protocol.rs:1126

  • Same flakiness risk as the pass assertion: these are global counters and other tests may increment them. Assert that the intended slot increased by at least 1 instead of exactly 1.
                assert_eq!(
                    AUDIT_FAIL[slot].load(Ordering::Relaxed),
                    before + 1,
                    "kind {kind_index} reason {reason:?} must land in its own slot",
                );

src/replication/protocol.rs:1140

  • Same flakiness risk as above: AUDIT_DROPPED is process-global and may be incremented by other tests. Use a >= assertion to avoid nondeterministic failures in parallel runs.
            assert_eq!(
                AUDIT_DROPPED[kind_index].load(Ordering::Relaxed),
                before + 1
            );

Comment thread src/replication/mod.rs
@@ -1673,6 +2295,7 @@ impl ReplicationEngine {
)) => {
protocol::log_traffic_summary();
protocol::log_served_peers_summary();
protocol::log_audit_outcome_summary();
Comment on lines +1098 to +1099
/// No other test mutates the audit outcome counters, so per-slot deltas are
/// stable even under parallel test execution.
Comment thread src/replication/mod.rs Outdated
Comment on lines +977 to +983
let cutoff_secs = GOSSIP_ANSWERABILITY_TTL
.as_secs()
.saturating_sub(MONETIZED_AUDIT_SKEW_MARGIN.as_secs());
// Compile-time division-by-zero if the launch interval is ever zeroed:
// a zero interval makes the budget (and this cap) meaningless.
let launchable = cutoff_secs / config::FIRST_AUDIT_LAUNCH_INTERVAL.as_secs();
(launchable + config::FIRST_AUDIT_BUDGET_BURST as u64) as usize

@dirvine dirvine left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head d1fc1ddf0a4f710e16debd471cd3ec4c3916e92f with a six-seat panel (four substantive completions; two tool-failed), then independently checked the scheduler ordering and focused tests.

Verdict: changes requested. The queue-sizing diagnosis is sound, but the current keep-newest cap introduces a deterministic suppression route before the scheduler can reserve work.

Blocking — drain-before-reserve can evict a target deterministically

FIRST_AUDIT_PENDING_CAP is 32 (src/replication/mod.rs:956-984), while FIRST_AUDIT_DRAIN_BATCH is 64 (src/replication/config.rs:399-405). On an ingress wake the event loop enqueues/drains up to 64 nominations first (src/replication/mod.rs:1621-1640) and only then attempts reservation (:1736-1758). At capacity, every new distinct peer deterministically evicts the LRU peer (:199-227, :619-632).

Therefore an ordered batch of target + 32 later distinct-peer nominations removes the target before either newest/oldest lane can inspect it, even when a token and in-flight slot are available. Lane alternation cannot help because the target is already gone.

This cuts the stacked ADR's documented capacity-eviction threshold from 4,096 nominations (docs/adr/ADR-0004-commitment-bound-quote-pricing.md:462-470) to 32 — a 128x reduction — while Amendment 4 says every fresh pin retains an “unpredictable chance” of prompt audit (:527-533). The current replacement is deterministic under controlled arrival order. That matters because the ADR already assumes recyclable payment principal and sidecar-only pins without a lottery backstop (:443-453).

Please either give eligible work a reservation opportunity before destructive overflow, or use genuine adversary-resistant/random/weighted retention rather than deterministic keep-newest LRU. Add a production-order regression with pending_cap < ordered_batch <= drain_batch; the new enqueue-only test currently proves that the oldest target is evicted but does not exercise the drain/reserve boundary.

Also fix in this head

  • Cap maths/wording: the strict predicate rejects age >= 150m (src/replication/mod.rs:920-928). For a fresh quote, burst 2 plus refills at 5…145 minutes gives 31 usable launches, not 32; the max-jitter/slack prefilter is stricter again. The test at :6590-6604 repeats the formula rather than simulating the predicate. Derive the cap from the same strict launch predicate/horizon.
  • Occupancy telemetry: a reservation is held outside pending (:477-487, :721-728), so the scheduler may hold one reserved + 32 pending while reporting pending=32, pending_cap=32. Report reserved/total schedulable occupancy or narrow the claim.
  • Stack dependency: #183 must remain after #177. The merge-order comment names stale cap SHA cbc2f5e; the current cap commit is this head, d1fc1dd.

Verification

  • cargo fmt --all -- --check — pass
  • cargo test first_audit_ --lib --no-fail-fast — 28/28 pass
  • GitHub CI — all current checks green across Linux/macOS/Windows, clippy, format, docs, no-logging, audit and ADR validation
  • Head revalidated immediately before review

The causal note on DEV-02/DEV-03 is fair: that comparison cannot attribute the upload p90 change. I also agree another attribution run is not necessary for this queue finding; the scheduler counters already show the admission/budget mismatch. This review is about the proposed retention policy, not the diagnosis.

grumbach added 3 commits July 23, 2026 15:17
…udit cooldown

audit_launch_decision stamped the shared audit_on_gossip_cooldown before
consulting the lottery, so a losing ticket consumed the 30-minute window
despite sending no challenge. The monetized first-audit scheduler reads
the same map as recent-audit coverage, so a peer whose changed commitment
kept losing the lottery near each reopening could hold a paid first audit
pending until its answerability window expired, and sidecar-only pins
have no gossip-lottery backstop.

The lottery attempt window is now a gossip-private map (stamped on every
roll, win or lose, preserving one-roll-per-window flood resistance) and
the shared cooldown is stamped only when a real audit launches. Also
documents the promotion-before-send stamped-but-unsent residual on
resolve().
The pending queue shared the 4096-entry commitment-cache cap while the
token bucket can launch at most ~32 audits inside one effective
answerability window, so under production-scale arrival rates (staging
2026-07-17: ~113 admitted nominations per node-hour against ~12
launches) over 99% of admitted work could never launch. The queue
became an aging backlog whose entries mostly expired, and the
pending/oldest-age telemetry measured the backlog instead of
schedulable work.

pending is now capped at FIRST_AUDIT_PENDING_CAP, derived from the
budget and window constants with compile-time guardrails. At capacity
the LRU displaces the least-recently-refreshed entry, counted as
capacity_evicted; keep-newest displacement preserves the newest-first
lane's prompt-audit deterrence under flood, and displacement stamps no
suppression state. The summary line reports pending_cap so occupancy
is a dashboard quantity. ADR-0004 Amendment 4 documents the sampling
coverage semantics.
…n for the pending cap

Review findings on the admission cap:

- Deterministic keep-newest LRU displacement let an ordered batch of cap
  distinct-peer nominations flush a chosen target inside one 64-event
  ingress drain, before either launch lane could see it, cutting the
  documented suppression cost from 4096 nominations to 32. Overflow now
  displaces a uniformly random incumbent (per-nomination eviction
  probability 1/cap, never certain), and the drainer grants pending work
  a reservation opportunity before any destructive overflow, so a burst
  can never flush eligible work past a ready token.
- The cap derivation over-counted by one: the strict reserve predicate
  (answerable through max jitter plus send slack) permits 31 usable
  launches per window, not 32. The unit test now simulates the shipped
  predicate instant-by-instant instead of repeating the formula.
- The reservation is held outside pending, so the summary now reports
  reserved alongside pending/pending_cap; schedulable occupancy is their
  sum.

Regressions: ordered-flood non-determinism (100-trial statistical bounds
with miss odds below 1e-20), reserve-before-eviction at capacity, and
displaced-peer re-nomination under random retention.
Copilot AI review requested due to automatic review settings July 23, 2026 06:25
@grumbach
grumbach force-pushed the fix/first-audit-admission-cap branch from d1fc1dd to 3462edf Compare July 23, 2026 06:25
@grumbach
grumbach requested a review from dirvine July 23, 2026 06:25

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 6 out of 7 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/payment/verifier.rs:1165

  • The settlement-redirect check runs before verifying the on-chain amount is sufficient. For an unpaid quote hash, the vault mapping will usually return a zeroed rewards-address prefix, which will be treated as a “redirected settlement” and yields a misleading error.

Consider applying the redirect check only after on_chain_amount >= candidate.expected_amount so “not paid enough” remains the failure mode when no sufficient settlement exists.

        if let Some(recorded) = recorded_rewards_prefix {
            let expected = Self::rewards_address_prefix(&candidate.quote.rewards_address);
            if recorded != expected {
                return Err(Error::Payment(format!(
                    "Median-priced quote settlement for peer {:?} was redirected: recorded rewards address prefix {} does not match the quote's {}",

Comment on lines +273 to +290
pub(crate) fn record_audit_fail(kind: AuditOutcomeKind, reason: &AuditFailureReason) {
AUDIT_FAIL[kind as usize * N_FAIL_REASONS + fail_reason_index(reason)]
.fetch_add(1, Ordering::Relaxed);
}

/// Record a responder-side challenge dropped at the admission caps.
pub(crate) fn record_audit_drop(kind: AuditDropKind) {
AUDIT_DROPPED[kind as usize].fetch_add(1, Ordering::Relaxed);
}

/// Emit the cumulative audit outcome tallies as one INFO summary line, same
/// target and cadence as [`log_traffic_summary`] (`group = 4`), so the
/// telegraf→Elasticsearch pipeline lifts each key into a `tail.*` field and
/// pass/fail/drop rates per hour fall out of max-per-hour deltas.
pub(crate) fn log_audit_outcome_summary() {
let pass = |k: usize| AUDIT_PASS[k].load(Ordering::Relaxed);
let fail = |k: usize, r: usize| AUDIT_FAIL[k * N_FAIL_REASONS + r].load(Ordering::Relaxed);
let drop = |k: usize| AUDIT_DROPPED[k].load(Ordering::Relaxed);
Comment thread src/replication/mod.rs
Comment on lines 1321 to +1326
/// ADR-0004: a sender the payment verifier uses to surface monetized pins
/// (commitments that backed a payment) for a deterministic first audit.
/// Cloneable; the engine drains the matching receiver.
/// (commitments that backed a payment) for a first audit. Cloneable; the
/// engine drains the matching receiver. Bounded: senders must `try_send`
/// and treat a full queue as a benign drop (Amendment 2 best-effort).
#[must_use]
pub fn monetized_pin_sender(&self) -> mpsc::UnboundedSender<MonetizedPinEvent> {
pub fn monetized_pin_sender(&self) -> mpsc::Sender<MonetizedPinEvent> {
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.

3 participants