Skip to content

v0.8.3: prover/verify performance + faucet flag (#151 #152 #153) - #156

Merged
dendisuhubdy merged 4 commits into
mainfrom
fix/v083-perf
Oct 28, 2026
Merged

dendisuhubdy merged 4 commits into
mainfrom
fix/v083-perf

Conversation

@dendisuhubdy

Copy link
Copy Markdown
Member

Three v0.8.3 fixes from the 2026-10-28 chain-21 network experiment, one commit each.

#151: prover RNG runs in parallel, ProverData is cached

Random columns and Merkle salts in parallel. The hiding PCS's random columns (commit, quotient chunks, ZK randomisation polynomial) and the hiding MMCS's Merkle salts are now ChaCha20 counter-mode streams (vendor/p3-merkle-tree/src/counter_rng.rs):

  • Each call takes one fresh 256-bit secret key from the configuration's existing generator. The spin lock is held only for that 32-byte draw, never while values are generated.
  • Within the call, (lane = matrix, chunk = 1024 rows) selects the ChaCha nonce, so all chunks fill in parallel.
  • Values are sampled exactly as before: StandardUniform rejection sampling over Goldilocks.
  • Chunking never depends on the thread count, so a seeded run is reproducible.
  • Security rests on ChaCha20 as a PRF under a secret key. That is the same assumption the old generators made (StdRng is ChaCha12; KeyRngV2::from_seed is a keyed Poseidon2 sponge).

Which generators get the parallel path. OS entropy (SaltRng::Fresh) and seed-keyed sponges (proving-config clones) do. Verifier-key label streams (KeyRngV2::from_label) do not: they keep the serial draw bit for bit, so no verifier key moves. tests/verifier_key.rs and tests/key_derivation_v2.rs are unchanged and pass.

Wire format and verifier unchanged. No verifier function is touched. tests/counter_rng.rs::a_new_proof_passes_the_unchanged_v080_verifier_path checks that a new proof passes the admission path v0.8.0 nodes run (non_canonical_proof, auth_header, verify_auth) and that it round-trips canonically.

ProverData cache. ProverData is a pure function of (profile, tier, table heights):

  • key_config salts the preprocessed commit from the public label streams.
  • The hiding PCS pads preprocessed columns with zeros, not random values.
  • The preprocessed columns depend on no program.

So prove_traces takes it from a process-wide LRU (machine::cached_prover_data), shared by every Machine the wallet and rand-prover create:

  • 4 shapes, tiers ≤ 16, single-flight.
  • Tier-18 msauth data is not kept resident.

tests/prover_data_cache.rs checks that two proofs cost one build, and that both verify on a machine that rebuilds the verifier key. That second check proves the cached commitment is the verifier key's.

Tier-14 bundle proof timing. Production profile, Apple M4 Max, 16 threads, hidden_bundle.rs mixed() case, two passes each:

phase before after (first proof) after (cache warm)
total 19.5 s 11.3–11.7 s 10.1 s
main trace commit 5 875 ms 2 347 2 345
permutation commit 2 412 999 995
quotient eval + chunk LDE (incl. chunk randomisation) 2 757 1 404 1 511
quotient commit 3 521 3 154 3 189
ZK randomisation commit 1 119 461 461
opening + FRI (commit-phase salts) 1 359 387 553
preprocessed commit / ProverData 1 435 1 420 0
LogUp permutation trace gen 1 026 1 027 1 005

Auth proof, tier 10: 1 446 ms → 922 ms.

What is left is mostly Poseidon2 Merkle hashing (parallel) and quotient evaluation.

#152: one decode per admission, faster hex, docs refreshed

  • One decode per proof per admission. executor::decode_canonical_shared memoises the canonical decode on the exact bytes: 16 entries, a hit compares the bytes in full, refusals are not kept. These paths all go through it:

    • non_canonical_proof
    • auth_header_of
    • the pinned digest
    • bundle_gas_limit
    • both verifies
    • the call-policy reads

    The ledger and the ConfidentialExecutor trait are unchanged. The ledger still hands each check the bytes in the same order; the decodes meet inside the executor, so no verdict can change. tests/decode_memo.rs shows five reads cost one decode with identical verdicts.

  • Faster hex. rand_sendTransaction decodes its body with faster-hex 0.10.1 (MIT; cargo deny check licenses sources bans: ok). rpc::tests::the_hex_body_decodes_as_hex_does checks the result matches hex::decode for both cases, mixed case, 0x, odd lengths and bad characters at lane boundaries.

  • Docs. docs/block-space.md, docs/node-hardware.md §2.1 and docs/compute-optimization.md §1.1 had the tier-12 research proof's 16 ms / 838 ms. They now give the issue's chain-21 numbers (73 ms warm, 1.6 s cold, n = 38, M4 Max), the v0.8.3 numbers, and the per-block line.

Admission verify timing. 38 captured chain-21 transactions × 30 warm reps, the node's executor without its verdict cache. The "after" bench clears the decode memo before each rep, so every rep is a fresh admission. Two passes each:

before after
warm verify, p50 70.8 / 72.2 ms 61.4 / 62.2 ms
warm verify, p99 73.6 / 76.5 ms 64.4 / 66.5 ms
cold verify (key build), p50 1.55 / 1.59 s 1.53 / 1.56 s
body decode (hex + bincode), p50 18.0 / 18.1 ms 1.2 / 1.2 ms
header checks (non_canonical + auth_header + digest + gas) 9.7 ms 4.4 ms
one admission (decode + warm verify) ~90 ms ~63 ms

#153: faucet means this node can mint

admission::faucet_refusal is now the single test for whether this node can mint. All three must hold:

  • the faucet is on;
  • the node is a validator;
  • its key is in faucet_minters.

rand_status.faucet and rand_getEmission.faucet report exactly that. rand_mint checks it before building anything, and a non-minter's error names the minter set. docs/rpc.md documents the change and has a v0.8.3 changelog entry. Tests:

  • admission::tests::the_faucet_is_served_only_by_a_validator_whose_key_is_a_faucet_minter
  • node::tests::a_validator_outside_the_faucet_minters_refuses_rand_mint_up_front

Tests

  • zkvm, integration suites (Test-profile proof round trips at tier 10 and up): 91 passed, 0 failed, 7 ignored, across 13 suites:
    • counter_rng 7
    • prover_data_cache 1
    • decode_memo 1
    • verifier_key 2
    • key_derivation_v2 6
    • key_cache 2
    • zk 3
    • privacy_floor 4
    • e2e 25 (+6 ignored)
    • executor 24 (+1 ignored)
    • auth_cheating 3
    • logup_blind 2
    • proof_binding 11
  • zkvm lib: 44 passed.
  • zkvm, mock-cuda: cargo check --features mock-cuda --tests compiles.
  • node lib: 772 passed, 1 failed, 5 ignored. The failure is msauth_key::tests::an_exe_in_the_tree_target_falls_back_only_when_opted_in, which asserts that the test binary sits under the source tree's target/. It was built with a shared CARGO_TARGET_DIR outside the tree, and the test's file is untouched here. The faucet, status, emission and hex tests are among the 772.
  • clippy -D warnings:
    • randprotocol-node --all-targets: clean.
    • p3-fri, p3-merkle-tree (lib): clean.
    • randprotocol-zkvm: not in the CI lint set and carries pre-existing findings; none are on lines this PR adds.
  • cargo deny check licenses sources bans: ok. cargo check --locked: ok. The lockfile diff is only faster-hex, heapless 0.8 and p3-fri → p3-merkle-tree.

Measurement conditions

  • Each timed run waited until no rand --rpc http://127.0.0.1:18690 process (the chain-21 experiment) existed.
  • A run aborted and retried if the experiment started during it. 10 runs were retried.
  • 1-minute load average at the start of each accepted run: 4.5–11.3. Other worktrees' cargo builds were running on the Mac.
  • The bench instrumentation (tracing spans, examples/verify_latency.rs, the prover_phase_breakdown test) is from bench/verify-latency and is not in this PR.

Follow-ups

Closes #151
Closes #152
Closes #153

🤖 Generated with Claude Code

https://claude.ai/code/session_01GFs5h141Ct6RTGGrCTQ1vy

dendisuhubdy and others added 4 commits October 28, 2026 01:29
…treams filled in parallel; ProverData is cached per shape

The hiding PCS's random columns (commit, quotient chunks, the ZK randomisation polynomial) and the
hiding MMCS's Merkle salts were drawn serially from one generator behind a spin lock: ~7.5 s of a
18.2 s tier-14 bundle proof on 16 threads. They are now ChaCha20 counter-mode streams
(vendor/p3-merkle-tree/src/counter_rng.rs): per call the configuration's generator hands out one
fresh 256-bit secret key (the lock is held for that draw only), and lane (matrix) / chunk
(CHUNK_ROWS = 1024 rows) select the ChaCha nonce, so every chunk fills in parallel. Values are
sampled exactly as before (StandardUniform's rejection sampling over Goldilocks), uniform and
secret under the PRF assumption the previous generators (StdRng = ChaCha12, the keyed Poseidon2
sponge) already rested on. The chunking never depends on the thread count, so a seeded run is
reproducible.

Who hands out keys (CounterModeRng): OS entropy (SaltRng::Fresh) and a seed-keyed sponge
(KeyRngV2::from_seed, a proving config's clone). A label stream (KeyRngV2::from_label, a verifier
key's) hands out none and is drawn from serially, bit for bit as before, so no verifier key moves
(tests/verifier_key.rs and tests/key_derivation_v2.rs unchanged and passing). The proof format and
every verifier function are untouched: old verifiers accept new proofs
(tests/counter_rng.rs::a_new_proof_passes_the_unchanged_v080_verifier_path).

ProverData (the preprocessed commitment and its Merkle tree, 1.4 s at tier 14) is a pure function
of (profile, tier, table heights): key_config salts it from the public label streams and the
preprocessed columns depend on no program. prove_traces takes it from a process-wide LRU
(machine::cached_prover_data, 4 shapes, tiers <= 16, single-flight), shared by every Machine the
wallet and rand-prover build. tests/prover_data_cache.rs: one build for two proofs, both verify on
a machine that recomputes the verifier key.

deploy/sync-zkvm.sh keeps the two new test files and refuses a re-vendor from a circuits commit
without these changes (to be ported to zkp-circuits).

Fixes #151

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFs5h141Ct6RTGGrCTQ1vy
… rand_mint refuses a non-minter key up front

`faucet` echoed the genesis flag, so a validator bonded after genesis (chain 21's guardian hosts
g1-g6) advertised a faucet, accepted rand_mint on its operator RPC, built and pooled the mint, and
only then failed with MinterNotAllowed. admission::faucet_refusal is now the one predicate: the
faucet on, a validator, and a key in faucet_minters (staking.faucet_minters where the genesis lists
them, else the genesis validators). NodeStatus.faucet is `faucet_refusal(..).is_none()`, and
Node::mint returns its reason before building anything; the non-minter's names the minter set.

Tests: admission::the_faucet_is_served_only_by_a_validator_whose_key_is_a_faucet_minter (both
minter sources, every refusal), node::a_validator_outside_the_faucet_minters_refuses_rand_mint_up_front
(nothing pooled). docs/rpc.md: rand_mint, the field, and a v0.8.3 changelog entry.

Fixes #153

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFs5h141Ct6RTGGrCTQ1vy
…x body is decoded with faster-hex; verify numbers refreshed

One admission asked the executor for the same 1.4-1.5 MB proofs four times (the canonical-proof
rule, check_auth_fields' header, the digest and gas reads, the verify), and every ask decoded and
re-encoded them. executor::decode_canonical_shared memoises decode_canonical on the exact bytes
(16 entries, a hit compares them in full, refusals are not kept); non_canonical_proof,
auth_header_of, the pinned digest, bundle_gas_limit, both verifies and the call-policy reads go
through it, and check_decoded / check_msauth_decoded check by reference. The ledger and the
ConfidentialExecutor trait are unchanged: the ledger keeps handing every check the bytes, in the
same order, and the decodes meet where they are answered, so no verdict can move.
tests/decode_memo.rs: five reads of an auth proof, one decode, the same verdicts; a padded or
one-byte-flipped proof is never a hit.

rand_sendTransaction decodes its body with faster-hex 0.10.1 (MIT; cargo deny: bans, licenses,
sources ok) through rpc::decode_hex_body, with hex::decode's verdicts
(rpc::tests::the_hex_body_decodes_as_hex_does).

Measured (Apple M4 Max, 16 threads, 38 captured chain-21 transactions x 30 warm reps, two idle-gated
passes): warm verify p50 72.2 -> 62.2 ms, body decode 18.1 -> 1.2 ms, cold 1.59 -> 1.56 s; one
admission ~90 -> ~63 ms. docs/block-space.md, docs/node-hardware.md 2.1 and
docs/compute-optimization.md 1.1 carried the tier-12 research proof's 16 ms / 838 ms; they now carry
the chain-21 numbers before (73 ms / 1.6 s, the issue's) and after, and the per-block line.

Fixes #152

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFs5h141Ct6RTGGrCTQ1vy
…rrowing it (clippy needless_borrow)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFs5h141Ct6RTGGrCTQ1vy
@codecov

codecov Bot commented Oct 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dendisuhubdy
dendisuhubdy merged commit 641cae9 into main Oct 28, 2026
23 checks passed
@dendisuhubdy
dendisuhubdy deleted the fix/v083-perf branch October 28, 2026 01:25
dendisuhubdy added a commit that referenced this pull request Oct 28, 2026
… the verify queue; ack only after hand-off (#157, PR #158)

CHANGELOG: the #157 entry joins the v0.8.3 "Node" list. rpc.rs merged
without a textual conflict: #156's faster-hex decode of the
rand_sendTransaction body and #158's reply handling below it are both
kept. The stem path from #155 is brought under the admission budget in
the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFs5h141Ct6RTGGrCTQ1vy
dendisuhubdy added a commit that referenced this pull request Oct 28, 2026
- CHANGELOG.md, Unreleased v0.8.3, "Node": #156 (issues #151 #152 #153)
  carried no entry; its three items are added from the PR's own
  measurements.
- docs/rpc.md changelog, "newest first": the #147 wallet entry moves up
  beside the other v0.8.3 entries (#154 had put it under v0.8.2), and a
  v0.8.3 entry records the client-visible parts of #145 and #157 — the
  stem's `unknown` status, the meaning of the returned hash, `-32005`,
  and `rand_status.admission_bytes` / `admission_budget_bytes`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFs5h141Ct6RTGGrCTQ1vy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment