Skip to content

fix(shielded-pq): quantify + floor-pin pool-v2 tree exhaustion (PQV2-04) - #22

Merged
cloudzombie merged 1 commit into
mainfrom
fix/pqv2-04-tree-exhaustion
Jul 28, 2026
Merged

fix(shielded-pq): quantify + floor-pin pool-v2 tree exhaustion (PQV2-04)#22
cloudzombie merged 1 commit into
mainfrom
fix/pqv2-04-tree-exhaustion

Conversation

@cloudzombie

Copy link
Copy Markdown
Owner

Closes audit Medium PQV2-04 — "the depth-20 commitment tree can be economically exhausted" — for the DORMANT pool-v2 (shielded-pq, BIP-9 bit 2, unarmed on every canonical chain).

Two halves

1. root() / capacity boundary bug — already fixed on main, re-verified here.
At exactly size == 2^20 the O(depth) frontier's low bits are all zero, so root() would return the empty-tree root for a full tree (a full-pool anchor colliding with the empty-pool anchor). Fixed by capping usable capacity at MAX_V2_NOTES = MAX_TREE_LEAVES = 2^20 − 1 (final leaf slot deliberately unusable) with a compile-time MAX_V2_NOTES < 2^TREE_DEPTH guard; both mutators reject the insertion reaching exact capacity with a typed TreeFull, mutating nothing. Proven — not by an early refusal — by the release-only frontier_matches_the_reference_tree_at_full_capacity, which appends 2^20 − 1 real leaves and pins the frontier root against the reference tree at full capacity (NOT the empty root). Re-verified this session: 1 passed in 68 s.

2. Economic/growth exhaustion — quantified, floor-pinned; real fix gated.
Traced from code (post-activation, mainnet schedule): fee = gas_for(ShieldedV2) × 10 grains/gas, paid transparently to the miner (in-circuit fee leg pinned to zero). Each bundle appends ≤ NUM_SLOTS = 4 leaves. Conservative floor to fill 2^20 − 1 leaves:

  • ≥ ~13,657 XUS in fees (fixed gas terms only; ~39k XUS counting the ~60 KB bundle bytes), AND
  • ~11.4 days / 6,554 consecutive saturated blocks (block weight caps commitments/block), the attacker winning the auction against all other traffic the whole time.

The gain is pure griefing (fees enrich miners; holders can still de-shield out). But depth-20 is a prototype capacity — exhaustible for a bounded one-time cost AND by honest growth (1.05M notes). Pricing cannot fix honest exhaustion; the robust fix is a deeper tree. Horizon-derived (MAX_V2_COMMITMENTS_PER_BLOCK × ~20 yr of blocks ≈ 6.7×10^8 ⇒ depth ~30) → Orchard-parity HORIZON_SAFE_TREE_DEPTH = 32, >6× headroom.

Why the depth change is NOT in this PR (reserve-grade honesty): raising TREE_DEPTH is a STARK spend-circuit re-derivation — the AIR trace row-map bakes the Merkle path length into fixed literals (INPUT_SEGMENT_ROWS, root_row, nf_row) and TRACE_LENGTH = 1024; depth 32 doubles the trace to 2048 and invalidates every proof KAT and the measured verify-cost basis. It is a re-audited, re-proven change to the trust path, deferred and recorded as a hard prerequisite for arming bit 2, not shipped half-done.

What this PR adds

  • pool_v2_exhaustion_has_a_pinned_fee_floor (runtime) — ≥13k-XUS fee floor.
  • filling_the_tree_takes_thousands_of_saturated_blocks — ≥10 days / 6,554 blocks.
  • depth_20_is_a_documented_prototype_shortfall_pending_a_circuit_upgrade + HORIZON_SAFE_TREE_DEPTH const with derivation.
  • PQV2-04 resolution note + updated arming bar in notes/pq-pool-v2-audit-response.md.

Dormancy / byte-identity

Additive only: no consensus digest, gas value, or TREE_DEPTH changes — the sole non-test addition is a documentation constant. Bit 2 unarmed on every canonical chain (shielded_v2_is_dormant_everywhere green), so no v2 action executes. Safe to change only while dormant; the depth upgrade becomes a hard fork once armed.

Verification (real output)

  • cargo test -p sov-shielded-pq -p sov-runtime — green (runtime 97, shielded-pq 70 +1 ignored, decode_hardening 18, kat 23, security_level 2, verify_cost 2).
  • Release-only full-capacity boundary test — 1 passed / 68 s.
  • cargo fmt --check + cargo clippy --all-targets -D warnings — clean on both crates.
  • cargo build --workspace — green; standalone Station (node/) build — green.

PQV2-04 has two halves. The boundary defect (root() returning the empty-tree
root at exactly size==2^20) is already fixed on main by capping usable capacity
at MAX_V2_NOTES = 2^20-1; re-verified this session by the release-only
frontier_matches_the_reference_tree_at_full_capacity (real appends to the 2^20
boundary, root pinned against the reference tree — 1 passed in 68s).

This commit closes the economic half:

- Trace the real per-commitment cost: fee = gas_for(ShieldedV2) x gas_price
  (10 grains/gas mainnet), paid transparently to the miner. Conservative floor
  to fill 2^20-1 leaves is >=~13.6k XUS (realistically ~39k counting bundle
  bytes) AND ~11 days / 6,554 consecutive saturated blocks (block-weight caps
  commitments/block). Pinned by two regression tests so a pre-arming gas retune
  cannot silently cheapen the attack.

- Record depth-20 as a documented PROTOTYPE shortfall: capacity is below the
  issuance/confirmation horizon (exhaustible by honest growth too), and the
  robust fix is a deeper tree. Adds HORIZON_SAFE_TREE_DEPTH = 32 (Orchard
  parity, horizon-derived) with a test pinning TREE_DEPTH < it.

The depth increase is deliberately NOT shipped: it is a STARK spend-circuit
re-derivation (the AIR trace row-map bakes the Merkle path length into fixed
literals and TRACE_LENGTH=1024; all proof KATs and the measured verify-cost
basis depend on it). Per reserve-grade rules it is deferred to an audited,
re-proven circuit revision and recorded as a hard prerequisite for arming
bit 2. Additive + dormant: no consensus digest, gas value, or TREE_DEPTH
changes; bit 2 unarmed everywhere. Workspace + Station build green.
@cloudzombie
cloudzombie merged commit bfd6e7a into main Jul 28, 2026
36 checks passed
@cloudzombie
cloudzombie deleted the fix/pqv2-04-tree-exhaustion branch July 28, 2026 23:50
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