fold step cost: the "~30 field ops class" estimate against a real measurement
date: 2026-09-23 · revision: 177fad4 (origin/master) · rustc 1.98.0 (macOS arm64)
launch registry property #3 / #5. docs/explanation/latency-targets.md's
clock-C table (light client join and tip follow) estimates:
| steady-state per tip | O(1) fold (~30 field ops class) + header | no re-decide from genesis |
launch #3 (2) (foculus#35, reviewed) reconciled the same table's
decide(folding_acc) verify line against a real measurement
(audit/ticket-proof-cost.md, foculus#6) and left the fold-cost line as
"an architectural estimate, not a measurement — a follow-up once a
per-block fold benchmark exists." this file is that follow-up.
what "steady-state per tip" actually calls
src/tickets.rs's absorb_ticket (fold a new ticket into an existing
accumulator) and fold_acc (merge two accumulators, the tournament-tree
step) are the two candidates. both:
- insert into / iterate
ClusterAcc.seen: BTreeSet<(miner, nonce)>, whose size isk, not a constant; - call
acc_commitment, which rehashes the entire accumulator — everysum_mentry and everyseenpair — on every single call, not just the newly absorbed ticket.
k is not unbounded: ClusterAcc::k_min (Hoeffding target for ε=0.1,
δ=0.01) is ~265, and settlement rewards turn off once k ≥ k_min
(specs/fold-mining.md "payment"). so this is a bounded cost, not the
unbounded-network-size case Assumption-G-style arguments worry about
elsewhere in this repo — but "bounded by a few hundred BTreeSet ops plus
one Poseidon2-class hash over the whole accumulator" and "~30 field ops"
are different claims, and the doc table does not distinguish them.
measurement
tests/fold_step_cost.rs, 8 contributors (matching
audit/hardware-profile.md's ε-support range), k_min = 265
(ClusterAcc::k_min(0.1, 0.01)), 2000 iterations per timing, real tickets
from grind_settlement with target = u64::MAX (every nonce wins, so the
timing measures the fold step itself, not the win-test):
$ cargo test --release --test fold_step_cost -- --nocapture
[fold-step-cost] fold_acc: two k=132 accumulators (k_min=265) -> 664967 ns/call
[fold-step-cost] absorb_ticket: cold(k=0->1)=10702 ns/call, steady(k=264->265)=656250 ns/call, ratio=61.32x
test result: ok. 2 passed; 0 failed
absorb_ticket at realistic steady-state size (k=264→265, the case a
light client repeats every tip once it has caught up) costs ~656 µs — 61×
its cost at k=0→1, confirming the per-call cost scales with k, not a
constant. fold_acc merging two k=132 accumulators into a k_min-sized one
costs ~665 µs, the same order as absorb_ticket at the same total k (both
pay for one acc_commitment rehash of a k_min-sized accumulator).
reconciliation
~30 field ops on this crate's Goldilocks arithmetic is sub-microsecond —
five to six orders of magnitude below the ~0.66 ms measured. the estimate
undercounts because it prices the algebraic sum (sum_m[i] += m[i], which
genuinely is O(1) per call) but not the BTreeSet bookkeeping or the
whole-accumulator rehash acc_commitment does on every call. even at the
measured ~0.66 ms, the fold step stays two orders of magnitude below
decide/verify (9–19 ms, audit/ticket-proof-cost.md) and four orders
below clock-A/B network stages (gossip ~0.4–3 s, φ* contraction ~1–4 s), so
docs/explanation/latency-targets.md's conclusion — "verify is effectively
free compared to network" — still holds. only the specific "~30 field ops
class" line for the fold step itself does not.
what remains
- update
docs/explanation/latency-targets.md's clock-C table with this measurement, in the same style foculus#35 used for the decide/verify line (done in this PR). acc_commitmentrehashing the whole accumulator on everyabsorb_ticketcall rather than an incremental update is itself an implementation choice, not a protocol requirement — an incremental commitment (e.g. folding the new ticket's contribution into the hash state rather than rehashingsum_mandseenfrom scratch) would cut steady-stateabsorb_ticketcost close to the cold-start ~12 µs baseline. that is a real code change to a hot, security-relevant function (acc_commitment/fold_acc/absorb_ticketall touch whatdecideultimately proves over) and is out of scope for this measurement-only slice.- foculus#50 tried such an incremental commitment as an additive multiset
digest (four wrapping
u64limbs summed overseenelements) and was closed unmerged on 2026-10-17: a sum-of-hashes digest over Z_{2^64}^4 admits generalized-birthday (Wagner k-tree) collisions over attacker-chosen(miner, nonce)pairs, so it no longer bindsseenthe way the full rehash does. a sound incremental form needs a collision-resistant set commitment (a multiplicative hash in a large prime-order group, or a Merkle-style set root), not a cheaper additive one.