Skip to content

Commit 4c9f42e

Browse files
authored
feat(shadow): dummy XMSS proofs + sim-cost sleeps behind shadow-integration (lambdaclass#484)
## Summary Ports zeam's Shadow-simulator **fake-XMSS** mode + rate-based **sim-cost sleeps** into ethlambda, so the [lean-shadow-fuzzer](https://github.com/kamilsa/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 (`ethlambda` → `ethlambda-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/rate`s into aggregation. Unset or ≤0 disables. | | `--shadow-xmss-verify-aggregated-signatures-rate <f64>` | Sigs verified/aggregate/sec; injects `n/rate`s into Type-1 verify. | | `--shadow-xmss-merge-rate <f64>` | Type-1 components merged into a Type-2/sec; injects `n/rate`s 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 ```bash # 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 ```
1 parent 61981b3 commit 4c9f42e

7 files changed

Lines changed: 327 additions & 5 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎bin/ethlambda/Cargo.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ jemalloc = ["dep:tikv-jemallocator"]
1616
# Shadow simulator compatibility: single-threaded tokio runtime and no jemalloc.
1717
# The quinn-udp UDP fallback is a Cargo `[patch]` (which cannot be feature-gated),
1818
# injected at build time by `shadow/build.sh` / `make shadow-build`.
19-
shadow-integration = []
19+
shadow-integration = ["ethlambda-crypto/shadow-integration"]
2020

2121
[dependencies]
2222
ethlambda-blockchain.workspace = true
23+
ethlambda-crypto.workspace = true
2324
ethlambda-network-api.workspace = true
2425
ethlambda-p2p.workspace = true
2526
ethlambda-types.workspace = true

‎bin/ethlambda/src/cli.rs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,46 @@ pub(crate) struct CliOptions {
107107
/// `on_block`.
108108
#[arg(long, default_value = "3")]
109109
pub(crate) max_attestations_per_block: usize,
110+
/// Shadow-simulator sim-cost + fake-XMSS flags (only under the
111+
/// `shadow-integration` feature).
112+
#[cfg(feature = "shadow-integration")]
113+
#[command(flatten)]
114+
pub(crate) shadow: ShadowOptions,
115+
}
116+
117+
/// Shadow-simulator sim-cost + fake-XMSS flags. Compiled only under the
118+
/// `shadow-integration` feature.
119+
#[cfg(feature = "shadow-integration")]
120+
#[derive(Debug, clap::Args)]
121+
pub(crate) struct ShadowOptions {
122+
/// Shadow sim only: replace the XMSS aggregation prover/verifier with a
123+
/// deterministic stub (no leanVM proving/verifying). Off by default.
124+
#[arg(long, default_value = "false")]
125+
pub(crate) shadow_xmss_fake: bool,
126+
127+
/// Shadow sim only: signatures aggregated per second. Injects a sleep of
128+
/// n/rate seconds into aggregation so its CPU cost shows up on Shadow's
129+
/// virtual clock. Unset or <= 0 disables.
130+
#[arg(long)]
131+
pub(crate) shadow_xmss_aggregate_signatures_rate: Option<f64>,
132+
133+
/// Shadow sim only: signatures verified per aggregate per second; injects
134+
/// a sleep of n/rate seconds into verification. Unset or <= 0 disables.
135+
#[arg(long)]
136+
pub(crate) shadow_xmss_verify_aggregated_signatures_rate: Option<f64>,
137+
138+
/// Shadow sim only: Type-1 components merged into a Type-2 per second;
139+
/// injects a sleep of n/rate seconds into the proposal Type-2 merge.
140+
/// Unset or <= 0 disables.
141+
#[arg(long)]
142+
pub(crate) shadow_xmss_merge_rate: Option<f64>,
143+
144+
/// Shadow sim only: byte length of each fake stub proof. Defaults to 32
145+
/// KiB; capped at the 512 KiB on-wire proof limit.
146+
#[arg(
147+
long,
148+
default_value_t = ethlambda_crypto::shadow_cost::DEFAULT_FAKE_PROOF_SIZE as u64,
149+
value_parser = clap::value_parser!(u64).range(1..=524_288)
150+
)]
151+
pub(crate) shadow_xmss_fake_proof_size: u64,
110152
}

‎bin/ethlambda/src/main.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ async fn main() -> eyre::Result<()> {
8080

8181
let options = CliOptions::parse();
8282

83+
#[cfg(feature = "shadow-integration")]
84+
init_shadow_cost(&options.shadow);
85+
8386
// Initialize metrics
8487
ethlambda_blockchain::metrics::init();
8588
ethlambda_blockchain::metrics::set_node_info("ethlambda", version::CLIENT_VERSION);
@@ -301,6 +304,30 @@ async fn main() -> eyre::Result<()> {
301304
Ok(())
302305
}
303306

307+
/// Apply the Shadow-simulator sim-cost / fake-XMSS configuration from the CLI.
308+
///
309+
/// Compiled only under the `shadow-integration` feature. Call once at startup,
310+
/// before any consensus/aggregation work, so the fake-proof and sim-cost hooks
311+
/// are installed before the first signing or aggregation path runs.
312+
#[cfg(feature = "shadow-integration")]
313+
fn init_shadow_cost(shadow: &cli::ShadowOptions) {
314+
info!(
315+
fake = shadow.shadow_xmss_fake,
316+
aggregate_rate = ?shadow.shadow_xmss_aggregate_signatures_rate,
317+
verify_rate = ?shadow.shadow_xmss_verify_aggregated_signatures_rate,
318+
merge_rate = ?shadow.shadow_xmss_merge_rate,
319+
fake_proof_size = shadow.shadow_xmss_fake_proof_size,
320+
"Applying Shadow XMSS sim-cost / fake-XMSS config"
321+
);
322+
ethlambda_crypto::shadow_cost::init(
323+
shadow.shadow_xmss_fake,
324+
shadow.shadow_xmss_aggregate_signatures_rate,
325+
shadow.shadow_xmss_verify_aggregated_signatures_rate,
326+
shadow.shadow_xmss_merge_rate,
327+
shadow.shadow_xmss_fake_proof_size as usize,
328+
);
329+
}
330+
304331
/// Boot the binary in Hive test-driver mode.
305332
///
306333
/// Skips every consensus/p2p subsystem and just exposes the

‎crates/common/crypto/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ leansig.workspace = true
2121
thiserror.workspace = true
2222
rand.workspace = true
2323

24+
[features]
25+
shadow-integration = []
26+
2427
[dev-dependencies]
2528
hex.workspace = true

‎crates/common/crypto/src/lib.rs‎

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use lean_multisig::{
1414
use leansig_wrapper::{XmssPublicKey as LeanSigPubKey, XmssSignature as LeanSigSignature};
1515
use thiserror::Error;
1616

17+
#[cfg(feature = "shadow-integration")]
18+
pub mod shadow_cost;
19+
1720
/// log(1/rate) for the WHIR commitment scheme used inside lean-multisig.
1821
const LOG_INV_RATE: usize = 2;
1922

@@ -163,6 +166,19 @@ pub fn aggregate_signatures(
163166
return Err(AggregationError::EmptyInput);
164167
}
165168

169+
#[cfg(feature = "shadow-integration")]
170+
if crate::shadow_cost::fake_xmss() {
171+
let agg_n = public_keys.len();
172+
let count_bytes = public_keys.len().to_le_bytes();
173+
let slot_bytes = slot.to_le_bytes();
174+
let dummy = crate::shadow_cost::fill_fake_proof(
175+
crate::shadow_cost::fake_proof_size(),
176+
&[&message.0, &slot_bytes, &count_bytes],
177+
);
178+
crate::shadow_cost::sleep(crate::shadow_cost::aggregate_delay(agg_n));
179+
return Ok(dummy);
180+
}
181+
166182
ensure_prover_ready();
167183

168184
let raw_xmss: Vec<(LeanSigPubKey, LeanSigSignature)> = public_keys
@@ -174,7 +190,8 @@ pub fn aggregate_signatures(
174190
let proof = aggregate_single_message_signatures(&[], raw_xmss, message.0, slot, LOG_INV_RATE)
175191
.map_err(|err| AggregationError::ProverFailure(err.to_string()))?;
176192

177-
compress_type1_to_byte_list(&proof)
193+
let result = compress_type1_to_byte_list(&proof)?;
194+
Ok(result)
178195
}
179196

180197
/// Aggregate both existing Type-1 proofs (children) and raw XMSS signatures.
@@ -201,6 +218,22 @@ pub fn aggregate_mixed(
201218
return Err(AggregationError::InsufficientChildren(children.len()));
202219
}
203220

221+
#[cfg(feature = "shadow-integration")]
222+
if crate::shadow_cost::fake_xmss() {
223+
let agg_n = raw_public_keys.len();
224+
let count_bytes = raw_public_keys.len().to_le_bytes();
225+
let slot_bytes = slot.to_le_bytes();
226+
let mut parts: Vec<&[u8]> = vec![&message.0, &slot_bytes];
227+
for (_, proof) in &children {
228+
parts.push(proof.iter().as_slice());
229+
}
230+
parts.push(&count_bytes);
231+
let dummy =
232+
crate::shadow_cost::fill_fake_proof(crate::shadow_cost::fake_proof_size(), &parts);
233+
crate::shadow_cost::sleep(crate::shadow_cost::aggregate_delay(agg_n));
234+
return Ok(dummy);
235+
}
236+
204237
ensure_prover_ready();
205238

206239
let children_native: Vec<LMType1> = children
@@ -224,7 +257,8 @@ pub fn aggregate_mixed(
224257
)
225258
.map_err(|err| AggregationError::ProverFailure(err.to_string()))?;
226259

227-
compress_type1_to_byte_list(&proof)
260+
let result = compress_type1_to_byte_list(&proof)?;
261+
Ok(result)
228262
}
229263

230264
/// Recursively aggregate two or more already-aggregated Type-1 proofs into one.
@@ -240,6 +274,20 @@ pub fn aggregate_proofs(
240274
return Err(AggregationError::InsufficientChildren(children.len()));
241275
}
242276

277+
#[cfg(feature = "shadow-integration")]
278+
if crate::shadow_cost::fake_xmss() {
279+
let agg_n = children.len();
280+
let slot_bytes = slot.to_le_bytes();
281+
let mut parts: Vec<&[u8]> = vec![&message.0, &slot_bytes];
282+
for (_, proof) in &children {
283+
parts.push(proof.iter().as_slice());
284+
}
285+
let dummy =
286+
crate::shadow_cost::fill_fake_proof(crate::shadow_cost::fake_proof_size(), &parts);
287+
crate::shadow_cost::sleep(crate::shadow_cost::aggregate_delay(agg_n));
288+
return Ok(dummy);
289+
}
290+
243291
ensure_prover_ready();
244292

245293
let children_native: Vec<LMType1> = children
@@ -257,7 +305,8 @@ pub fn aggregate_proofs(
257305
)
258306
.map_err(|err| AggregationError::ProverFailure(err.to_string()))?;
259307

260-
compress_type1_to_byte_list(&proof)
308+
let result = compress_type1_to_byte_list(&proof)?;
309+
Ok(result)
261310
}
262311

263312
/// Verify a Type-1 aggregated signature proof.
@@ -272,6 +321,14 @@ pub fn verify_aggregated_signature(
272321
message: &H256,
273322
slot: u32,
274323
) -> Result<(), VerificationError> {
324+
// Skip the real verifier under fake-XMSS; otherwise verify for real.
325+
#[cfg(feature = "shadow-integration")]
326+
if crate::shadow_cost::fake_xmss() {
327+
let verify_n = public_keys.len();
328+
// Model verify cost on the virtual clock (no-op unless a rate is set).
329+
crate::shadow_cost::sleep(crate::shadow_cost::verify_delay(verify_n));
330+
return Ok(());
331+
}
275332
ensure_verifier_ready();
276333

277334
let lean_pubkeys = into_lean_pubkeys(public_keys);
@@ -310,6 +367,21 @@ pub fn merge_type_1s_into_type_2(
310367
return Err(AggregationError::EmptyInput);
311368
}
312369

370+
#[cfg(feature = "shadow-integration")]
371+
if crate::shadow_cost::fake_xmss() {
372+
let merge_n = type_1s.len();
373+
let count_bytes = type_1s.len().to_le_bytes();
374+
let mut parts: Vec<&[u8]> = Vec::with_capacity(type_1s.len() + 1);
375+
for (_, proof) in &type_1s {
376+
parts.push(proof.iter().as_slice());
377+
}
378+
parts.push(&count_bytes);
379+
let dummy =
380+
crate::shadow_cost::fill_fake_proof(crate::shadow_cost::fake_proof_size(), &parts);
381+
crate::shadow_cost::sleep(crate::shadow_cost::merge_delay(merge_n));
382+
return Ok(dummy);
383+
}
384+
313385
ensure_prover_ready();
314386

315387
let type_1s_native: Vec<LMType1> = type_1s
@@ -321,7 +393,8 @@ pub fn merge_type_1s_into_type_2(
321393
let merged = merge_single_message_aggregates(type_1s_native, LOG_INV_RATE)
322394
.map_err(|err| AggregationError::ProverFailure(err.to_string()))?;
323395

324-
compress_type2_to_byte_list(&merged)
396+
let result = compress_type2_to_byte_list(&merged)?;
397+
Ok(result)
325398
}
326399

327400
/// Verify a Type-2 merged proof against the per-component expected bindings.
@@ -341,6 +414,11 @@ pub fn verify_type_2_signature(
341414
});
342415
}
343416

417+
#[cfg(feature = "shadow-integration")]
418+
if crate::shadow_cost::fake_xmss() {
419+
return Ok(());
420+
}
421+
344422
ensure_verifier_ready();
345423

346424
let pubkeys_per_info: Vec<Vec<LeanSigPubKey>> = pubkeys_per_component
@@ -391,6 +469,14 @@ pub fn split_type_2_by_message(
391469
pubkeys_per_component: Vec<Vec<ValidatorPublicKey>>,
392470
message: &H256,
393471
) -> Result<ByteList512KiB, AggregationError> {
472+
#[cfg(feature = "shadow-integration")]
473+
if crate::shadow_cost::fake_xmss() {
474+
return Ok(crate::shadow_cost::fill_fake_proof(
475+
crate::shadow_cost::fake_proof_size(),
476+
&[proof_data, &message.0],
477+
));
478+
}
479+
394480
ensure_prover_ready();
395481

396482
let pubkeys_per_info: Vec<Vec<LeanSigPubKey>> = pubkeys_per_component

0 commit comments

Comments
 (0)