Skip to content

feat(shadow): dummy XMSS proofs + sim-cost sleeps behind shadow-integration#484

Open
MegaRedHand wants to merge 7 commits into
mainfrom
worktree-enchanted-wobbling-wozniak
Open

feat(shadow): dummy XMSS proofs + sim-cost sleeps behind shadow-integration#484
MegaRedHand wants to merge 7 commits into
mainfrom
worktree-enchanted-wobbling-wozniak

Conversation

@MegaRedHand

Copy link
Copy Markdown
Collaborator

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-integration Cargo feature; a stock build compiles none of it and behaves identically, and Cargo.lock gains only one new dep edge (ethlambdaethlambda-crypto).

Two independent mechanisms, exactly as in zeam:

  • Fake XMSS — a single process-global toggle that replaces the aggregation prover/verifier with a deterministic stub: provers return a fixed-size dummy proof, verifiers return Ok(()). Uniform across all entry points (a fake aggregate feeding a real verify would mix incompatible byte formats).
  • Sim-cost sleeps — rate-based (sleep = n / rate seconds) 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)

Flag Effect
--shadow-xmss-fake Replace the aggregation prover/verifier with a deterministic stub (off by default).
--shadow-xmss-aggregate-signatures-rate <f64> Sigs aggregated/sec; injects n/rates into aggregation. Unset or ≤0 disables.
--shadow-xmss-verify-aggregated-signatures-rate <f64> Sigs verified/aggregate/sec; injects n/rates into Type-1 verify.
--shadow-xmss-merge-rate <f64> Type-1 components merged into a Type-2/sec; injects n/rates into the block-proof merge.

The four flags live in one feature-gated ShadowOptions struct flattened into CliOptions; main calls shadow_cost::init(...) once after arg parse.

What changed

  • ethlambda-crypto
    • New shadow-integration feature + shadow_cost module: 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.
    • Feature-gated fake/sleep interception in all 7 aggregation/verify functions. The fake branch sits after cheap arg validation but before ensure_prover_ready(), so fake mode never even pays leanVM setup. AggregationBits (who voted) stay real; only the SNARK bytes are stubbed.
  • ethlambda (bin) — the ShadowOptions flags, the transitive feature enable, and the init wiring.

Intentional deviations from zeam

  1. Feature-gated rather than always-present (matches ethlambda's opt-in Shadow model).
  2. --shadow-xmss-fake is a real CLI flag (zeam uses env-only because zigcli couldn't take another field; clap has no such limit).
  3. CLI-only, no env fallback.
  4. aggregate_proofs sleeps on n = #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_signature and split_type_2_by_message have 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 — new shadow_cost unit 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.)
  • Stock build unchanged: cargo build -p ethlambda[-crypto]; stock --help lists no shadow flags.
  • Full binary under the feature: cargo check -p ethlambda --no-default-features --features shadow-integration (the --no-default-features drops jemalloc per the existing compile_error! guard).
  • clippy -D warnings and fmt --check clean in both stock and shadow configs.

How to build

# via the existing shadow build wrapper
make shadow-build
# then, e.g.
ethlambda ... --shadow-xmss-fake \
  --shadow-xmss-aggregate-signatures-rate 22.7 \
  --shadow-xmss-merge-rate 22.7

…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.
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

  1. High: fake-XMSS verification now accepts arbitrary proof bytes without even doing the cheap structural/binding checks. In crates/common/crypto/src/lib.rs and crates/common/crypto/src/lib.rs, the fake path returns Ok(()) before deserialization, (message, slot) binding checks, or Type-2 component-count checks. That means shadow runs will accept malformed or mismatched attestation/block proofs through the normal import paths in crates/blockchain/src/store.rs and crates/blockchain/src/store.rs. For a simulation feature, skipping the expensive crypto is fine, but dropping these invariants makes shadow materially less faithful and can hide consensus/SSZ regressions.

  2. Medium: split_type_2_by_message also bypasses all message-presence/uniqueness validation in fake mode. The early return in crates/common/crypto/src/lib.rs means shadow reaggregation never reports UnknownMessage or MultipleMessages; it fabricates a Type-1 proof for any (proof_data, message) pair. That changes behavior in the reaggregation path at crates/blockchain/src/reaggregate.rs and can mask bugs around duplicated attestation-data roots or malformed merged proofs.

  3. Medium: the simulated aggregation cost for aggregate_mixed is undercounted when the work comes from child proofs rather than raw signatures. In crates/common/crypto/src/lib.rs, agg_n is set to raw_public_keys.len(), so a child-only aggregate_mixed call sleeps for zero even though it still performs recursive aggregation work. That path is reachable from crates/blockchain/src/aggregation.rs. If the goal is Shadow cost modeling, this will make aggregation-heavy scenarios look artificially cheap.

I couldn’t run the crate tests in this environment because the toolchain/cache locations under ~/.rustup and ~/.cargo are read-only here.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Shadow-only fake XMSS proofs and simulated crypto cost controls. The main changes are:

  • New shadow-integration feature wiring between ethlambda and ethlambda-crypto.
  • Feature-gated CLI flags for fake XMSS and per-operation rates.
  • Global Shadow cost configuration and deterministic dummy proof generation.
  • Fake and sleep branches in XMSS aggregation, merge, verify, and split helpers.
  • Tests for the Shadow cost helpers and fake interception paths.

Confidence Score: 4/5

The fake proof path needs fixes before merging.

  • Several fake proof seeds omit validator or binding data that the real crypto path receives.
  • Shadow runs can collapse distinct aggregates or block proofs into the same dummy bytes.
  • The feature gating and CLI wiring look consistent.

crates/common/crypto/src/lib.rs

Important Files Changed

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

Comment thread crates/common/crypto/src/lib.rs Outdated
Comment on lines +176 to +180
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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

Comment thread crates/common/crypto/src/lib.rs Outdated
Comment on lines +232 to +239
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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Comment thread crates/common/crypto/src/lib.rs Outdated
Comment on lines +291 to +297
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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

Comment thread crates/common/crypto/src/lib.rs Outdated
Comment on lines +389 to +398
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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

Comment on lines +491 to +497
#[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],
));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Review: feat(shadow): dummy XMSS proofs + sim-cost sleeps behind shadow-integration

Overall this is a clean, well-isolated feature addition. The feature-gating is consistent throughout (module-level #[cfg], per-call-site #[cfg], transitive Cargo feature flag), the atomic-based global config avoids unsafe/lock contention on the hot path, and the deterministic dummy-proof generation is careful to seed only from consensus-visible inputs (never pointers/addresses), which is the right call for reproducible Shadow runs. AggregationBits correctly stay real while only the SNARK bytes are stubbed — that's the detail that actually matters for a useful simulation, and it's handled correctly at every one of the 7 call sites. Good test coverage of the delay math and the fake-path shapes.

One substantive finding and a couple of minor notes below.

Findings

1. Test isolation gap for the process-global fake-mode toggle (crates/common/crypto/src/lib.rs, crates/common/crypto/src/shadow_cost.rs)

FAKE_ENABLED/AGG_RATE/VERIFY_RATE/MERGE_RATE are process-wide statics. The new shadow_cost::tests and fake_interception_tests modules correctly serialize themselves with TEST_LOCK, but the pre-existing real-crypto round-trip tests (test_aggregate_single_signature, test_aggregate_multiple_signatures, test_verify_wrong_message_fails, test_verify_wrong_slot_fails, test_type_2_merge_verify_split_round_trip at lib.rs:573-680) call the same 7 gated functions but never touch TEST_LOCK.

Today this is masked because those five tests are all #[ignore = "too slow"], so the default cargo test -p ethlambda-crypto --features shadow-integration run (as documented in the PR's testing section) never executes them alongside the fake tests. But if anyone later runs cargo test --features shadow-integration -- --include-ignored (a natural thing to try, to confirm real XMSS still round-trips under this feature), Rust's default multi-threaded test runner can interleave a real test's call to e.g. aggregate_signatures/verify_aggregated_signature with a fake test that has FAKE_ENABLED=true mid-flight (holding TEST_LOCK, which the real test doesn't check). That would nondeterministically flip a real crypto test into the fake branch and produce a spurious failure (or, less likely, a spurious pass).

Suggest either having the five ignored real-crypto tests also acquire shadow_cost::TEST_LOCK, or adding a comment on TEST_LOCK explicitly warning that any test touching these 7 functions under this feature must take the lock — otherwise this is a latent flakiness trap for the next person who runs the full (non-default) suite.

Minor / non-blocking

  • shadow_cost.rs: aggregate_delay/verify_delay/merge_delay doc comments say "Nanoseconds to sleep…" but the functions return Duration, not raw nanoseconds — slightly misleading wording.
  • lib.rs (e.g. lines 169-182, 222-239, 282-297): the #[cfg(feature = "shadow-integration")] let agg_n = ...; followed immediately by a second #[cfg(...)] if ... block is repeated 4x with identical structure. Could be collapsed into a single #[cfg(feature = "shadow-integration")] { ... } block per call site for readability, but purely stylistic.
  • Explicitly documented (not a bug, just worth double-checking intent): sim-cost sleeps are applied even when --shadow-xmss-fake is off, meaning a real leanVM proving/verifying call plus an independent injected sleep both contribute delay in that mode. That's called out as intentional zeam-parity behavior in the PR description, so no change needed, just flagging that it's easy to misread as double-counting.

No correctness, security, or consensus-safety issues found in the production (non-shadow) build — the whole surface is compiled out unless shadow-integration is enabled, and the one always-compiled change (ethlambda-crypto added as an unconditional bin dependency) is inert without the feature.


Automated review by Claude (Anthropic) · sonnet · custom prompt

…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
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