feat(shadow): dummy XMSS proofs + sim-cost sleeps behind shadow-integration#484
feat(shadow): dummy XMSS proofs + sim-cost sleeps behind shadow-integration#484MegaRedHand wants to merge 7 commits into
Conversation
…ration Port zeam's Shadow-simulator fake-XMSS mode so the lean-shadow-fuzzer can run ethlambda without the multi-second leanVM aggregation prover/verifier. Everything is gated behind the shadow-integration feature; a stock build compiles none of it and behaves identically. crypto: new shadow_cost module (process-global atomics config for a fake toggle plus three rate knobs), rate-based sleep helpers (aggregate/verify/merge = n/rate seconds on Shadow's virtual clock), and a dependency-free deterministic dummy-proof fill (FNV-1a seed fold + SplitMix64). Fake/sleep interception in all 7 aggregation/verify functions; the fake path short-circuits before leanVM setup and returns a deterministic stub proof (provers) or Ok(()) (verifiers). cli: feature-gated ShadowOptions (4 flags) flattened into CliOptions; main wires shadow_cost::init once at startup. Matches zeam: split and Type-2 verify have no modeled sleep cost.
🤖 Codex Code Review
I couldn’t run the crate tests in this environment because the toolchain/cache locations under Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Greptile SummaryThis PR adds Shadow-only fake XMSS proofs and simulated crypto cost controls. The main changes are:
Confidence Score: 4/5The fake proof path needs fixes before merging.
crates/common/crypto/src/lib.rs
|
| Filename | Overview |
|---|---|
| bin/ethlambda/Cargo.toml | Adds the crypto dependency and forwards the Shadow feature to ethlambda-crypto. |
| bin/ethlambda/src/cli.rs | Adds feature-gated Shadow options for fake XMSS and simulated operation rates. |
| bin/ethlambda/src/main.rs | Initializes the Shadow XMSS configuration after CLI parsing. |
| crates/common/crypto/Cargo.toml | Adds the shadow-integration feature for the crypto crate. |
| crates/common/crypto/src/lib.rs | Adds fake and simulated-cost branches to XMSS aggregation, merge, verification, and split helpers; several fake seeds omit binding inputs. |
| crates/common/crypto/src/shadow_cost.rs | Adds global Shadow fake/rate state, delay calculation, and deterministic dummy proof generation. |
Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 5
crates/common/crypto/src/lib.rs:176-180
**Signer Set Missing From Seed**
When fake mode aggregates the same message and slot for two different validator sets with the same count, this branch produces identical proof bytes because the seed only includes `message`, `slot`, and `count`. Callers later pass real public keys into verification, so Shadow runs can no longer distinguish which validators the aggregate represents.
### Issue 2 of 5
crates/common/crypto/src/lib.rs:232-239
**Participant Keys Missing From Seed**
`aggregate_mixed` includes the child proof bytes and raw signer count, but it drops both the child and raw public keys. Two aggregations with the same child dummy proofs, message, slot, and raw count but different participants produce the same fake proof, which can make distinct committee contents look identical in Shadow.
### Issue 3 of 5
crates/common/crypto/src/lib.rs:291-297
**Child Keys Missing From Seed**
`aggregate_proofs` seeds the fake proof from the message, slot, and child proof bytes, but not from each child's public-key set. Reaggregating the same fake child proofs under different validator memberships yields identical output, so downstream fake verification cannot tell which participants the aggregate was built for.
### Issue 4 of 5
crates/common/crypto/src/lib.rs:389-398
**Type Two Binding Missing**
The fake Type-2 merge seed contains only child proof bytes and the component count. If two blocks merge the same dummy Type-1 bytes for different message, slot, or proposer/attester key bindings, they receive the same Type-2 proof bytes, so block-proof identity no longer reflects the bindings that the real verifier checks.
### Issue 5 of 5
crates/common/crypto/src/lib.rs:491-497
**Split Context Missing From Seed**
The fake split output depends only on the Type-2 bytes and message. Calls with the same merged proof and message but different component public keys or slot context produce identical Type-1 bytes, so reaggregation can pair a split proof with a different validator context than the one the real split path would derive.
Reviews (1): Last reviewed commit: "feat(shadow): dummy XMSS proofs + sim-co..." | Re-trigger Greptile
| let dummy = crate::shadow_cost::fill_fake_proof( | ||
| crate::shadow_cost::FAKE_PROOF_SIZE, | ||
| &[&message.0, &slot_bytes, &count_bytes], | ||
| ); | ||
| std::thread::sleep(crate::shadow_cost::aggregate_delay(agg_n)); |
There was a problem hiding this comment.
When fake mode aggregates the same message and slot for two different validator sets with the same count, this branch produces identical proof bytes because the seed only includes message, slot, and count. Callers later pass real public keys into verification, so Shadow runs can no longer distinguish which validators the aggregate represents.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/crypto/src/lib.rs
Line: 176-180
Comment:
**Signer Set Missing From Seed**
When fake mode aggregates the same message and slot for two different validator sets with the same count, this branch produces identical proof bytes because the seed only includes `message`, `slot`, and `count`. Callers later pass real public keys into verification, so Shadow runs can no longer distinguish which validators the aggregate represents.
How can I resolve this? If you propose a fix, please make it concise.| let mut parts: Vec<&[u8]> = vec![&message.0, &slot_bytes]; | ||
| for (_, proof) in &children { | ||
| parts.push(proof.iter().as_slice()); | ||
| } | ||
| parts.push(&count_bytes); | ||
| let dummy = | ||
| crate::shadow_cost::fill_fake_proof(crate::shadow_cost::FAKE_PROOF_SIZE, &parts); | ||
| std::thread::sleep(crate::shadow_cost::aggregate_delay(agg_n)); |
There was a problem hiding this comment.
Participant Keys Missing From Seed
aggregate_mixed includes the child proof bytes and raw signer count, but it drops both the child and raw public keys. Two aggregations with the same child dummy proofs, message, slot, and raw count but different participants produce the same fake proof, which can make distinct committee contents look identical in Shadow.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/crypto/src/lib.rs
Line: 232-239
Comment:
**Participant Keys Missing From Seed**
`aggregate_mixed` includes the child proof bytes and raw signer count, but it drops both the child and raw public keys. Two aggregations with the same child dummy proofs, message, slot, and raw count but different participants produce the same fake proof, which can make distinct committee contents look identical in Shadow.
How can I resolve this? If you propose a fix, please make it concise.| let mut parts: Vec<&[u8]> = vec![&message.0, &slot_bytes]; | ||
| for (_, proof) in &children { | ||
| parts.push(proof.iter().as_slice()); | ||
| } | ||
| let dummy = | ||
| crate::shadow_cost::fill_fake_proof(crate::shadow_cost::FAKE_PROOF_SIZE, &parts); | ||
| std::thread::sleep(crate::shadow_cost::aggregate_delay(agg_n)); |
There was a problem hiding this comment.
aggregate_proofs seeds the fake proof from the message, slot, and child proof bytes, but not from each child's public-key set. Reaggregating the same fake child proofs under different validator memberships yields identical output, so downstream fake verification cannot tell which participants the aggregate was built for.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/crypto/src/lib.rs
Line: 291-297
Comment:
**Child Keys Missing From Seed**
`aggregate_proofs` seeds the fake proof from the message, slot, and child proof bytes, but not from each child's public-key set. Reaggregating the same fake child proofs under different validator memberships yields identical output, so downstream fake verification cannot tell which participants the aggregate was built for.
How can I resolve this? If you propose a fix, please make it concise.| if crate::shadow_cost::fake_xmss() { | ||
| let count_bytes = type_1s.len().to_le_bytes(); | ||
| let mut parts: Vec<&[u8]> = Vec::with_capacity(type_1s.len() + 1); | ||
| for (_, proof) in &type_1s { | ||
| parts.push(proof.iter().as_slice()); | ||
| } | ||
| parts.push(&count_bytes); | ||
| let dummy = | ||
| crate::shadow_cost::fill_fake_proof(crate::shadow_cost::FAKE_PROOF_SIZE, &parts); | ||
| std::thread::sleep(crate::shadow_cost::merge_delay(merge_n)); |
There was a problem hiding this comment.
The fake Type-2 merge seed contains only child proof bytes and the component count. If two blocks merge the same dummy Type-1 bytes for different message, slot, or proposer/attester key bindings, they receive the same Type-2 proof bytes, so block-proof identity no longer reflects the bindings that the real verifier checks.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/crypto/src/lib.rs
Line: 389-398
Comment:
**Type Two Binding Missing**
The fake Type-2 merge seed contains only child proof bytes and the component count. If two blocks merge the same dummy Type-1 bytes for different message, slot, or proposer/attester key bindings, they receive the same Type-2 proof bytes, so block-proof identity no longer reflects the bindings that the real verifier checks.
How can I resolve this? If you propose a fix, please make it concise.| #[cfg(feature = "shadow-integration")] | ||
| if crate::shadow_cost::fake_xmss() { | ||
| return Ok(crate::shadow_cost::fill_fake_proof( | ||
| crate::shadow_cost::FAKE_PROOF_SIZE, | ||
| &[proof_data, &message.0], | ||
| )); | ||
| } |
There was a problem hiding this comment.
Split Context Missing From Seed
The fake split output depends only on the Type-2 bytes and message. Calls with the same merged proof and message but different component public keys or slot context produce identical Type-1 bytes, so reaggregation can pair a split proof with a different validator context than the one the real split path would derive.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/crypto/src/lib.rs
Line: 491-497
Comment:
**Split Context Missing From Seed**
The fake split output depends only on the Type-2 bytes and message. Calls with the same merged proof and message but different component public keys or slot context produce identical Type-1 bytes, so reaggregation can pair a split proof with a different validator context than the one the real split path would derive.
How can I resolve this? If you propose a fix, please make it concise.
🤖 Claude Code ReviewReview:
|
…erify Add a shadow_cost::sleep helper that skips the sleep when the delay is zero (rate unset/disabled), matching zeam's 'if (delay_ns != 0)' guard so a disabled rate costs nothing. Route every prover sleep through it, and restructure verify_aggregated_signature to zeam's single fall-through sleep (run the real verifier unless fake, then one sleep) instead of the early-return two-sleep shape. Behavior is unchanged.
Make the fake stub proof size runtime-configurable instead of a hardcoded constant. Adds a --shadow-xmss-fake-proof-size CLI flag (default 32 KiB, bounded to the 512 KiB ByteList512KiB cap so it can't panic), stored in the shadow_cost config atomics and read by every fake-proof call site via fake_proof_size(). DEFAULT_FAKE_PROOF_SIZE remains the shared default. An ethlambda-only capability; zeam hardcodes the 32 KiB constant.
# Conflicts: # bin/ethlambda/src/cli.rs
Summary
Ports zeam's Shadow-simulator fake-XMSS mode + rate-based sim-cost sleeps into ethlambda, so the lean-shadow-fuzzer can drive the client under Shadow without paying the multi-second leanVM aggregation prover/verifier (which is also the thing that stack-overflows the debug binary). Everything is behind the existing
shadow-integrationCargo feature; a stock build compiles none of it and behaves identically, andCargo.lockgains only one new dep edge (ethlambda→ethlambda-crypto).Two independent mechanisms, exactly as in zeam:
Ok(()). Uniform across all entry points (a fake aggregate feeding a real verify would mix incompatible byte formats).sleep = n / rateseconds) sleeps that model CPU cost on Shadow's virtual clock, applied whether or not fake is on.New CLI flags (only under
--features shadow-integration)--shadow-xmss-fake--shadow-xmss-aggregate-signatures-rate <f64>n/rates into aggregation. Unset or ≤0 disables.--shadow-xmss-verify-aggregated-signatures-rate <f64>n/rates into Type-1 verify.--shadow-xmss-merge-rate <f64>n/rates into the block-proof merge.The four flags live in one feature-gated
ShadowOptionsstruct flattened intoCliOptions;maincallsshadow_cost::init(...)once after arg parse.What changed
ethlambda-cryptoshadow-integrationfeature +shadow_costmodule: atomics config (init,fake_xmss), rate-based delay helpers,FAKE_PROOF_SIZE, and a dependency-free deterministic dummy-proof fill (FNV-1a seed fold → SplitMix64). The dummy proof is seeded only from what the real FFI binds (message, slot, child-proof bytes, participant counts) so every node produces identical bytes for identical inputs — reproducible Shadow runs, no consensus divergence.ensure_prover_ready(), so fake mode never even pays leanVM setup.AggregationBits(who voted) stay real; only the SNARK bytes are stubbed.ethlambda(bin) — theShadowOptionsflags, the transitive feature enable, and theinitwiring.Intentional deviations from zeam
--shadow-xmss-fakeis a real CLI flag (zeam uses env-only because zigcli couldn't take another field; clap has no such limit).aggregate_proofssleeps onn = #children(it has no raw sigs; zeam models aggregate cost on raw count only, which would leave this children-only path cost-free).Design note (zeam parity)
Only
verify_aggregated_signature(Type-1) has a verify sleep.verify_type_2_signatureandsplit_type_2_by_messagehave no modeled cost, matching zeam. So fake-mode block import (which verifies a merged Type-2) incurs zero modeled verify cost — if Shadow ever undercounts import-side verify time, that's the missing knob.Testing / verification
cargo test -p ethlambda-crypto --features shadow-integration— newshadow_costunit tests (delays off/zero/proportional; deterministic fill) + fake round-trip tests for the interception (dummy size, determinism, message-sensitivity, fake verify accepts). All pass. (No real XMSS keygen in the fake tests.)cargo build -p ethlambda[-crypto]; stock--helplists noshadowflags.cargo check -p ethlambda --no-default-features --features shadow-integration(the--no-default-featuresdrops jemalloc per the existingcompile_error!guard).clippy -D warningsandfmt --checkclean in both stock and shadow configs.How to build