From 36c388ee8b321e01183ca4b83fec41cea529ee60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:02:05 -0300 Subject: [PATCH 01/14] feat(crypto)!: track leanVM main (internalized XMSS + reworked aggregation) Point the leanVM dependency at `main`. leanVM's `main` ("Xmss api rework") removed the `lean-multisig` and `leansig_wrapper` crates ethlambda imported, internalized XMSS in its own `xmss` crate (dropping the external leanSig dependency), and reworked the recursive-aggregation API. Tracking main is therefore a migration of the whole signature stack, not a version bump. Changes: - Cargo: replace `leansig`/`lean-multisig`/`leansig_wrapper` with `xmss` + `rec_aggregation` (git, branch=main), plus `ssz` (ethereum_ssz, used by the xmss SSZ impls) and `postcard` (secret-key format). - types::signature: rebuild ValidatorPublicKey/Signature/SecretKey on leanVM's `xmss` crate. Keys/signatures (de)serialize via SSZ (`PUB_KEY_SSZ_LEN` / `SIGNATURE_SSZ_LEN`); secret keys via postcard. `SIGNATURE_SIZE` and the new `PUBLIC_KEY_SIZE` are sourced from the xmss scheme constants. The sliding preparation-window API becomes a fixed activation range (`advance_preparation` is now a no-op; keys warm their signing cache on demand). - crypto: rewrite aggregation/verification against `rec_aggregation`. Proofs now serialize with `to_bytes()`/`from_bytes()` and embed participant pubkeys, so the verify paths cross-check the embedded set against the expected validators instead of attaching them at decode time. `setup_prover`/`setup_verifier` are replaced by the idempotent `init_aggregation_bytecode`. - Validator public keys are now 32 bytes (was 52); genesis/state layouts and fixtures updated accordingly. BREAKING: this changes the on-wire signature/proof formats and the genesis key format. It is interop-incompatible with clients still on the previous scheme and requires regenerated genesis keys; the fixture-driven spec tests need regenerated fixtures. Landing this needs ecosystem coordination. --- Cargo.lock | 680 +++++------------- Cargo.toml | 13 +- bin/ethlambda/src/checkpoint_sync.rs | 12 +- bin/ethlambda/src/main.rs | 11 +- crates/blockchain/Cargo.toml | 1 - crates/blockchain/src/aggregation.rs | 29 +- crates/blockchain/src/block_builder.rs | 20 +- crates/blockchain/src/reaggregate.rs | 7 +- crates/blockchain/state_transition/src/lib.rs | 4 +- crates/common/crypto/Cargo.toml | 10 +- crates/common/crypto/src/lib.rs | 199 ++--- crates/common/test-fixtures/src/common.rs | 7 +- crates/common/types/Cargo.toml | 4 +- crates/common/types/src/attestation.rs | 60 +- crates/common/types/src/block.rs | 32 +- crates/common/types/src/genesis.rs | 34 +- crates/common/types/src/signature.rs | 198 ++--- crates/common/types/src/state.rs | 4 +- crates/net/rpc/src/genesis.rs | 4 +- crates/storage/Cargo.toml | 1 - crates/storage/src/state_diff.rs | 8 +- crates/storage/src/store.rs | 23 +- 22 files changed, 524 insertions(+), 837 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddd13c0c..f9063761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "air" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "field", + "poly", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -652,19 +661,18 @@ dependencies = [ [[package]] name = "backend" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "mt-air", - "mt-fiat-shamir", - "mt-field", - "mt-koala-bear", - "mt-poly", - "mt-sumcheck", - "mt-symetric", - "mt-utils", - "mt-whir", +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "air", + "fiat-shamir", + "field", + "koala-bear", "parallel", - "tracing", + "poly", + "sumcheck", + "symetric", + "utils", + "whir", "zk-alloc", ] @@ -1166,12 +1174,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - [[package]] name = "const-str" version = "0.4.3" @@ -1601,7 +1603,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid 0.9.6", + "const-oid", "pem-rfc7468", "zeroize", ] @@ -1732,7 +1734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", + "const-oid", "crypto-common 0.1.7", "subtle", ] @@ -1744,7 +1746,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", - "const-oid 0.10.2", "crypto-common 0.2.2", ] @@ -2048,7 +2049,6 @@ dependencies = [ "ethlambda-test-fixtures", "ethlambda-types", "hex", - "leansig", "libssz", "libssz-types", "rand 0.10.1", @@ -2065,13 +2065,13 @@ dependencies = [ name = "ethlambda-crypto" version = "0.1.0" dependencies = [ + "ethereum_ssz", "ethlambda-types", "hex", - "lean-multisig", - "leansig", - "leansig_wrapper", "rand 0.10.1", + "rec_aggregation", "thiserror 2.0.18", + "xmss", ] [[package]] @@ -2170,7 +2170,6 @@ name = "ethlambda-storage" version = "0.1.0" dependencies = [ "ethlambda-types", - "leansig", "libssz", "libssz-derive", "libssz-types", @@ -2199,18 +2198,20 @@ name = "ethlambda-types" version = "0.1.0" dependencies = [ "datatest-stable 0.3.3", + "ethereum_ssz", "ethlambda-test-fixtures", "hex", - "leansig", "libssz", "libssz-derive", "libssz-merkle", "libssz-types", + "postcard", "rand 0.10.1", "serde", "serde_json", "serde_yaml_ng", "thiserror 2.0.18", + "xmss", ] [[package]] @@ -2560,6 +2561,31 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-shamir" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "field", + "koala-bear", + "parallel", + "serde", + "symetric", + "utils", +] + +[[package]] +name = "field" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "parallel", + "paste", + "rand 0.10.1", + "serde", + "utils", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -2962,7 +2988,7 @@ dependencies = [ "hash32", "rustc_version 0.4.1", "serde", - "spin 0.9.8", + "spin", "stable_deref_trait", ] @@ -3618,6 +3644,18 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "koala-bear" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "field", + "paste", + "rand 0.10.1", + "serde", + "utils", +] + [[package]] name = "konst" version = "0.2.20" @@ -3644,7 +3682,7 @@ dependencies = [ "serde_arrays", "sha2", "sp1_bls12_381", - "spin 0.9.8", + "spin", ] [[package]] @@ -3668,126 +3706,43 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lean-multisig" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "backend", - "clap", - "lean_vm", - "leansig_wrapper", - "libc", - "rand 0.10.1", - "rec_aggregation", - "serde_json", - "sub_protocols", - "system-info", - "utils", - "zk-alloc", -] - [[package]] name = "lean_compiler" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "include_dir", "lean_vm", "pest", "pest_derive", - "rand 0.10.1", "sub_protocols", - "tracing", - "utils", + "xmss", ] [[package]] name = "lean_prover" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", - "itertools 0.14.0", "lean_compiler", "lean_vm", - "pest", - "pest_derive", "rand 0.10.1", "serde", "sub_protocols", "tracing", - "utils", + "xmss", ] [[package]] name = "lean_vm" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", - "itertools 0.14.0", - "leansig_wrapper", - "pest", - "pest_derive", - "rand 0.10.1", - "serde", "tracing", - "utils", -] - -[[package]] -name = "leansig" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanSig?branch=devnet4#15cbdd43ec8525aa43fea2f42cafc5ed366084ae" -dependencies = [ - "dashmap", - "ethereum_ssz", - "num-bigint 0.4.6", - "num-traits", - "p3-baby-bear", - "p3-field 0.5.1", - "p3-koala-bear 0.5.1", - "p3-symmetric 0.5.1", - "rand 0.10.1", - "rayon", - "serde", - "sha3 0.10.9", - "thiserror 2.0.18", -] - -[[package]] -name = "leansig_fast_keygen" -version = "0.1.0" -source = "git+https://github.com/TomWambsgans/leanSig?branch=devnet4-fast-keygen#0fa9e19b8946ef50a34f3d50d82918b98bcfa4a5" -dependencies = [ - "dashmap", - "ethereum_ssz", - "num-bigint 0.4.6", - "num-traits", - "p3-baby-bear", - "p3-field 0.5.1", - "p3-koala-bear 0.5.1", - "p3-symmetric 0.5.1", - "rand 0.10.1", - "rayon", - "serde", - "sha3 0.10.9", - "thiserror 2.0.18", -] - -[[package]] -name = "leansig_wrapper" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "backend", - "ethereum_ssz", - "leansig", - "leansig_fast_keygen", - "p3-field 0.5.1", - "rand 0.10.1", + "xmss", ] [[package]] @@ -4637,15 +4592,6 @@ dependencies = [ "libc", ] -[[package]] -name = "lz4_flex" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" -dependencies = [ - "twox-hash", -] - [[package]] name = "malachite" version = "0.6.1" @@ -4812,127 +4758,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "mt-air" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "mt-field", - "mt-poly", -] - -[[package]] -name = "mt-fiat-shamir" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "mt-field", - "mt-koala-bear", - "mt-symetric", - "mt-utils", - "parallel", - "serde", - "tracing", -] - -[[package]] -name = "mt-field" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "itertools 0.14.0", - "mt-utils", - "num-bigint 0.3.3", - "parallel", - "paste", - "rand 0.10.1", - "serde", - "tracing", -] - -[[package]] -name = "mt-koala-bear" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "itertools 0.14.0", - "mt-field", - "mt-utils", - "num-bigint 0.3.3", - "paste", - "rand 0.10.1", - "serde", - "tracing", -] - -[[package]] -name = "mt-poly" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "itertools 0.14.0", - "mt-field", - "mt-utils", - "parallel", - "rand 0.10.1", - "serde", - "system-info", - "zk-alloc", -] - -[[package]] -name = "mt-sumcheck" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "mt-air", - "mt-fiat-shamir", - "mt-field", - "mt-poly", - "parallel", - "tracing", - "zk-alloc", -] - -[[package]] -name = "mt-symetric" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "mt-field", - "mt-koala-bear", - "parallel", - "zk-alloc", -] - -[[package]] -name = "mt-utils" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "serde", -] - -[[package]] -name = "mt-whir" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" -dependencies = [ - "itertools 0.14.0", - "mt-fiat-shamir", - "mt-field", - "mt-koala-bear", - "mt-poly", - "mt-sumcheck", - "mt-symetric", - "mt-utils", - "parallel", - "rand 0.10.1", - "system-info", - "tracing", - "zk-alloc", -] - [[package]] name = "multiaddr" version = "0.18.2" @@ -5310,21 +5135,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "p3-baby-bear" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "p3-challenger 0.5.1", - "p3-field 0.5.1", - "p3-mds 0.5.1", - "p3-monty-31", - "p3-poseidon1", - "p3-poseidon2 0.5.1", - "p3-symmetric 0.5.1", - "rand 0.10.1", -] - [[package]] name = "p3-bn254-fr" version = "0.3.3-succinct" @@ -5333,9 +5143,9 @@ checksum = "577200e3fa7e49e2b21e940a6dc7399dc63acb8581da088558cdf7c455adafc0" dependencies = [ "ff", "num-bigint 0.4.6", - "p3-field 0.3.3-succinct", - "p3-poseidon2 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", + "p3-field", + "p3-poseidon2", + "p3-symmetric", "rand 0.8.6", "serde", ] @@ -5346,51 +5156,24 @@ version = "0.3.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75358edd6e2562752c01f5064a66d88144a3e75ace0407166dbdf8a727597f52" dependencies = [ - "p3-field 0.3.3-succinct", - "p3-maybe-rayon 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", - "p3-util 0.3.3-succinct", + "p3-field", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", "serde", "tracing", ] -[[package]] -name = "p3-challenger" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "p3-field 0.5.1", - "p3-maybe-rayon 0.5.1", - "p3-monty-31", - "p3-symmetric 0.5.1", - "p3-util 0.5.1", - "tracing", -] - [[package]] name = "p3-dft" version = "0.3.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "761f1e1b014f2b1b69bd0309124e233d64aa3590e6a41ee786000dd849506d51" dependencies = [ - "p3-field 0.3.3-succinct", - "p3-matrix 0.3.3-succinct", - "p3-maybe-rayon 0.3.3-succinct", - "p3-util 0.3.3-succinct", - "tracing", -] - -[[package]] -name = "p3-dft" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "itertools 0.14.0", - "p3-field 0.5.1", - "p3-matrix 0.5.1", - "p3-maybe-rayon 0.5.1", - "p3-util 0.5.1", - "spin 0.10.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", "tracing", ] @@ -5403,26 +5186,11 @@ dependencies = [ "itertools 0.12.1", "num-bigint 0.4.6", "num-traits", - "p3-util 0.3.3-succinct", + "p3-util", "rand 0.8.6", "serde", ] -[[package]] -name = "p3-field" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "itertools 0.14.0", - "num-bigint 0.4.6", - "p3-maybe-rayon 0.5.1", - "p3-util 0.5.1", - "paste", - "rand 0.10.1", - "serde", - "tracing", -] - [[package]] name = "p3-koala-bear" version = "0.3.3-succinct" @@ -5431,30 +5199,15 @@ checksum = "6cea0ba3389b034b6088d566aea8b57aa29dd2e180966e0c8056f61331c92b4e" dependencies = [ "cfg-if 1.0.4", "num-bigint 0.4.6", - "p3-field 0.3.3-succinct", - "p3-mds 0.3.3-succinct", - "p3-poseidon2 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", + "p3-field", + "p3-mds", + "p3-poseidon2", + "p3-symmetric", "rand 0.8.6", "rustc_version 0.4.1", "serde", ] -[[package]] -name = "p3-koala-bear" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "p3-challenger 0.5.1", - "p3-field 0.5.1", - "p3-mds 0.5.1", - "p3-monty-31", - "p3-poseidon1", - "p3-poseidon2 0.5.1", - "p3-symmetric 0.5.1", - "rand 0.10.1", -] - [[package]] name = "p3-matrix" version = "0.3.3-succinct" @@ -5462,39 +5215,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fae5cc6ce726cc265cc687c1214e3f1ac1f5c6e973442286ba00d1e75da1c3cb" dependencies = [ "itertools 0.12.1", - "p3-field 0.3.3-succinct", - "p3-maybe-rayon 0.3.3-succinct", - "p3-util 0.3.3-succinct", + "p3-field", + "p3-maybe-rayon", + "p3-util", "rand 0.8.6", "serde", "tracing", ] -[[package]] -name = "p3-matrix" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "itertools 0.14.0", - "p3-field 0.5.1", - "p3-maybe-rayon 0.5.1", - "p3-util 0.5.1", - "rand 0.10.1", - "serde", - "tracing", -] - [[package]] name = "p3-maybe-rayon" version = "0.3.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac1d2f102cf8c71dba1b449575c99697781fcc028831e83d2245787bd7a650" -[[package]] -name = "p3-maybe-rayon" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" - [[package]] name = "p3-mds" version = "0.3.3-succinct" @@ -5502,59 +5236,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f072643e385d65fb9eb089ee6824b320417f78671a0db748566e057e28b250e" dependencies = [ "itertools 0.12.1", - "p3-dft 0.3.3-succinct", - "p3-field 0.3.3-succinct", - "p3-matrix 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", - "p3-util 0.3.3-succinct", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-symmetric", + "p3-util", "rand 0.8.6", ] -[[package]] -name = "p3-mds" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "p3-dft 0.5.1", - "p3-field 0.5.1", - "p3-symmetric 0.5.1", - "p3-util 0.5.1", - "rand 0.10.1", -] - -[[package]] -name = "p3-monty-31" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "itertools 0.14.0", - "num-bigint 0.4.6", - "p3-dft 0.5.1", - "p3-field 0.5.1", - "p3-matrix 0.5.1", - "p3-maybe-rayon 0.5.1", - "p3-mds 0.5.1", - "p3-poseidon1", - "p3-poseidon2 0.5.1", - "p3-symmetric 0.5.1", - "p3-util 0.5.1", - "paste", - "rand 0.10.1", - "serde", - "spin 0.10.0", - "tracing", -] - -[[package]] -name = "p3-poseidon1" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "p3-field 0.5.1", - "p3-symmetric 0.5.1", - "rand 0.10.1", -] - [[package]] name = "p3-poseidon2" version = "0.3.3-succinct" @@ -5562,25 +5251,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00cc4b6e8a439f79541b0910a016da9e6e12a05a24309bbb713e1db0db396952" dependencies = [ "gcd", - "p3-field 0.3.3-succinct", - "p3-mds 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", + "p3-field", + "p3-mds", + "p3-symmetric", "rand 0.8.6", "serde", ] -[[package]] -name = "p3-poseidon2" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "p3-field 0.5.1", - "p3-mds 0.5.1", - "p3-symmetric 0.5.1", - "p3-util 0.5.1", - "rand 0.10.1", -] - [[package]] name = "p3-symmetric" version = "0.3.3-succinct" @@ -5588,18 +5265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebff7fea7deb08a57ccf731a0ed39df25cc66a0e0c2d92c4472c4dee02ee21" dependencies = [ "itertools 0.12.1", - "p3-field 0.3.3-succinct", - "serde", -] - -[[package]] -name = "p3-symmetric" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "itertools 0.14.0", - "p3-field 0.5.1", - "p3-util 0.5.1", + "p3-field", "serde", ] @@ -5612,15 +5278,6 @@ dependencies = [ "serde", ] -[[package]] -name = "p3-util" -version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb2b654acddef22" -dependencies = [ - "serde", - "transpose", -] - [[package]] name = "pairing" version = "0.23.0" @@ -5633,7 +5290,7 @@ dependencies = [ [[package]] name = "parallel" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "system-info", ] @@ -5825,6 +5482,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "poly" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "field", + "parallel", + "rand 0.10.1", + "serde", + "system-info", + "utils", + "zk-alloc", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -6397,25 +6068,20 @@ dependencies = [ [[package]] name = "rec_aggregation" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "include_dir", "lean_compiler", "lean_prover", "lean_vm", - "leansig_wrapper", - "lz4_flex", "objc2", "objc2-foundation", "postcard", - "rand 0.10.1", "serde", - "sha3 0.11.0", "sub_protocols", "tracing", - "utils", - "zk-alloc", + "xmss", ] [[package]] @@ -7110,7 +6776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a473c3a06b466dd0708829415a8a9fab451740da066e07862c8c098904aaad6" dependencies = [ "itertools 0.14.0", - "p3-field 0.3.3-succinct", + "p3-field", "serde", ] @@ -7136,7 +6802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4e80df718cef7d3100658dc8b46fafcc994b814421ec9a7d0763a6ee1e5070c" dependencies = [ "futures", - "p3-challenger 0.3.3-succinct", + "p3-challenger", "serde", "slop-algebra", "slop-symmetric", @@ -7149,7 +6815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6586b1c0e66c503e4026a8cb007349fa99c2466957c5b09d18fe658d1391ed8" dependencies = [ "lazy_static", - "p3-koala-bear 0.3.3-succinct", + "p3-koala-bear", "serde", "slop-algebra", "slop-challenger", @@ -7163,7 +6829,7 @@ version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c956b11fff1b8a071fa4ba982dc35e458cff1620dc7b33d9cf22d8df30895f79" dependencies = [ - "p3-poseidon2 0.3.3-succinct", + "p3-poseidon2", ] [[package]] @@ -7181,7 +6847,7 @@ version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955145ad6e3a1d083a428f9274071cfbb44c3b29013aae9d6c4c29fb7328cfc0" dependencies = [ - "p3-symmetric 0.3.3-succinct", + "p3-symmetric", ] [[package]] @@ -7373,15 +7039,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "lock_api", -] - [[package]] name = "spki" version = "0.7.3" @@ -7410,12 +7067,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f446288b699d66d0fd2e30d1cfe7869194312524b3b9252594868ed26ef056a" -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "strsim" version = "0.11.1" @@ -7446,12 +7097,11 @@ dependencies = [ [[package]] name = "sub_protocols" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "lean_vm", "tracing", - "utils", ] [[package]] @@ -7460,6 +7110,31 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "sumcheck" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "air", + "fiat-shamir", + "field", + "parallel", + "poly", + "tracing", + "zk-alloc", +] + +[[package]] +name = "symetric" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "field", + "koala-bear", + "parallel", + "zk-alloc", +] + [[package]] name = "syn" version = "1.0.109" @@ -7540,7 +7215,7 @@ dependencies = [ [[package]] name = "system-info" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "libc", ] @@ -7951,28 +7626,12 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - [[package]] name = "typenum" version = "1.20.0" @@ -8089,10 +7748,10 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ - "backend", - "tracing", + "parallel", + "serde", "tracing-forest", "tracing-subscriber", ] @@ -8350,6 +8009,25 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whir" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "fiat-shamir", + "field", + "koala-bear", + "parallel", + "poly", + "rand 0.10.1", + "sumcheck", + "symetric", + "system-info", + "tracing", + "utils", + "zk-alloc", +] + [[package]] name = "wide" version = "0.7.33" @@ -8889,6 +8567,18 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "xmss" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +dependencies = [ + "backend", + "ethereum_ssz", + "postcard", + "rand 0.10.1", + "serde", +] + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -9055,7 +8745,7 @@ dependencies = [ [[package]] name = "zk-alloc" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30fdddbbf8ae26a333116c68cec7026" +source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "libc", "parallel", diff --git a/Cargo.toml b/Cargo.toml index 100b1a4a..28fc5b82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,8 +55,17 @@ prometheus = "0.14" clap = { version = "4.3", features = ["derive", "env"] } -# XMSS signatures -leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" } +# XMSS signatures + recursive aggregation (leanVM, main branch). +# leanVM's `main` internalized XMSS (dropping the external leanSig dependency) +# and reworked the aggregation API (see "Xmss api rework"). +xmss = { git = "https://github.com/leanEthereum/leanVM.git", branch = "main" } +rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", branch = "main" } + +# SSZ codec used by leanVM's xmss pubkey/signature types (ethereum_ssz, not libssz). +ssz = { package = "ethereum_ssz", version = "0.10" } + +# Secret-key (de)serialization for the leanVM xmss key format. +postcard = { version = "1.1.3", features = ["alloc"] } # SSZ implementation libssz = "0.2.2" diff --git a/bin/ethlambda/src/checkpoint_sync.rs b/bin/ethlambda/src/checkpoint_sync.rs index 9bed6372..e84aeb44 100644 --- a/bin/ethlambda/src/checkpoint_sync.rs +++ b/bin/ethlambda/src/checkpoint_sync.rs @@ -385,16 +385,16 @@ mod tests { fn create_test_validator() -> Validator { Validator { - attestation_pubkey: [1u8; 52], - proposal_pubkey: [11u8; 52], + attestation_pubkey: [1u8; 32], + proposal_pubkey: [11u8; 32], index: 0, } } fn create_different_validator() -> Validator { Validator { - attestation_pubkey: [2u8; 52], - proposal_pubkey: [22u8; 52], + attestation_pubkey: [2u8; 32], + proposal_pubkey: [22u8; 32], index: 0, } } @@ -402,8 +402,8 @@ mod tests { fn create_validators_with_indices(count: usize) -> Vec { (0..count) .map(|i| Validator { - attestation_pubkey: [i as u8 + 1; 52], - proposal_pubkey: [i as u8 + 101; 52], + attestation_pubkey: [i as u8 + 1; 32], + proposal_pubkey: [i as u8 + 101; 32], index: i as u64, }) .collect() diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index b4d2828d..e0392eb5 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -479,8 +479,8 @@ fn read_bootnodes(bootnodes_path: impl AsRef) -> eyre::Result Vec { (0..n) .map(|i| Validator { - attestation_pubkey: [i as u8; 52], - proposal_pubkey: [i as u8; 52], + attestation_pubkey: [i as u8; 32], + proposal_pubkey: [i as u8; 32], index: i as u64, }) .collect() } - /// A cheap-but-real XMSS signature (tiny lifetime, cached) for tests that - /// only need `ValidatorSignature::from_bytes` to succeed. `resolve_job` - /// never checks signature validity, only that it clones and carries a - /// resolvable id — mirrors `ethlambda_storage::store::tests::make_dummy_sig`. + /// A structurally valid XMSS signature for tests that only need + /// `ValidatorSignature::from_bytes` to succeed. `resolve_job` never checks + /// signature validity, only that it clones and carries a resolvable id — + /// mirrors `ethlambda_storage::store::tests::make_dummy_sig`. An all-zero + /// blob decodes as a valid (unverifiable) signature. fn dummy_sig() -> ValidatorSignature { - use ethlambda_types::signature::LeanSignatureScheme; - use leansig::{serialization::Serializable, signature::SignatureScheme}; - use rand::{SeedableRng, rngs::StdRng}; - - static CACHED_SIG: std::sync::LazyLock> = std::sync::LazyLock::new(|| { - let mut rng = StdRng::seed_from_u64(42); - let lifetime = 1 << 5; // small for speed - let (_pk, sk) = LeanSignatureScheme::key_gen(&mut rng, 0, lifetime); - let sig = LeanSignatureScheme::sign(&sk, 0, &[0u8; 32]).unwrap(); - sig.to_bytes() - }); - - ValidatorSignature::from_bytes(&CACHED_SIG).expect("cached test signature") + use ethlambda_types::signature::SIGNATURE_SIZE; + ValidatorSignature::from_bytes(&vec![0u8; SIGNATURE_SIZE]) + .expect("all-zero test signature decodes") } /// A `HashedAttestationData` over default (all-zero) data for `resolve_job` diff --git a/crates/blockchain/src/block_builder.rs b/crates/blockchain/src/block_builder.rs index 787c55b4..91935f08 100644 --- a/crates/blockchain/src/block_builder.rs +++ b/crates/blockchain/src/block_builder.rs @@ -973,8 +973,8 @@ mod tests { let validators: Vec<_> = (0..NUM_VALIDATORS) .map(|i| ethlambda_types::state::Validator { - attestation_pubkey: [i as u8; 52], - proposal_pubkey: [i as u8; 52], + attestation_pubkey: [i as u8; 32], + proposal_pubkey: [i as u8; 32], index: i as u64, }) .collect(); @@ -1133,8 +1133,8 @@ mod tests { let validators: Vec<_> = (0..NUM_VALIDATORS) .map(|i| ethlambda_types::state::Validator { - attestation_pubkey: [i as u8; 52], - proposal_pubkey: [i as u8; 52], + attestation_pubkey: [i as u8; 32], + proposal_pubkey: [i as u8; 32], index: i as u64, }) .collect(); @@ -1262,8 +1262,8 @@ mod tests { let validators: Vec<_> = (0..NUM_VALIDATORS) .map(|i| ethlambda_types::state::Validator { - attestation_pubkey: [i as u8; 52], - proposal_pubkey: [i as u8; 52], + attestation_pubkey: [i as u8; 32], + proposal_pubkey: [i as u8; 32], index: i as u64, }) .collect(); @@ -1566,8 +1566,8 @@ mod tests { let validators: Vec<_> = (0..NUM_VALIDATORS) .map(|i| ethlambda_types::state::Validator { - attestation_pubkey: [i as u8; 52], - proposal_pubkey: [i as u8; 52], + attestation_pubkey: [i as u8; 32], + proposal_pubkey: [i as u8; 32], index: i as u64, }) .collect(); @@ -1693,8 +1693,8 @@ mod tests { let validators: Vec<_> = (0..NUM_VALIDATORS) .map(|i| ethlambda_types::state::Validator { - attestation_pubkey: [i as u8; 52], - proposal_pubkey: [i as u8; 52], + attestation_pubkey: [i as u8; 32], + proposal_pubkey: [i as u8; 32], index: i as u64, }) .collect(); diff --git a/crates/blockchain/src/reaggregate.rs b/crates/blockchain/src/reaggregate.rs index f47d6c51..92ff3108 100644 --- a/crates/blockchain/src/reaggregate.rs +++ b/crates/blockchain/src/reaggregate.rs @@ -122,11 +122,8 @@ pub fn reaggregate_from_block( // Step 1: SNARK-split this attestation's component out of the block's // merged multi-message aggregate proof. let merged_bytes = signed_block.proof.proof_bytes(); - let split_bytes = match ethlambda_crypto::split_type_2_by_message( - merged_bytes, - pubkeys_per_component.clone(), - &data_root, - ) { + let split_bytes = match ethlambda_crypto::split_type_2_by_message(merged_bytes, &data_root) + { Ok(bytes) => bytes, Err(err) => { debug!(%err, data_root = %ethlambda_types::ShortRoot(&data_root.0), diff --git a/crates/blockchain/state_transition/src/lib.rs b/crates/blockchain/state_transition/src/lib.rs index 403b170a..69448a7c 100644 --- a/crates/blockchain/state_transition/src/lib.rs +++ b/crates/blockchain/state_transition/src/lib.rs @@ -623,8 +623,8 @@ mod tests { fn make_validators(n: usize) -> Vec { (0..n) .map(|i| Validator { - attestation_pubkey: [i as u8; 52], - proposal_pubkey: [i as u8; 52], + attestation_pubkey: [i as u8; 32], + proposal_pubkey: [i as u8; 32], index: i as u64, }) .collect() diff --git a/crates/common/crypto/Cargo.toml b/crates/common/crypto/Cargo.toml index 5ec86eca..83291605 100644 --- a/crates/common/crypto/Cargo.toml +++ b/crates/common/crypto/Cargo.toml @@ -12,12 +12,11 @@ version.workspace = true [dependencies] ethlambda-types.workspace = true +# rec_aggregation is leanVM's recursive XMSS aggregation prover/verifier; +# xmss provides the public-key/signature types its API consumes. +rec_aggregation.workspace = true +xmss.workspace = true -lean-multisig = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df" } -# leansig_wrapper provides XmssPublicKey/XmssSignature types used by lean-multisig's public API -leansig_wrapper = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df" } - -leansig.workspace = true thiserror.workspace = true rand.workspace = true @@ -26,3 +25,4 @@ shadow-integration = [] [dev-dependencies] hex.workspace = true +ssz.workspace = true diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 4e081445..ca1dfa61 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -1,37 +1,37 @@ -use std::sync::Once; - use ethlambda_types::{ block::ByteList512KiB, primitives::H256, - signature::{ValidatorPublicKey, ValidatorSignature}, + signature::{LeanSigPublicKey, LeanSigSignature, ValidatorPublicKey, ValidatorSignature}, }; -use lean_multisig::{ - MultiMessageAggregateSignature as LMType2, ProofError, - SingleMessageAggregateSignature as LMType1, aggregate_single_message_signatures, - merge_single_message_aggregates, setup_prover, setup_verifier, split_multi_message_aggregate, - verify_multi_message_aggregate, verify_single_message_aggregate, +use rec_aggregation::{ + MultiMessageAggregateSignature as LMType2, SingleMessageAggregateSignature as LMType1, + aggregate_single_message_signatures, init_aggregation_bytecode, + merge_single_message_aggregates, split_multi_message_aggregate, verify_multi_message_aggregate, + verify_single_message_aggregate, }; -use leansig_wrapper::{XmssPublicKey as LeanSigPubKey, XmssSignature as LeanSigSignature}; use thiserror::Error; #[cfg(feature = "shadow-integration")] pub mod shadow_cost; -/// log(1/rate) for the WHIR commitment scheme used inside lean-multisig. +/// log(1/rate) for the WHIR commitment scheme used inside the aggregation prover. const LOG_INV_RATE: usize = 2; -// Lazy initialization for prover and verifier setup -static PROVER_INIT: Once = Once::new(); -static VERIFIER_INIT: Once = Once::new(); - -/// Ensure the prover is initialized. Safe to call multiple times. +/// Ensure the aggregation bytecode is compiled. Safe to call multiple times. +/// +/// leanVM's `main` replaced the old `setup_prover`/`setup_verifier` split with a +/// single self-referential aggregation bytecode used by both the prover and the +/// verifier; `init_aggregation_bytecode` is idempotent (`OnceLock::get_or_init`). pub fn ensure_prover_ready() { - PROVER_INIT.call_once(setup_prover); + init_aggregation_bytecode(); } -/// Ensure the verifier is initialized. Safe to call multiple times. +/// Ensure the aggregation bytecode is compiled. Safe to call multiple times. +/// +/// Deserializing any proof (`from_bytes`) also requires the bytecode, so this +/// must run before any verification path. pub fn ensure_verifier_ready() { - VERIFIER_INIT.call_once(setup_verifier); + init_aggregation_bytecode(); } /// Error type for signature aggregation operations. @@ -81,7 +81,10 @@ pub enum VerificationError { DeserializationFailed, #[error("verification failed: {0}")] - ProofError(#[from] ProofError), + VerificationFailed(String), + + #[error("aggregate binds a different validator set than expected at component {0}")] + PublicKeySetMismatch(usize), #[error( "(message, slot) mismatch: proof binds {got_slot}/{got_msg:?}, expected {expected_slot}/{expected_msg:?}" @@ -107,33 +110,45 @@ pub enum VerificationError { // Helpers // ===================================================================== -fn into_lean_pubkeys(pubkeys: Vec) -> Vec { - pubkeys +/// The sorted, de-duplicated set of native pubkeys, matching the canonical form +/// leanVM's aggregation embeds inside a proof's `info.pubkeys`. +fn sorted_dedup_pubkeys(pubkeys: Vec) -> Vec { + let mut keys: Vec = pubkeys .into_iter() .map(ValidatorPublicKey::into_inner) - .collect() + .collect(); + keys.sort(); + keys.dedup(); + keys } -/// Decompress a stored Type-1 proof (without-pubkeys form) into a native -/// `SingleMessageAggregateSignature` by attaching the resolved validator pubkeys. +/// Deserialize a stored Type-1 proof. +/// +/// leanVM's `main` embeds the participant pubkeys inside the serialized proof +/// (`info.pubkeys`), so decoding no longer needs the caller-supplied pubkeys. +/// They are still cross-checked against the proof's embedded set as an +/// integrity guard. fn decompress_type1( pubkeys: Vec, proof_bytes: &ByteList512KiB, index: usize, ) -> Result { - let lean_pks = into_lean_pubkeys(pubkeys); - LMType1::decompress_without_pubkeys(proof_bytes.iter().as_slice(), lean_pks) - .ok_or(AggregationError::ChildDeserializationFailed(index)) + let sig = LMType1::from_bytes(proof_bytes.iter().as_slice()) + .ok_or(AggregationError::ChildDeserializationFailed(index))?; + if sig.info.pubkeys != sorted_dedup_pubkeys(pubkeys) { + return Err(AggregationError::ChildDeserializationFailed(index)); + } + Ok(sig) } fn compress_type1_to_byte_list(sig: &LMType1) -> Result { - let serialized = sig.compress_without_pubkeys(); + let serialized = sig.to_bytes(); let len = serialized.len(); ByteList512KiB::try_from(serialized).map_err(|_| AggregationError::ProofTooBig(len)) } fn compress_type2_to_byte_list(sig: &LMType2) -> Result { - let serialized = sig.compress_without_pubkeys(); + let serialized = sig.to_bytes(); let len = serialized.len(); ByteList512KiB::try_from(serialized).map_err(|_| AggregationError::ProofTooBig(len)) } @@ -144,12 +159,14 @@ fn compress_type2_to_byte_list(sig: &LMType2) -> Result, signatures: Vec, @@ -181,7 +198,7 @@ pub fn aggregate_signatures( ensure_prover_ready(); - let raw_xmss: Vec<(LeanSigPubKey, LeanSigSignature)> = public_keys + let raw_xmss: Vec<(LeanSigPublicKey, LeanSigSignature)> = public_keys .into_iter() .zip(signatures) .map(|(pk, sig)| (pk.into_inner(), sig.into_inner())) @@ -242,7 +259,7 @@ pub fn aggregate_mixed( .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) .collect::>()?; - let raw_xmss: Vec<(LeanSigPubKey, LeanSigSignature)> = raw_public_keys + let raw_xmss: Vec<(LeanSigPublicKey, LeanSigSignature)> = raw_public_keys .into_iter() .zip(raw_signatures) .map(|(pk, sig)| (pk.into_inner(), sig.into_inner())) @@ -331,20 +348,25 @@ pub fn verify_aggregated_signature( } ensure_verifier_ready(); - let lean_pubkeys = into_lean_pubkeys(public_keys); - let sig = LMType1::decompress_without_pubkeys(proof_data.iter().as_slice(), lean_pubkeys) + let sig = LMType1::from_bytes(proof_data.iter().as_slice()) .ok_or(VerificationError::DeserializationFailed)?; - if sig.info.without_pubkeys.message != message.0 || sig.info.without_pubkeys.slot != slot { + if sig.info.message != message.0 || sig.info.slot != slot { return Err(VerificationError::BindingMismatch { expected_msg: *message, expected_slot: slot, - got_msg: H256(sig.info.without_pubkeys.message), - got_slot: sig.info.without_pubkeys.slot, + got_msg: H256(sig.info.message), + got_slot: sig.info.slot, }); } - verify_single_message_aggregate(&sig)?; + // The proof embeds its participant set; bind it to the caller's expectation. + if sig.info.pubkeys != sorted_dedup_pubkeys(public_keys) { + return Err(VerificationError::PublicKeySetMismatch(0)); + } + + verify_single_message_aggregate(&sig) + .map_err(|err| VerificationError::VerificationFailed(format!("{err:?}")))?; Ok(()) } @@ -355,11 +377,11 @@ pub fn verify_aggregated_signature( /// Merge many independent Type-1 multi-signatures into a single Type-2 proof. /// /// Each input is `(participant_pubkeys, type_1_proof_bytes)` where the bytes -/// are the `compress_without_pubkeys()` form of a `SingleMessageAggregateSignature`. +/// are the `to_bytes()` form of a `SingleMessageAggregateSignature`. The pubkeys +/// are also embedded in the proof; the caller-supplied set is cross-checked. /// -/// The returned blob is the `compress_without_pubkeys()` form of the resulting -/// `MultiMessageAggregateSignature`. A verifier decoding it back needs the per-component -/// pubkey sets in the same order. +/// The returned blob is the `to_bytes()` form of the resulting +/// `MultiMessageAggregateSignature`, with each component's pubkeys embedded. pub fn merge_type_1s_into_type_2( type_1s: Vec<(Vec, ByteList512KiB)>, ) -> Result { @@ -421,13 +443,12 @@ pub fn verify_type_2_signature( ensure_verifier_ready(); - let pubkeys_per_info: Vec> = pubkeys_per_component + let expected_pubkeys_per_info: Vec> = pubkeys_per_component .into_iter() - .map(into_lean_pubkeys) + .map(sorted_dedup_pubkeys) .collect(); - let sig = LMType2::decompress_without_pubkeys(proof_data, pubkeys_per_info) - .ok_or(VerificationError::DeserializationFailed)?; + let sig = LMType2::from_bytes(proof_data).ok_or(VerificationError::DeserializationFailed)?; if sig.info.len() != expected_bindings.len() { return Err(VerificationError::Type2ComponentCountMismatch { @@ -439,34 +460,35 @@ pub fn verify_type_2_signature( for (idx, ((expected_msg, expected_slot), info)) in expected_bindings.iter().zip(sig.info.iter()).enumerate() { - if info.without_pubkeys.message != expected_msg.0 - || info.without_pubkeys.slot != *expected_slot - { + if info.message != expected_msg.0 || info.slot != *expected_slot { return Err(VerificationError::BindingMismatch { expected_msg: *expected_msg, expected_slot: *expected_slot, - got_msg: H256(info.without_pubkeys.message), - got_slot: info.without_pubkeys.slot, + got_msg: H256(info.message), + got_slot: info.slot, }); } - let _ = idx; // index reserved for richer diagnostics if needed + // The proof embeds each component's participant set; bind it to the + // caller's expectation. + if info.pubkeys != expected_pubkeys_per_info[idx] { + return Err(VerificationError::PublicKeySetMismatch(idx)); + } } - verify_multi_message_aggregate(&sig)?; + verify_multi_message_aggregate(&sig) + .map_err(|err| VerificationError::VerificationFailed(format!("{err:?}")))?; Ok(()) } /// Split (disaggregate) a Type-2 merged proof into a single Type-1 proof for /// the component bound to `message`. Generates a fresh SNARK; expensive. /// -/// Mirrors leanSpec PR #717 `split_multi_message_aggregate_by_message`: the caller -/// supplies the expected message (an attestation data root or the block -/// root) and the wrapper locates the unique matching component inside the -/// decompressed proof. Returns the `compress_without_pubkeys()` form of the -/// resulting Type-1. +/// Mirrors `split_multi_message_aggregate_by_message`: the caller supplies the +/// expected message (an attestation data root or the block root) and the +/// wrapper locates the unique matching component inside the decoded proof. +/// Returns the `to_bytes()` form of the resulting Type-1. pub fn split_type_2_by_message( proof_data: &[u8], - pubkeys_per_component: Vec>, message: &H256, ) -> Result { #[cfg(feature = "shadow-integration")] @@ -479,19 +501,15 @@ pub fn split_type_2_by_message( ensure_prover_ready(); - let pubkeys_per_info: Vec> = pubkeys_per_component - .into_iter() - .map(into_lean_pubkeys) - .collect(); - - let type_2 = LMType2::decompress_without_pubkeys(proof_data, pubkeys_per_info) - .ok_or(AggregationError::DeserializationFailed)?; + // leanVM embeds each component's pubkeys in the serialized proof, so decoding + // no longer needs caller-supplied pubkey sets. + let type_2 = LMType2::from_bytes(proof_data).ok_or(AggregationError::DeserializationFailed)?; let matches: Vec = type_2 .info .iter() .enumerate() - .filter_map(|(i, info)| (info.without_pubkeys.message == message.0).then_some(i)) + .filter_map(|(i, info)| (info.message == message.0).then_some(i)) .collect(); let index = match matches.as_slice() { [i] => *i, @@ -508,43 +526,43 @@ pub fn split_type_2_by_message( #[cfg(test)] mod tests { use super::*; - use ethlambda_types::signature::LeanSignatureScheme; - use leansig::{serialization::Serializable, signature::SignatureScheme}; - use rand::{SeedableRng, rngs::StdRng}; + use ssz::Encode as _; + use xmss::{xmss_key_gen_from_seed, xmss_sign}; /// Generate a test keypair and sign a message. /// /// Note: This is slow because XMSS key generation is computationally expensive. - /// TODO: move to pre-generated keys fn generate_keypair_and_sign( seed: u64, activation_epoch: u32, signing_epoch: u32, message: &H256, ) -> (ValidatorPublicKey, ValidatorSignature) { - let mut rng = StdRng::seed_from_u64(seed); + let mut seed_bytes = [0u8; 32]; + seed_bytes[..8].copy_from_slice(&seed.to_le_bytes()); - // Use a small lifetime for faster test key generation - let log_lifetime = 5; // 2^5 = 32 epochs - let lifetime = 1 << log_lifetime; + // Small active range (starting at the activation epoch and covering the + // signing slot) for fast key generation. + let num_active_slots = 64u64; + let (pk, sk) = + xmss_key_gen_from_seed(seed_bytes, activation_epoch as u64, num_active_slots) + .expect("valid activation range"); - let (pk, sk) = LeanSignatureScheme::key_gen(&mut rng, activation_epoch as usize, lifetime); + let sig = xmss_sign(&sk, signing_epoch, &message.0).expect("sign"); - let sig = LeanSignatureScheme::sign(&sk, signing_epoch, &message.0).unwrap(); - - // Convert to ethlambda types via bytes - let pk_bytes = pk.to_bytes(); - let sig_bytes = sig.to_bytes(); - - let validator_pk = ValidatorPublicKey::from_bytes(&pk_bytes).unwrap(); - let validator_sig = ValidatorSignature::from_bytes(&sig_bytes).unwrap(); + // Convert to ethlambda types via SSZ wire bytes. + let validator_pk = ValidatorPublicKey::from_bytes(&pk.as_ssz_bytes()).unwrap(); + let validator_sig = ValidatorSignature::from_bytes(&sig.as_ssz_bytes()).unwrap(); (validator_pk, validator_sig) } #[test] + #[ignore = "slow: compiles the leanVM aggregation bytecode (needs a release-sized stack)"] fn test_setup_is_idempotent() { - // Should not panic when called multiple times + // Should not panic when called multiple times. The first call compiles + // the self-referential aggregation bytecode; subsequent calls are cheap + // (`OnceLock::get_or_init`). ensure_prover_ready(); ensure_prover_ready(); ensure_verifier_ready(); @@ -681,12 +699,7 @@ mod tests { ) .expect("verify type-2"); - let split = split_type_2_by_message( - merged.iter().as_slice(), - vec![vec![pk_a.clone()], vec![pk_b.clone()]], - &msg_a, - ) - .expect("split"); + let split = split_type_2_by_message(merged.iter().as_slice(), &msg_a).expect("split"); verify_aggregated_signature(&split, vec![pk_a.clone()], &msg_a, slot_a) .expect("verify split"); diff --git a/crates/common/test-fixtures/src/common.rs b/crates/common/test-fixtures/src/common.rs index 6293b897..5bad0bab 100644 --- a/crates/common/test-fixtures/src/common.rs +++ b/crates/common/test-fixtures/src/common.rs @@ -330,7 +330,12 @@ where let pubkey: ValidatorPubkeyBytes = hex::decode(value.strip_prefix("0x").unwrap_or(&value)) .map_err(|_| D::Error::custom("ValidatorPubkey value is not valid hex"))? .try_into() - .map_err(|_| D::Error::custom("ValidatorPubkey length != 52"))?; + .map_err(|_| { + D::Error::custom(format!( + "ValidatorPubkey length != {}", + ethlambda_types::signature::PUBLIC_KEY_SIZE + )) + })?; Ok(pubkey) } diff --git a/crates/common/types/Cargo.toml b/crates/common/types/Cargo.toml index 1de09a67..e7a733d6 100644 --- a/crates/common/types/Cargo.toml +++ b/crates/common/types/Cargo.toml @@ -14,7 +14,9 @@ thiserror.workspace = true serde.workspace = true hex.workspace = true -leansig.workspace = true +xmss.workspace = true +ssz.workspace = true +postcard.workspace = true libssz.workspace = true libssz-derive.workspace = true diff --git a/crates/common/types/src/attestation.rs b/crates/common/types/src/attestation.rs index 0d68addb..f8706697 100644 --- a/crates/common/types/src/attestation.rs +++ b/crates/common/types/src/attestation.rs @@ -58,34 +58,15 @@ pub struct SignedAttestation { /// XMSS signature as a fixed-length byte vector (`SIGNATURE_SIZE` bytes). pub type XmssSignature = SszVector; -/// SSZ offset (in bytes) of the `path` body inside an XMSS `Signature` container. -/// -/// Layout: 4-byte path offset + 28-byte rho + 4-byte hashes offset = 36. -const SIGNATURE_PATH_OFFSET: u32 = 36; - -/// SSZ offset (in bytes) of the `hashes` body inside an XMSS `Signature`. -/// -/// `path` body is 4-byte siblings offset + LOG_LIFETIME (32) siblings × 32-byte -/// digest = 1028, starting at byte 36, so hashes start at 36 + 1028 = 1064. -const SIGNATURE_HASHES_OFFSET: u32 = 1064; - -/// SSZ offset (in bytes) of the `siblings` list inside the `path` container. -const SIGNATURE_PATH_SIBLINGS_OFFSET: u32 = 4; - -/// Build a placeholder XMSS signature that decodes as a structurally valid -/// leanSpec `Signature` container of all-zero hashes. +/// Build a placeholder XMSS signature of all-zero field elements. /// /// Used for genesis-style anchor blocks that were never proposed and therefore -/// have no real signature. Parent containers inline this as an opaque -/// `SIGNATURE_SIZE`-byte blob; consumers that decode the inner `Signature` -/// container see `path = HashTreeOpening { siblings = [0; 32] }`, `rho = 0`, -/// `hashes = [0; 46]`. Matches ream's `Signature::blank()` so the wire format -/// is byte-identical across clients. +/// have no real signature. leanVM's `XmssSignature` SSZ-encodes as a +/// fixed-length sequence of field elements (no variable-length offsets), and a +/// zero word is a valid canonical field element, so an all-zero blob decodes as +/// a structurally valid (but unverifiable) signature. pub fn blank_xmss_signature() -> XmssSignature { - let mut bytes = vec![0u8; SIGNATURE_SIZE]; - bytes[..4].copy_from_slice(&SIGNATURE_PATH_OFFSET.to_le_bytes()); - bytes[32..36].copy_from_slice(&SIGNATURE_HASHES_OFFSET.to_le_bytes()); - bytes[36..40].copy_from_slice(&SIGNATURE_PATH_SIBLINGS_OFFSET.to_le_bytes()); + let bytes = vec![0u8; SIGNATURE_SIZE]; XmssSignature::try_from(bytes).expect("size matches SIGNATURE_SIZE") } @@ -311,35 +292,14 @@ mod tests { assert!(!bits_is_subset(&a, &b)); } - /// Guard the three SSZ offsets at fixed byte positions so a one-off in any - /// constant doesn't silently produce a blob that still has the right outer - /// length but decodes incorrectly at the inner `Signature` container level. + /// The blank placeholder is exactly `SIGNATURE_SIZE` all-zero bytes and + /// decodes back as a structurally valid signature. #[test] - fn blank_xmss_signature_has_expected_ssz_offsets() { + fn blank_xmss_signature_is_zero_and_decodes() { let sig = blank_xmss_signature(); let bytes: Vec = sig.into_iter().collect(); assert_eq!(bytes.len(), SIGNATURE_SIZE); - assert_eq!( - u32::from_le_bytes(bytes[0..4].try_into().unwrap()), - SIGNATURE_PATH_OFFSET, - ); - assert_eq!( - u32::from_le_bytes(bytes[32..36].try_into().unwrap()), - SIGNATURE_HASHES_OFFSET, - ); - assert_eq!( - u32::from_le_bytes(bytes[36..40].try_into().unwrap()), - SIGNATURE_PATH_SIBLINGS_OFFSET, - ); - - // Everything outside the three offset slots must be zero — the - // placeholder is "all-zero hashes" once the offsets locate them. - for (i, b) in bytes.iter().enumerate() { - let in_offset_slot = matches!(i, 0..4 | 32..36 | 36..40); - if !in_offset_slot { - assert_eq!(*b, 0, "non-offset byte at index {i} should be zero"); - } - } + assert!(bytes.iter().all(|b| *b == 0)); } } diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index 5c5508a2..c2d69174 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -48,12 +48,13 @@ pub type ByteList512KiB = ByteList<524_288>; /// A merged proof covering multiple messages with a single proof blob. /// -/// Also known as a *type-2* proof: the lean-multisig term for an aggregate -/// binding several distinct messages, each with its own participant set. +/// Also known as a *type-2* proof: leanVM's term for an aggregate binding +/// several distinct messages, each with its own participant set. /// -/// The proof bytes use lean-multisig's compact public-key-free -/// representation. SSZ encoding this container adds the offset required for -/// its variable-length field. +/// The proof bytes are the `to_bytes()` (postcard) form of leanVM's +/// `MultiMessageAggregateSignature`, which embeds each component's participant +/// pubkeys. SSZ encoding this container adds the offset required for its +/// variable-length field. #[derive(Debug, Default, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] pub struct MultiMessageAggregate { /// Serialized multi-message aggregate proof bytes. @@ -66,7 +67,7 @@ impl MultiMessageAggregate { Self { proof } } - /// Copy raw lean-multisig proof bytes into the bounded SSZ container. + /// Copy raw leanVM aggregation proof bytes into the bounded SSZ container. pub fn from_bytes(bytes: &[u8]) -> Result { let len = bytes.len(); ByteList512KiB::try_from(bytes.to_vec()) @@ -74,7 +75,7 @@ impl MultiMessageAggregate { .map_err(|_| MultiMessageAggregateError::ProofTooLarge(len)) } - /// Return the raw lean-multisig proof bytes. + /// Return the raw leanVM aggregation proof bytes. pub fn proof_bytes(&self) -> &[u8] { self.proof.iter().as_slice() } @@ -98,7 +99,7 @@ pub enum MultiMessageAggregateError { // from the surrounding block body (attestation `data` + slot for body // components, block root + slot for the proposer component). // -// `MultiMessageAggregate` carries the raw lean-multisig type-2 bytes. +// `MultiMessageAggregate` carries the raw leanVM type-2 bytes. // Component participant bitfields come from // `block.body.attestations[i].aggregation_bits` (and `block.proposer_index` for // the trailing proposer entry). @@ -110,8 +111,8 @@ pub const MAX_ATTESTATIONS_DATA: usize = 8; /// A single-message proof aggregating signatures from many validators. /// -/// Also known as a *type-1* proof: the lean-multisig term for an aggregate -/// where every participant signs the same message for the same slot. +/// Also known as a *type-1* proof: leanVM's term for an aggregate where every +/// participant signs the same message for the same slot. /// /// Used: /// - as a gossip-level `SignedAggregatedAttestation.proof`, @@ -119,15 +120,16 @@ pub const MAX_ATTESTATIONS_DATA: usize = 8; /// - as one of the components fed into `merge_type_1s_into_type_2` when /// building a block proof. /// -/// `participants` and `proof` are independent fields: the proof bytes are -/// the lean-multisig `compress_without_pubkeys()` form; `participants` is -/// the bitfield identifying which validators are bound by the proof. The -/// verifier resolves pubkeys from `participants` at verify time. +/// `participants` and `proof` are independent fields: the proof bytes are the +/// `to_bytes()` form of leanVM's `SingleMessageAggregateSignature` (which +/// embeds the participant pubkeys); `participants` is the bitfield identifying +/// which validators are bound. The verifier resolves the expected pubkeys from +/// `participants` and cross-checks them against the set embedded in the proof. #[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)] pub struct SingleMessageAggregate { /// Bitfield identifying validators bound by this proof. pub participants: AggregationBits, - /// Aggregated proof bytes in lean-multisig compact (no-pubkeys) form. + /// Aggregated proof bytes in leanVM's `to_bytes()` form (pubkeys embedded). pub proof: ByteList512KiB, } diff --git a/crates/common/types/src/genesis.rs b/crates/common/types/src/genesis.rs index 27baebf6..02b2bbab 100644 --- a/crates/common/types/src/genesis.rs +++ b/crates/common/types/src/genesis.rs @@ -44,7 +44,11 @@ where let bytes = hex::decode(s).map_err(|_| D::Error::custom(format!("pubkey is not valid hex: {s}")))?; bytes.try_into().map_err(|v: Vec| { - D::Error::custom(format!("pubkey has length {} (expected 52)", v.len())) + D::Error::custom(format!( + "pubkey has length {} (expected {})", + v.len(), + crate::signature::PUBLIC_KEY_SIZE + )) }) } @@ -56,12 +60,12 @@ mod tests { state::{State, Validator}, }; - const ATT_PUBKEY_A: &str = "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d2d25ca7b19604f5a180037046cf2a02e1da4a800"; - const PROP_PUBKEY_A: &str = "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940fa03e4c012e8a0e656b731934aeac124a5337e333"; - const ATT_PUBKEY_B: &str = "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05e143e305017847445859650d6dd06e6efb3f8410"; - const PROP_PUBKEY_B: &str = "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d2d25ca7b19604f5a180037046cf2a02e1da4a800"; - const ATT_PUBKEY_C: &str = "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940fa03e4c012e8a0e656b731934aeac124a5337e333"; - const PROP_PUBKEY_C: &str = "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05e143e305017847445859650d6dd06e6efb3f8410"; + const ATT_PUBKEY_A: &str = "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d"; + const PROP_PUBKEY_A: &str = "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940f"; + const ATT_PUBKEY_B: &str = "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05"; + const PROP_PUBKEY_B: &str = "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d"; + const ATT_PUBKEY_C: &str = "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940f"; + const PROP_PUBKEY_C: &str = "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05"; const TEST_CONFIG_YAML: &str = r#"# Genesis Settings GENESIS_TIME: 1770407233 @@ -74,12 +78,12 @@ VALIDATOR_COUNT: 3 # Genesis Validator Pubkeys GENESIS_VALIDATORS: - - attestation_pubkey: "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d2d25ca7b19604f5a180037046cf2a02e1da4a800" - proposal_pubkey: "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940fa03e4c012e8a0e656b731934aeac124a5337e333" - - attestation_pubkey: "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05e143e305017847445859650d6dd06e6efb3f8410" - proposal_pubkey: "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d2d25ca7b19604f5a180037046cf2a02e1da4a800" - - attestation_pubkey: "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940fa03e4c012e8a0e656b731934aeac124a5337e333" - proposal_pubkey: "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05e143e305017847445859650d6dd06e6efb3f8410" + - attestation_pubkey: "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d" + proposal_pubkey: "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940f" + - attestation_pubkey: "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05" + proposal_pubkey: "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d" + - attestation_pubkey: "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940f" + proposal_pubkey: "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05" "#; #[test] @@ -146,7 +150,7 @@ GENESIS_VALIDATORS: // Pin the state root so SSZ layout changes are caught immediately. let expected_state_root = crate::primitives::H256::from_slice( - &hex::decode("babcdc9235a29dfc0d605961df51cfc85732f85291c2beea8b7510a92ec458fe") + &hex::decode("3e8c8507e94e045327c2fc66a58db374805cb490a087b3101bb13a9b8b611b54") .unwrap(), ); assert_eq!(root, expected_state_root, "state root mismatch"); @@ -155,7 +159,7 @@ GENESIS_VALIDATORS: block.state_root = root; let block_root = block.hash_tree_root(); let expected_block_root = crate::primitives::H256::from_slice( - &hex::decode("66a8beaa81d2aaeac7212d4bf8f5fea2bd22d479566a33a83c891661c21235ef") + &hex::decode("ba3502921697db025b3a6d7c05fbaf58e52155575438cca9794e22e6e9872090") .unwrap(), ); assert_eq!(block_root, expected_block_root, "block root mismatch"); diff --git a/crates/common/types/src/signature.rs b/crates/common/types/src/signature.rs index fa9c8da8..766a756d 100644 --- a/crates/common/types/src/signature.rs +++ b/crates/common/types/src/signature.rs @@ -1,34 +1,32 @@ use std::ops::Range; -use leansig::{ - serialization::Serializable, - signature::{SignatureScheme, SignatureSchemeSecretKey as _, SigningError}, +use ssz::{Decode, Encode}; +use xmss::{ + PUB_KEY_SSZ_LEN, SIGNATURE_SSZ_LEN, XmssPublicKey, XmssSecretKey, XmssSignature, + XmssSignatureError, xmss_sign, xmss_verify, }; use crate::primitives::H256; -/// The XMSS signature scheme used for validator signatures. -/// -/// This is a post-quantum secure signature scheme based on hash functions. -/// Uses Poseidon1 hashing with an aborting hypercube message hash, -/// 32-bit lifetime (2^32 signatures per key), dimension 46, and base 8. -pub type LeanSignatureScheme = leansig::signature::generalized_xmss::instantiations_aborting::lifetime_2_to_the_32::SIGAbortingTargetSumLifetime32Dim46Base8; - -/// The public key type from the leansig library. -pub type LeanSigPublicKey = ::PublicKey; +/// The public key type from leanVM's xmss crate. +pub type LeanSigPublicKey = XmssPublicKey; -/// The signature type from the leansig library. -pub type LeanSigSignature = ::Signature; +/// The signature type from leanVM's xmss crate. +pub type LeanSigSignature = XmssSignature; -/// The secret key type from the leansig library. -pub type LeanSigSecretKey = ::SecretKey; +/// The secret key type from leanVM's xmss crate. +pub type LeanSigSecretKey = XmssSecretKey; pub type Signature = LeanSigSignature; -/// Size of an XMSS signature in bytes. +/// Size of an SSZ-encoded XMSS signature in bytes. /// -/// Computed from: path(32*8*4) + rho(7*4) + hashes(46*8*4) + ssz_offsets(3*4) = 2536 -pub const SIGNATURE_SIZE: usize = 2536; +/// Sourced from leanVM's xmss crate rather than hardcoded, so it tracks the +/// scheme parameters (`WOTS_SIG_SIZE_FE`, `LOG_LIFETIME`, `XMSS_DIGEST_LEN`). +pub const SIGNATURE_SIZE: usize = SIGNATURE_SSZ_LEN; + +/// Size of an SSZ-encoded XMSS public key in bytes. +pub const PUBLIC_KEY_SIZE: usize = PUB_KEY_SSZ_LEN; /// Error returned when parsing signature or key bytes fails. #[derive(Debug, Clone, thiserror::Error)] @@ -41,18 +39,20 @@ pub struct ValidatorSignature { } impl ValidatorSignature { + /// Parse from the SSZ-encoded wire form (`SIGNATURE_SIZE` bytes). pub fn from_bytes(bytes: &[u8]) -> Result { - let sig = LeanSigSignature::from_bytes(bytes) + let sig = LeanSigSignature::from_ssz_bytes(bytes) .map_err(|e| SignatureParseError(format!("{e:?}")))?; Ok(Self { inner: sig }) } + /// Encode to the SSZ wire form (`SIGNATURE_SIZE` bytes). pub fn to_bytes(&self) -> Vec { - self.inner.to_bytes() + self.inner.as_ssz_bytes() } pub fn is_valid(&self, pubkey: &ValidatorPublicKey, slot: u32, message: &H256) -> bool { - LeanSignatureScheme::verify(&pubkey.inner, slot, &message.0, &self.inner) + xmss_verify(&pubkey.inner, slot, &message.0, &self.inner).is_ok() } pub fn into_inner(self) -> LeanSigSignature { @@ -66,14 +66,16 @@ pub struct ValidatorPublicKey { } impl ValidatorPublicKey { + /// Parse from the SSZ-encoded wire form (`PUBLIC_KEY_SIZE` bytes). pub fn from_bytes(bytes: &[u8]) -> Result { - let pk = LeanSigPublicKey::from_bytes(bytes) + let pk = LeanSigPublicKey::from_ssz_bytes(bytes) .map_err(|e| SignatureParseError(format!("{e:?}")))?; Ok(Self { inner: pk }) } + /// Encode to the SSZ wire form (`PUBLIC_KEY_SIZE` bytes). pub fn to_bytes(&self) -> Vec { - self.inner.to_bytes() + self.inner.as_ssz_bytes() } pub fn into_inner(self) -> LeanSigPublicKey { @@ -87,100 +89,116 @@ pub struct ValidatorSecretKey { } impl ValidatorSecretKey { + /// Parse from the postcard-encoded key file produced by the genesis generator. + /// + /// leanVM's `XmssSecretKey` uses serde (postcard) rather than the SSZ-style + /// `Serializable` of the old leanSig scheme. pub fn from_bytes(bytes: &[u8]) -> Result { - let sk = LeanSigSecretKey::from_bytes(bytes) + let sk = postcard::from_bytes::(bytes) .map_err(|e| SignatureParseError(format!("{e:?}")))?; Ok(Self { inner: sk }) } - /// Sign a message with this private key. + /// Serialize the secret key to its postcard key-file form. + pub fn to_bytes(&self) -> Result, SignatureParseError> { + postcard::to_allocvec(&self.inner).map_err(|e| SignatureParseError(format!("{e:?}"))) + } + + /// The public key derived from this secret key. + pub fn public_key(&self) -> ValidatorPublicKey { + ValidatorPublicKey { + inner: self.inner.public_key(), + } + } + + /// Sign a message at `slot`. /// - /// The slot is used as part of the XMSS signature scheme to track - /// one-time signature usage. - pub fn sign(&self, slot: u32, message: &H256) -> Result { - let sig = LeanSignatureScheme::sign(&self.inner, slot, &message.0)?; + /// The slot indexes the one-time XMSS leaf; never sign two different + /// messages at the same slot. + pub fn sign( + &self, + slot: u32, + message: &H256, + ) -> Result { + let sig = xmss_sign(&self.inner, slot, &message.0)?; Ok(ValidatorSignature { inner: sig }) } - /// Returns true if the key is prepared to sign at the given slot. + /// Returns true if the key can sign at the given slot. /// - /// XMSS keys maintain a sliding window of two bottom trees. Only slots - /// within this window can be signed without advancing the preparation. + /// leanVM's xmss keys cover a fixed activation range (fixed at key + /// generation); there is no sliding preparation window as in the old + /// leanSig scheme. pub fn is_prepared_for(&self, slot: u32) -> bool { - self.inner.get_prepared_interval().contains(&(slot as u64)) + self.inner.activation_slots().contains(&slot) } - /// Returns the slot range currently covered by the prepared window. + /// The half-open slot range this key can sign for. pub fn get_prepared_interval(&self) -> Range { - self.inner.get_prepared_interval() + let range = self.inner.activation_slots(); + (*range.start() as u64)..(*range.end() as u64 + 1) } - /// Advance the prepared window forward by one bottom tree. + /// No-op retained for API compatibility. /// - /// Each call slides the window by sqrt(LIFETIME) = 65,536 slots. - /// If the window is already at the end of the key's activation interval, - /// this is a no-op. - pub fn advance_preparation(&mut self) { - self.inner.advance_preparation(); - } + /// The old leanSig scheme advanced a two-bottom-tree preparation window; + /// leanVM's xmss keys have a fixed activation range and warm their signing + /// cache on demand inside `sign`, so there is nothing to advance. + pub fn advance_preparation(&mut self) {} } #[cfg(test)] mod tests { use super::*; - use leansig::serialization::Serializable; - use rand::{SeedableRng, rngs::StdRng}; - - const LEAVES_PER_BOTTOM_TREE: u32 = 1 << 16; // 65,536 - - /// Generate a ValidatorSecretKey with 3 bottom trees so advance_preparation can be tested. - /// - /// This is slow (~minutes) because it computes 3 bottom trees of 65,536 leaves each. - fn generate_key_with_three_bottom_trees() -> ValidatorSecretKey { - let mut rng = StdRng::seed_from_u64(42); - // Request enough active epochs for 3 bottom trees (> 2 * 65,536) - let num_active_epochs = (LEAVES_PER_BOTTOM_TREE as usize) * 2 + 1; - let (_pk, sk) = LeanSignatureScheme::key_gen(&mut rng, 0, num_active_epochs); - let sk_bytes = sk.to_bytes(); - ValidatorSecretKey::from_bytes(&sk_bytes).expect("valid secret key") + use xmss::xmss_key_gen_from_seed; + + /// Generate a validator key pair over a small activation range. + fn generate_key( + seed: [u8; 32], + activation_slot: u64, + num_active_slots: u64, + ) -> ValidatorSecretKey { + let (_pk, sk) = xmss_key_gen_from_seed(seed, activation_slot, num_active_slots) + .expect("valid activation range"); + ValidatorSecretKey { inner: sk } } #[test] - #[ignore = "slow: generates production-size XMSS key (~minutes)"] - fn test_advance_preparation_duration() { - println!("Generating XMSS key with 3 bottom trees (this takes a while)..."); - let keygen_start = std::time::Instant::now(); - let mut sk = generate_key_with_three_bottom_trees(); - println!("Key generation took: {:?}", keygen_start.elapsed()); - - // Initial window covers [0, 131072) - assert!(sk.is_prepared_for(0)); - assert!(sk.is_prepared_for(LEAVES_PER_BOTTOM_TREE - 1)); - assert!(sk.is_prepared_for(2 * LEAVES_PER_BOTTOM_TREE - 1)); - assert!(!sk.is_prepared_for(2 * LEAVES_PER_BOTTOM_TREE)); + #[ignore = "slow: XMSS key generation and signing"] + fn sign_verify_round_trip() { + let sk = generate_key([7u8; 32], 0, 64); + let pk = sk.public_key(); - // Time the advance_preparation call - let advance_start = std::time::Instant::now(); - sk.advance_preparation(); - let advance_duration = advance_start.elapsed(); + assert!(sk.is_prepared_for(0)); + assert!(sk.is_prepared_for(63)); + assert!(!sk.is_prepared_for(64)); + assert_eq!(sk.get_prepared_interval(), 0..64); - println!("advance_preparation() took: {advance_duration:?}"); + let message = H256::from([42u8; 32]); + let slot = 10u32; + let sig = sk.sign(slot, &message).expect("sign"); + assert!(sig.is_valid(&pk, slot, &message)); + assert!(!sig.is_valid(&pk, slot, &H256::from([43u8; 32]))); + assert!(!sig.is_valid(&pk, slot + 1, &message)); + } - // Window should now cover [65536, 196608) - assert!(!sk.is_prepared_for(0)); - assert!(sk.is_prepared_for(LEAVES_PER_BOTTOM_TREE)); - assert!(sk.is_prepared_for(3 * LEAVES_PER_BOTTOM_TREE - 1)); + #[test] + #[ignore = "slow: XMSS key generation and signing"] + fn sign_out_of_range_fails() { + let sk = generate_key([9u8; 32], 100, 32); + let message = H256::from([1u8; 32]); + // Slot 0 is outside the key's activation range [100, 132). + assert!(sk.sign(0, &message).is_err()); + } - // Verify signing works in the new window - let message = H256::from([42u8; 32]); - let slot = 2 * LEAVES_PER_BOTTOM_TREE; // slot 131,072 — the one that crashed the devnet - let sign_start = std::time::Instant::now(); - let result = sk.sign(slot, &message); - println!("Signing at slot {slot} took: {:?}", sign_start.elapsed()); - assert!( - result.is_ok(), - "signing should succeed after advance: {}", - result.err().map_or(String::new(), |e| e.to_string()) - ); + #[test] + #[ignore = "slow: XMSS key generation"] + fn public_key_ssz_round_trip() { + let sk = generate_key([3u8; 32], 0, 16); + let pk = sk.public_key(); + let bytes = pk.to_bytes(); + assert_eq!(bytes.len(), PUBLIC_KEY_SIZE); + let parsed = ValidatorPublicKey::from_bytes(&bytes).expect("round trip"); + assert_eq!(parsed.to_bytes(), bytes); } } diff --git a/crates/common/types/src/state.rs b/crates/common/types/src/state.rs index 26ff110d..58494922 100644 --- a/crates/common/types/src/state.rs +++ b/crates/common/types/src/state.rs @@ -6,7 +6,7 @@ use crate::{ block::{Block, BlockBody, BlockHeader}, checkpoint::Checkpoint, primitives::{self, H256}, - signature::{SignatureParseError, ValidatorPublicKey}, + signature::{PUBLIC_KEY_SIZE, SignatureParseError, ValidatorPublicKey}, }; // Convenience trait for calling hash_tree_root() without a hasher argument @@ -95,7 +95,7 @@ impl Validator { } } -pub type ValidatorPubkeyBytes = [u8; 52]; +pub type ValidatorPubkeyBytes = [u8; PUBLIC_KEY_SIZE]; impl State { pub fn from_genesis(genesis_time: u64, validators: Vec) -> Self { diff --git a/crates/net/rpc/src/genesis.rs b/crates/net/rpc/src/genesis.rs index aa7472c9..6ab0b59e 100644 --- a/crates/net/rpc/src/genesis.rs +++ b/crates/net/rpc/src/genesis.rs @@ -42,8 +42,8 @@ mod tests { async fn genesis_returns_time_and_validator_count() { // Build a state with 3 validators so the assertion is non-vacuous. let dummy_validator = |index: u64| Validator { - attestation_pubkey: [0u8; 52], - proposal_pubkey: [0u8; 52], + attestation_pubkey: [0u8; 32], + proposal_pubkey: [0u8; 32], index, }; let validators = vec![dummy_validator(0), dummy_validator(1), dummy_validator(2)]; diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index 7381a53e..2bc1968a 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -24,5 +24,4 @@ lru.workspace = true [dev-dependencies] tempfile = "3" -leansig.workspace = true rand.workspace = true diff --git a/crates/storage/src/state_diff.rs b/crates/storage/src/state_diff.rs index 1e9e5028..261619f8 100644 --- a/crates/storage/src/state_diff.rs +++ b/crates/storage/src/state_diff.rs @@ -248,13 +248,13 @@ mod tests { fn base_state() -> State { let validators = vec![ Validator { - attestation_pubkey: [1u8; 52], - proposal_pubkey: [2u8; 52], + attestation_pubkey: [1u8; 32], + proposal_pubkey: [2u8; 32], index: 0, }, Validator { - attestation_pubkey: [3u8; 52], - proposal_pubkey: [4u8; 52], + attestation_pubkey: [3u8; 32], + proposal_pubkey: [4u8; 32], index: 1, }, ]; diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index a320406f..c5eac04f 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -1798,8 +1798,8 @@ mod tests { /// post-state's `latest_block_header`). fn sample_state(slot: u64, parent_root: H256, hbh: Vec) -> State { let validators = vec![Validator { - attestation_pubkey: [7u8; 52], - proposal_pubkey: [9u8; 52], + attestation_pubkey: [7u8; 32], + proposal_pubkey: [9u8; 32], index: 0, }]; let mut state = State::from_genesis(1_000, validators); @@ -2484,19 +2484,12 @@ mod tests { // ============ GossipSignatureBuffer Tests ============ fn make_dummy_sig() -> ValidatorSignature { - use ethlambda_types::signature::LeanSignatureScheme; - use leansig::{serialization::Serializable, signature::SignatureScheme}; - use rand::{SeedableRng, rngs::StdRng}; - - static CACHED_SIG: std::sync::LazyLock> = std::sync::LazyLock::new(|| { - let mut rng = StdRng::seed_from_u64(42); - let lifetime = 1 << 5; // small for speed - let (_pk, sk) = LeanSignatureScheme::key_gen(&mut rng, 0, lifetime); - let sig = LeanSignatureScheme::sign(&sk, 0, &[0u8; 32]).unwrap(); - sig.to_bytes() - }); - - ValidatorSignature::from_bytes(&CACHED_SIG).expect("cached test signature") + // These tests never check signature validity, only that a signature + // decodes and carries through the buffers. An all-zero blob is a + // structurally valid (unverifiable) XMSS signature. + use ethlambda_types::signature::SIGNATURE_SIZE; + ValidatorSignature::from_bytes(&vec![0u8; SIGNATURE_SIZE]) + .expect("all-zero test signature decodes") } #[test] From 6b41823a281ddf5062c737c192064d18d5c75265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:09:18 -0300 Subject: [PATCH 02/14] build(deps): pin leanVM to main HEAD (a73ab11) instead of tracking the branch Tracking `branch = main` re-resolves on every fetch, so the build could change under us. Pin the current main HEAD for reproducibility; the comment notes it is main's tip and the rev can be bumped to move forward. --- Cargo.lock | 38 +++++++++++++++++++------------------- Cargo.toml | 8 +++++--- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9063761..a0e2fc67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ dependencies = [ [[package]] name = "air" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "field", "poly", @@ -661,7 +661,7 @@ dependencies = [ [[package]] name = "backend" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "air", "fiat-shamir", @@ -2564,7 +2564,7 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "fiat-shamir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "field", "koala-bear", @@ -2577,7 +2577,7 @@ dependencies = [ [[package]] name = "field" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "parallel", "paste", @@ -3647,7 +3647,7 @@ dependencies = [ [[package]] name = "koala-bear" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "field", "paste", @@ -3709,7 +3709,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lean_compiler" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "include_dir", @@ -3723,7 +3723,7 @@ dependencies = [ [[package]] name = "lean_prover" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "lean_compiler", @@ -3738,7 +3738,7 @@ dependencies = [ [[package]] name = "lean_vm" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "tracing", @@ -5290,7 +5290,7 @@ dependencies = [ [[package]] name = "parallel" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "system-info", ] @@ -5485,7 +5485,7 @@ dependencies = [ [[package]] name = "poly" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "field", "parallel", @@ -6068,7 +6068,7 @@ dependencies = [ [[package]] name = "rec_aggregation" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "include_dir", @@ -7097,7 +7097,7 @@ dependencies = [ [[package]] name = "sub_protocols" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "lean_vm", @@ -7113,7 +7113,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sumcheck" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "air", "fiat-shamir", @@ -7127,7 +7127,7 @@ dependencies = [ [[package]] name = "symetric" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "field", "koala-bear", @@ -7215,7 +7215,7 @@ dependencies = [ [[package]] name = "system-info" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "libc", ] @@ -7748,7 +7748,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "parallel", "serde", @@ -8012,7 +8012,7 @@ dependencies = [ [[package]] name = "whir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "fiat-shamir", "field", @@ -8570,7 +8570,7 @@ dependencies = [ [[package]] name = "xmss" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "backend", "ethereum_ssz", @@ -8745,7 +8745,7 @@ dependencies = [ [[package]] name = "zk-alloc" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?branch=main#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" dependencies = [ "libc", "parallel", diff --git a/Cargo.toml b/Cargo.toml index 28fc5b82..97cc5a6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,11 +55,13 @@ prometheus = "0.14" clap = { version = "4.3", features = ["derive", "env"] } -# XMSS signatures + recursive aggregation (leanVM, main branch). +# XMSS signatures + recursive aggregation (leanVM). # leanVM's `main` internalized XMSS (dropping the external leanSig dependency) # and reworked the aggregation API (see "Xmss api rework"). -xmss = { git = "https://github.com/leanEthereum/leanVM.git", branch = "main" } -rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", branch = "main" } +# Pinned to the current `main` HEAD (branch `main`, commit a73ab11) for +# reproducible builds; bump the rev to track main forward. +xmss = { git = "https://github.com/leanEthereum/leanVM.git", rev = "a73ab114721441aa833384e791f3b83801257456" } +rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", rev = "a73ab114721441aa833384e791f3b83801257456" } # SSZ codec used by leanVM's xmss pubkey/signature types (ethereum_ssz, not libssz). ssz = { package = "ethereum_ssz", version = "0.10" } From cd293eb9ad3ace43b2bbfb5aacedb556f702b74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:13:35 -0300 Subject: [PATCH 03/14] build(deps): bump leanVM pin to c83b40f Moves the pin two commits forward from a73ab11 (`Xmss api rework`, #262): - `105c5c69` rec_aggregation: optional pubkey-less aggregate serialization - `c83b40f0` fully single-threaded verifier (`Vec` instead of `ArenaVec`), plus a `forbid-parallelism` feature that asserts it `105c5c69` split `SingleMessageInfo` into a `SingleMessageCore` (message, slot, bytecode claim) plus the pubkey set, so the binding checks in the Type-1 and Type-2 verify paths reach through `.core`. The `xmss` crate is byte-identical between the two revs, so key and pubkey formats are unchanged: the genesis keys generated for a73ab11 verify as-is. `c83b40f0` makes proof verification sequential rather than pool-dispatched. The `forbid_parallelism()` guards it adds are inert unless leanVM's `forbid-parallelism` feature is enabled, which we do not enable. Validated on a 3-node all-ethlambda devnet with fresh new-format genesis: real leanVM proving on every block, Type-2 aggregates verified from gossip in ~50ms, finality advancing one slot per slot, no errors. --- Cargo.lock | 44 ++++++++++++++++----------------- Cargo.toml | 8 +++--- crates/common/crypto/src/lib.rs | 14 +++++------ 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0e2fc67..20c6e160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ dependencies = [ [[package]] name = "air" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "field", "poly", @@ -661,7 +661,7 @@ dependencies = [ [[package]] name = "backend" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "air", "fiat-shamir", @@ -1570,7 +1570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2564,7 +2564,7 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "fiat-shamir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "field", "koala-bear", @@ -2577,9 +2577,8 @@ dependencies = [ [[package]] name = "field" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ - "parallel", "paste", "rand 0.10.1", "serde", @@ -3647,7 +3646,7 @@ dependencies = [ [[package]] name = "koala-bear" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "field", "paste", @@ -3709,7 +3708,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lean_compiler" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "backend", "include_dir", @@ -3723,7 +3722,7 @@ dependencies = [ [[package]] name = "lean_prover" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "backend", "lean_compiler", @@ -3738,7 +3737,7 @@ dependencies = [ [[package]] name = "lean_vm" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "backend", "tracing", @@ -5290,7 +5289,7 @@ dependencies = [ [[package]] name = "parallel" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "system-info", ] @@ -5485,7 +5484,7 @@ dependencies = [ [[package]] name = "poly" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "field", "parallel", @@ -6068,7 +6067,7 @@ dependencies = [ [[package]] name = "rec_aggregation" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "backend", "include_dir", @@ -6077,6 +6076,7 @@ dependencies = [ "lean_vm", "objc2", "objc2-foundation", + "parallel", "postcard", "serde", "sub_protocols", @@ -7097,7 +7097,7 @@ dependencies = [ [[package]] name = "sub_protocols" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "backend", "lean_vm", @@ -7113,7 +7113,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sumcheck" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "air", "fiat-shamir", @@ -7127,7 +7127,7 @@ dependencies = [ [[package]] name = "symetric" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "field", "koala-bear", @@ -7215,7 +7215,7 @@ dependencies = [ [[package]] name = "system-info" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "libc", ] @@ -7239,7 +7239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -7748,7 +7748,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "parallel", "serde", @@ -8012,7 +8012,7 @@ dependencies = [ [[package]] name = "whir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "fiat-shamir", "field", @@ -8570,7 +8570,7 @@ dependencies = [ [[package]] name = "xmss" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "backend", "ethereum_ssz", @@ -8745,7 +8745,7 @@ dependencies = [ [[package]] name = "zk-alloc" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=a73ab114721441aa833384e791f3b83801257456#a73ab114721441aa833384e791f3b83801257456" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" dependencies = [ "libc", "parallel", diff --git a/Cargo.toml b/Cargo.toml index 97cc5a6a..556a3fcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,10 +58,10 @@ clap = { version = "4.3", features = ["derive", "env"] } # XMSS signatures + recursive aggregation (leanVM). # leanVM's `main` internalized XMSS (dropping the external leanSig dependency) # and reworked the aggregation API (see "Xmss api rework"). -# Pinned to the current `main` HEAD (branch `main`, commit a73ab11) for -# reproducible builds; bump the rev to track main forward. -xmss = { git = "https://github.com/leanEthereum/leanVM.git", rev = "a73ab114721441aa833384e791f3b83801257456" } -rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", rev = "a73ab114721441aa833384e791f3b83801257456" } +# Pinned to a `main` commit (`c83b40f`) for reproducible builds; bump the rev to +# track main forward. +xmss = { git = "https://github.com/leanEthereum/leanVM.git", rev = "c83b40f01e809f1bd3238d17a821a1ff3ed8171d" } +rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", rev = "c83b40f01e809f1bd3238d17a821a1ff3ed8171d" } # SSZ codec used by leanVM's xmss pubkey/signature types (ethereum_ssz, not libssz). ssz = { package = "ethereum_ssz", version = "0.10" } diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index ca1dfa61..10225dd1 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -351,12 +351,12 @@ pub fn verify_aggregated_signature( let sig = LMType1::from_bytes(proof_data.iter().as_slice()) .ok_or(VerificationError::DeserializationFailed)?; - if sig.info.message != message.0 || sig.info.slot != slot { + if sig.info.core.message != message.0 || sig.info.core.slot != slot { return Err(VerificationError::BindingMismatch { expected_msg: *message, expected_slot: slot, - got_msg: H256(sig.info.message), - got_slot: sig.info.slot, + got_msg: H256(sig.info.core.message), + got_slot: sig.info.core.slot, }); } @@ -460,12 +460,12 @@ pub fn verify_type_2_signature( for (idx, ((expected_msg, expected_slot), info)) in expected_bindings.iter().zip(sig.info.iter()).enumerate() { - if info.message != expected_msg.0 || info.slot != *expected_slot { + if info.core.message != expected_msg.0 || info.core.slot != *expected_slot { return Err(VerificationError::BindingMismatch { expected_msg: *expected_msg, expected_slot: *expected_slot, - got_msg: H256(info.message), - got_slot: info.slot, + got_msg: H256(info.core.message), + got_slot: info.core.slot, }); } // The proof embeds each component's participant set; bind it to the @@ -509,7 +509,7 @@ pub fn split_type_2_by_message( .info .iter() .enumerate() - .filter_map(|(i, info)| (info.message == message.0).then_some(i)) + .filter_map(|(i, info)| (info.core.message == message.0).then_some(i)) .collect(); let index = match matches.as_slice() { [i] => *i, From d68b246f14e945b572cd040c34d22431e58ddb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:02:37 -0300 Subject: [PATCH 04/14] feat(crypto)!: keep pubkeys off the proof wire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leanVM `105c5c69` restored optional pubkey-less aggregate serialization, so the wire format no longer has to change relative to `main`. Proofs serialize via `to_bytes_without_pubkeys()` and the caller attaches the resolved signer set at decode with `from_bytes_without_pubkeys()`, exactly as `compress_without_pubkeys()`/`decompress_without_pubkeys()` did before the migration. Every public signature in the crypto crate now matches `main` again, including `split_type_2_by_message`, which regains its `pubkeys_per_component` argument (`reaggregate.rs` already resolves that layout for its merge step, so the call site just passes it through). Attaching the caller's set is what binds a proof to its participants: leanVM sorts and de-duplicates it, then the SNARK checks it against the hash the proof commits to. So the explicit `PublicKeySetMismatch` comparison the embedded-pubkey form needed is gone, and a wrong set now fails inside the verifier — covered by a new `test_verify_wrong_pubkey_set_fails`. Validated on a 3-node all-ethlambda devnet: 114 block Type-2 aggregates decoded and verified through the pubkey-less path (111 carrying attestation components), p50 43.7ms, finality tracking head at 3 slots, no errors. Reaggregation never triggered there, so `split_type_2_by_message` is covered by the Type-2 merge/verify/split round-trip test instead. --- crates/blockchain/src/reaggregate.rs | 7 +- crates/common/crypto/src/lib.rs | 129 +++++++++++++++------------ 2 files changed, 78 insertions(+), 58 deletions(-) diff --git a/crates/blockchain/src/reaggregate.rs b/crates/blockchain/src/reaggregate.rs index 92ff3108..f47d6c51 100644 --- a/crates/blockchain/src/reaggregate.rs +++ b/crates/blockchain/src/reaggregate.rs @@ -122,8 +122,11 @@ pub fn reaggregate_from_block( // Step 1: SNARK-split this attestation's component out of the block's // merged multi-message aggregate proof. let merged_bytes = signed_block.proof.proof_bytes(); - let split_bytes = match ethlambda_crypto::split_type_2_by_message(merged_bytes, &data_root) - { + let split_bytes = match ethlambda_crypto::split_type_2_by_message( + merged_bytes, + pubkeys_per_component.clone(), + &data_root, + ) { Ok(bytes) => bytes, Err(err) => { debug!(%err, data_root = %ethlambda_types::ShortRoot(&data_root.0), diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 10225dd1..1ae4907a 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -83,9 +83,6 @@ pub enum VerificationError { #[error("verification failed: {0}")] VerificationFailed(String), - #[error("aggregate binds a different validator set than expected at component {0}")] - PublicKeySetMismatch(usize), - #[error( "(message, slot) mismatch: proof binds {got_slot}/{got_msg:?}, expected {expected_slot}/{expected_msg:?}" )] @@ -110,45 +107,37 @@ pub enum VerificationError { // Helpers // ===================================================================== -/// The sorted, de-duplicated set of native pubkeys, matching the canonical form -/// leanVM's aggregation embeds inside a proof's `info.pubkeys`. -fn sorted_dedup_pubkeys(pubkeys: Vec) -> Vec { - let mut keys: Vec = pubkeys +fn into_lean_pubkeys(pubkeys: Vec) -> Vec { + pubkeys .into_iter() .map(ValidatorPublicKey::into_inner) - .collect(); - keys.sort(); - keys.dedup(); - keys + .collect() } -/// Deserialize a stored Type-1 proof. +/// Decompress a stored Type-1 proof (without-pubkeys form) into a native +/// `SingleMessageAggregateSignature` by attaching the resolved validator pubkeys. /// -/// leanVM's `main` embeds the participant pubkeys inside the serialized proof -/// (`info.pubkeys`), so decoding no longer needs the caller-supplied pubkeys. -/// They are still cross-checked against the proof's embedded set as an -/// integrity guard. +/// leanVM sorts and de-duplicates the attached set itself, so the order here does +/// not matter. A set other than the one aggregated attaches fine but fails +/// verification: the proof binds a hash of the set. fn decompress_type1( pubkeys: Vec, proof_bytes: &ByteList512KiB, index: usize, ) -> Result { - let sig = LMType1::from_bytes(proof_bytes.iter().as_slice()) - .ok_or(AggregationError::ChildDeserializationFailed(index))?; - if sig.info.pubkeys != sorted_dedup_pubkeys(pubkeys) { - return Err(AggregationError::ChildDeserializationFailed(index)); - } - Ok(sig) + let lean_pks = into_lean_pubkeys(pubkeys); + LMType1::from_bytes_without_pubkeys(proof_bytes.iter().as_slice(), lean_pks) + .ok_or(AggregationError::ChildDeserializationFailed(index)) } fn compress_type1_to_byte_list(sig: &LMType1) -> Result { - let serialized = sig.to_bytes(); + let serialized = sig.to_bytes_without_pubkeys(); let len = serialized.len(); ByteList512KiB::try_from(serialized).map_err(|_| AggregationError::ProofTooBig(len)) } fn compress_type2_to_byte_list(sig: &LMType2) -> Result { - let serialized = sig.to_bytes(); + let serialized = sig.to_bytes_without_pubkeys(); let len = serialized.len(); ByteList512KiB::try_from(serialized).map_err(|_| AggregationError::ProofTooBig(len)) } @@ -164,9 +153,10 @@ fn compress_type2_to_byte_list(sig: &LMType2) -> Result, signatures: Vec, @@ -348,7 +338,8 @@ pub fn verify_aggregated_signature( } ensure_verifier_ready(); - let sig = LMType1::from_bytes(proof_data.iter().as_slice()) + let lean_pubkeys = into_lean_pubkeys(public_keys); + let sig = LMType1::from_bytes_without_pubkeys(proof_data.iter().as_slice(), lean_pubkeys) .ok_or(VerificationError::DeserializationFailed)?; if sig.info.core.message != message.0 || sig.info.core.slot != slot { @@ -360,11 +351,6 @@ pub fn verify_aggregated_signature( }); } - // The proof embeds its participant set; bind it to the caller's expectation. - if sig.info.pubkeys != sorted_dedup_pubkeys(public_keys) { - return Err(VerificationError::PublicKeySetMismatch(0)); - } - verify_single_message_aggregate(&sig) .map_err(|err| VerificationError::VerificationFailed(format!("{err:?}")))?; Ok(()) @@ -377,11 +363,11 @@ pub fn verify_aggregated_signature( /// Merge many independent Type-1 multi-signatures into a single Type-2 proof. /// /// Each input is `(participant_pubkeys, type_1_proof_bytes)` where the bytes -/// are the `to_bytes()` form of a `SingleMessageAggregateSignature`. The pubkeys -/// are also embedded in the proof; the caller-supplied set is cross-checked. +/// are the `to_bytes_without_pubkeys()` form of a `SingleMessageAggregateSignature`. /// -/// The returned blob is the `to_bytes()` form of the resulting -/// `MultiMessageAggregateSignature`, with each component's pubkeys embedded. +/// The returned blob is the `to_bytes_without_pubkeys()` form of the resulting +/// `MultiMessageAggregateSignature`. A verifier decoding it back needs the per-component +/// pubkey sets in the same order. pub fn merge_type_1s_into_type_2( type_1s: Vec<(Vec, ByteList512KiB)>, ) -> Result { @@ -443,12 +429,13 @@ pub fn verify_type_2_signature( ensure_verifier_ready(); - let expected_pubkeys_per_info: Vec> = pubkeys_per_component + let pubkeys_per_info: Vec> = pubkeys_per_component .into_iter() - .map(sorted_dedup_pubkeys) + .map(into_lean_pubkeys) .collect(); - let sig = LMType2::from_bytes(proof_data).ok_or(VerificationError::DeserializationFailed)?; + let sig = LMType2::from_bytes_without_pubkeys(proof_data, pubkeys_per_info) + .ok_or(VerificationError::DeserializationFailed)?; if sig.info.len() != expected_bindings.len() { return Err(VerificationError::Type2ComponentCountMismatch { @@ -457,9 +444,7 @@ pub fn verify_type_2_signature( }); } - for (idx, ((expected_msg, expected_slot), info)) in - expected_bindings.iter().zip(sig.info.iter()).enumerate() - { + for ((expected_msg, expected_slot), info) in expected_bindings.iter().zip(sig.info.iter()) { if info.core.message != expected_msg.0 || info.core.slot != *expected_slot { return Err(VerificationError::BindingMismatch { expected_msg: *expected_msg, @@ -468,11 +453,6 @@ pub fn verify_type_2_signature( got_slot: info.core.slot, }); } - // The proof embeds each component's participant set; bind it to the - // caller's expectation. - if info.pubkeys != expected_pubkeys_per_info[idx] { - return Err(VerificationError::PublicKeySetMismatch(idx)); - } } verify_multi_message_aggregate(&sig) @@ -483,12 +463,17 @@ pub fn verify_type_2_signature( /// Split (disaggregate) a Type-2 merged proof into a single Type-1 proof for /// the component bound to `message`. Generates a fresh SNARK; expensive. /// -/// Mirrors `split_multi_message_aggregate_by_message`: the caller supplies the -/// expected message (an attestation data root or the block root) and the -/// wrapper locates the unique matching component inside the decoded proof. -/// Returns the `to_bytes()` form of the resulting Type-1. +/// Mirrors leanSpec PR #717 `split_multi_message_aggregate_by_message`: the caller +/// supplies the expected message (an attestation data root or the block +/// root) and the wrapper locates the unique matching component inside the +/// decompressed proof. Returns the `to_bytes_without_pubkeys()` form of the +/// resulting Type-1. +/// +/// `pubkeys_per_component` gives one signer set per component, in the proof's +/// component order; they are not on the wire, so decoding needs them. pub fn split_type_2_by_message( proof_data: &[u8], + pubkeys_per_component: Vec>, message: &H256, ) -> Result { #[cfg(feature = "shadow-integration")] @@ -501,9 +486,13 @@ pub fn split_type_2_by_message( ensure_prover_ready(); - // leanVM embeds each component's pubkeys in the serialized proof, so decoding - // no longer needs caller-supplied pubkey sets. - let type_2 = LMType2::from_bytes(proof_data).ok_or(AggregationError::DeserializationFailed)?; + let pubkeys_per_info: Vec> = pubkeys_per_component + .into_iter() + .map(into_lean_pubkeys) + .collect(); + + let type_2 = LMType2::from_bytes_without_pubkeys(proof_data, pubkeys_per_info) + .ok_or(AggregationError::DeserializationFailed)?; let matches: Vec = type_2 .info @@ -671,6 +660,29 @@ mod tests { ); } + /// The signer set is not carried on the wire, so it is the caller-supplied + /// set that binds a proof to its participants. Supplying a different set of + /// the same size must be rejected: the proof commits to a hash of the set, + /// so it fails inside the SNARK verifier rather than at decode. + #[test] + #[ignore = "too slow"] + fn test_verify_wrong_pubkey_set_fails() { + let message = H256::from([42u8; 32]); + let slot = 10u32; + + let (pk, sig) = generate_keypair_and_sign(1, 5, slot, &message); + let (other_pk, _) = generate_keypair_and_sign(2, 5, slot, &message); + + let proof_data = aggregate_signatures(vec![pk], vec![sig], &message, slot).unwrap(); + + let verify_result = + verify_aggregated_signature(&proof_data, vec![other_pk], &message, slot); + assert!( + verify_result.is_err(), + "Verification should have failed with a different signer set" + ); + } + /// End-to-end Type-2 round-trip: produce two Type-1s (different (msg, slot)), /// merge them into a Type-2, verify the Type-2, then split out one component /// and verify it as a Type-1. @@ -699,7 +711,12 @@ mod tests { ) .expect("verify type-2"); - let split = split_type_2_by_message(merged.iter().as_slice(), &msg_a).expect("split"); + let split = split_type_2_by_message( + merged.iter().as_slice(), + vec![vec![pk_a.clone()], vec![pk_b.clone()]], + &msg_a, + ) + .expect("split"); verify_aggregated_signature(&split, vec![pk_a.clone()], &msg_a, slot_a) .expect("verify split"); From db7bc133f5e66a45bcd7ce065152fdbfe43b136b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 27 Jul 2026 19:58:01 +0100 Subject: [PATCH 05/14] fix(crypto): use leanVM's facade and prove one at a time `ensure_prover_ready` only called `init_aggregation_bytecode`, so the arena was never engaged and every prover buffer used the system allocator. `setup_prover` lives in the `lean-multisig` facade, not in `rec_aggregation`, so importing crate by crate hid it. Engaging the arena arms a panic: only one proof may run at a time. Take a permit around each proving entry point; without it 5 of the 7 crypto tests panic. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 61 +++++++++++++++++++++------------ Cargo.toml | 7 ++-- crates/common/crypto/Cargo.toml | 6 ++-- crates/common/crypto/src/lib.rs | 42 +++++++++++++++-------- 4 files changed, 72 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20c6e160..b99ebe33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,9 +88,10 @@ dependencies = [ [[package]] name = "air" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "field", + "koala-bear", "poly", ] @@ -661,7 +662,7 @@ dependencies = [ [[package]] name = "backend" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "air", "fiat-shamir", @@ -2068,10 +2069,10 @@ dependencies = [ "ethereum_ssz", "ethlambda-types", "hex", + "lean-multisig", "rand 0.10.1", - "rec_aggregation", "thiserror 2.0.18", - "xmss", + "tracing", ] [[package]] @@ -2564,7 +2565,7 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "fiat-shamir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "field", "koala-bear", @@ -2577,7 +2578,7 @@ dependencies = [ [[package]] name = "field" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "paste", "rand 0.10.1", @@ -3646,7 +3647,7 @@ dependencies = [ [[package]] name = "koala-bear" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "field", "paste", @@ -3705,10 +3706,26 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lean-multisig" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +dependencies = [ + "backend", + "clap", + "lean_vm", + "rec_aggregation", + "serde_json", + "sub_protocols", + "system-info", + "xmss", + "zk-alloc", +] + [[package]] name = "lean_compiler" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "backend", "include_dir", @@ -3722,7 +3739,7 @@ dependencies = [ [[package]] name = "lean_prover" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "backend", "lean_compiler", @@ -3737,7 +3754,7 @@ dependencies = [ [[package]] name = "lean_vm" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "backend", "tracing", @@ -5289,7 +5306,7 @@ dependencies = [ [[package]] name = "parallel" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "system-info", ] @@ -5484,9 +5501,10 @@ dependencies = [ [[package]] name = "poly" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "field", + "koala-bear", "parallel", "rand 0.10.1", "serde", @@ -6067,7 +6085,7 @@ dependencies = [ [[package]] name = "rec_aggregation" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "backend", "include_dir", @@ -7097,7 +7115,7 @@ dependencies = [ [[package]] name = "sub_protocols" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "backend", "lean_vm", @@ -7113,11 +7131,12 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sumcheck" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "air", "fiat-shamir", "field", + "koala-bear", "parallel", "poly", "tracing", @@ -7127,7 +7146,7 @@ dependencies = [ [[package]] name = "symetric" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "field", "koala-bear", @@ -7215,7 +7234,7 @@ dependencies = [ [[package]] name = "system-info" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "libc", ] @@ -7748,7 +7767,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "parallel", "serde", @@ -8012,7 +8031,7 @@ dependencies = [ [[package]] name = "whir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "fiat-shamir", "field", @@ -8570,7 +8589,7 @@ dependencies = [ [[package]] name = "xmss" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "backend", "ethereum_ssz", @@ -8745,7 +8764,7 @@ dependencies = [ [[package]] name = "zk-alloc" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=c83b40f01e809f1bd3238d17a821a1ff3ed8171d#c83b40f01e809f1bd3238d17a821a1ff3ed8171d" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" dependencies = [ "libc", "parallel", diff --git a/Cargo.toml b/Cargo.toml index 556a3fcc..b201f875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,10 +58,9 @@ clap = { version = "4.3", features = ["derive", "env"] } # XMSS signatures + recursive aggregation (leanVM). # leanVM's `main` internalized XMSS (dropping the external leanSig dependency) # and reworked the aggregation API (see "Xmss api rework"). -# Pinned to a `main` commit (`c83b40f`) for reproducible builds; bump the rev to -# track main forward. -xmss = { git = "https://github.com/leanEthereum/leanVM.git", rev = "c83b40f01e809f1bd3238d17a821a1ff3ed8171d" } -rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", rev = "c83b40f01e809f1bd3238d17a821a1ff3ed8171d" } +# Pinned to a `main` commit for reproducible builds; bump the revs to track main. +xmss = { git = "https://github.com/leanEthereum/leanVM.git", rev = "380da82c330eac2841252a655a86ea48ec173bb6" } +lean-multisig = { git = "https://github.com/leanEthereum/leanVM.git", rev = "380da82c330eac2841252a655a86ea48ec173bb6" } # SSZ codec used by leanVM's xmss pubkey/signature types (ethereum_ssz, not libssz). ssz = { package = "ethereum_ssz", version = "0.10" } diff --git a/crates/common/crypto/Cargo.toml b/crates/common/crypto/Cargo.toml index 83291605..57db14dd 100644 --- a/crates/common/crypto/Cargo.toml +++ b/crates/common/crypto/Cargo.toml @@ -12,13 +12,11 @@ version.workspace = true [dependencies] ethlambda-types.workspace = true -# rec_aggregation is leanVM's recursive XMSS aggregation prover/verifier; -# xmss provides the public-key/signature types its API consumes. -rec_aggregation.workspace = true -xmss.workspace = true +lean-multisig.workspace = true thiserror.workspace = true rand.workspace = true +tracing.workspace = true [features] shadow-integration = [] diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 1ae4907a..39eb75d0 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -3,13 +3,15 @@ use ethlambda_types::{ primitives::H256, signature::{LeanSigPublicKey, LeanSigSignature, ValidatorPublicKey, ValidatorSignature}, }; -use rec_aggregation::{ +use lean_multisig::{ MultiMessageAggregateSignature as LMType2, SingleMessageAggregateSignature as LMType1, - aggregate_single_message_signatures, init_aggregation_bytecode, - merge_single_message_aggregates, split_multi_message_aggregate, verify_multi_message_aggregate, + aggregate_single_message_signatures, merge_single_message_aggregates, setup_prover, + setup_verifier, split_multi_message_aggregate, verify_multi_message_aggregate, verify_single_message_aggregate, }; +use std::sync::{Mutex, MutexGuard}; use thiserror::Error; +use tracing::error; #[cfg(feature = "shadow-integration")] pub mod shadow_cost; @@ -17,21 +19,26 @@ pub mod shadow_cost; /// log(1/rate) for the WHIR commitment scheme used inside the aggregation prover. const LOG_INV_RATE: usize = 2; -/// Ensure the aggregation bytecode is compiled. Safe to call multiple times. -/// -/// leanVM's `main` replaced the old `setup_prover`/`setup_verifier` split with a -/// single self-referential aggregation bytecode used by both the prover and the -/// verifier; `init_aggregation_bytecode` is idempotent (`OnceLock::get_or_init`). +/// leanVM allows one proof at a time per process; a second one panics. +static PROVER_PERMIT: Mutex<()> = Mutex::new(()); + +/// The permit guards no data, so a poisoned lock is recovered rather than propagated: +/// one panicking prover must not brick every later proof. It is still an incident. +fn prover_permit() -> MutexGuard<'static, ()> { + PROVER_PERMIT.lock().unwrap_or_else(|poisoned| { + error!("a previous proving job panicked while holding the permit; continuing"); + poisoned.into_inner() + }) +} + +/// Engages leanVM's arena; skipping it stays correct but slow. pub fn ensure_prover_ready() { - init_aggregation_bytecode(); + setup_prover(); } -/// Ensure the aggregation bytecode is compiled. Safe to call multiple times. -/// -/// Deserializing any proof (`from_bytes`) also requires the bytecode, so this -/// must run before any verification path. +/// Needed before any decode, not just before verifying. pub fn ensure_verifier_ready() { - init_aggregation_bytecode(); + setup_verifier(); } /// Error type for signature aggregation operations. @@ -187,6 +194,7 @@ pub fn aggregate_signatures( } ensure_prover_ready(); + let _permit = prover_permit(); let raw_xmss: Vec<(LeanSigPublicKey, LeanSigSignature)> = public_keys .into_iter() @@ -242,6 +250,7 @@ pub fn aggregate_mixed( } ensure_prover_ready(); + let _permit = prover_permit(); let children_native: Vec = children .into_iter() @@ -296,6 +305,7 @@ pub fn aggregate_proofs( } ensure_prover_ready(); + let _permit = prover_permit(); let children_native: Vec = children .into_iter() @@ -391,6 +401,7 @@ pub fn merge_type_1s_into_type_2( } ensure_prover_ready(); + let _permit = prover_permit(); let type_1s_native: Vec = type_1s .into_iter() @@ -485,6 +496,7 @@ pub fn split_type_2_by_message( } ensure_prover_ready(); + let _permit = prover_permit(); let pubkeys_per_info: Vec> = pubkeys_per_component .into_iter() @@ -515,8 +527,8 @@ pub fn split_type_2_by_message( #[cfg(test)] mod tests { use super::*; + use lean_multisig::{xmss_key_gen_from_seed, xmss_sign}; use ssz::Encode as _; - use xmss::{xmss_key_gen_from_seed, xmss_sign}; /// Generate a test keypair and sign a message. /// From fcd4a428ada859e88b973b20cdad248a2e48d780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:31:28 -0300 Subject: [PATCH 06/14] refactor(types): drop xmss dep, static-assert wire sizes in crypto `ethlambda-types` pulled in leanVM's `xmss` crate solely to source two integers, dragging the whole signing backend into the dependency graph of a crate that only describes wire formats. Hardcode `SIGNATURE_SIZE` and `PUBLIC_KEY_SIZE` there instead, and pin them to the scheme in `ethlambda-crypto` -- the one crate that sees both sides -- with const assertions. A leanVM bump that changes the XMSS parameters now fails to compile with a named error rather than silently resizing every signature and pubkey on the wire. --- Cargo.lock | 1 - crates/common/crypto/src/signature.rs | 21 ++++++++++++++++++--- crates/common/types/Cargo.toml | 4 ---- crates/common/types/src/attestation.rs | 11 ++++++----- crates/common/types/src/state.rs | 11 ++++++----- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf2ae454..ab8ee7a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2213,7 +2213,6 @@ dependencies = [ "serde_json", "serde_yaml_ng", "thiserror 2.0.18", - "xmss", ] [[package]] diff --git a/crates/common/crypto/src/signature.rs b/crates/common/crypto/src/signature.rs index 507938e3..ceb3f067 100644 --- a/crates/common/crypto/src/signature.rs +++ b/crates/common/crypto/src/signature.rs @@ -3,12 +3,28 @@ use std::ops::Range; -use ethlambda_types::primitives::H256; +use ethlambda_types::{attestation::SIGNATURE_SIZE, primitives::H256, state::PUBLIC_KEY_SIZE}; use ssz::{Decode, Encode}; use xmss::{ - XmssPublicKey, XmssSecretKey, XmssSignature, XmssSignatureError, xmss_sign, xmss_verify, + PUB_KEY_SSZ_LEN, SIGNATURE_SSZ_LEN, XmssPublicKey, XmssSecretKey, XmssSignature, + XmssSignatureError, xmss_sign, xmss_verify, }; +// `ethlambda-types` hardcodes the XMSS wire sizes so it can stay free of the +// signing backend. This crate is the only place that sees both sides, so it is +// where they get pinned together: a leanVM bump that changes the scheme +// parameters fails to compile here instead of silently corrupting the wire +// format (every `XmssSignature` / `ValidatorPubkeyBytes` in `ethlambda-types` is +// sized by these constants). +const _: () = assert!( + SIGNATURE_SIZE == SIGNATURE_SSZ_LEN, + "ethlambda_types::attestation::SIGNATURE_SIZE no longer matches xmss::SIGNATURE_SSZ_LEN", +); +const _: () = assert!( + PUBLIC_KEY_SIZE == PUB_KEY_SSZ_LEN, + "ethlambda_types::state::PUBLIC_KEY_SIZE no longer matches xmss::PUB_KEY_SSZ_LEN", +); + /// The public key type from leanVM's xmss crate. pub type LeanSigPublicKey = XmssPublicKey; @@ -142,7 +158,6 @@ impl ValidatorSecretKey { #[cfg(test)] mod tests { use super::*; - use ethlambda_types::state::PUBLIC_KEY_SIZE; use xmss::xmss_key_gen_from_seed; /// Generate a validator key pair over a small activation range. diff --git a/crates/common/types/Cargo.toml b/crates/common/types/Cargo.toml index 787f855c..383f77a3 100644 --- a/crates/common/types/Cargo.toml +++ b/crates/common/types/Cargo.toml @@ -14,10 +14,6 @@ thiserror.workspace = true serde.workspace = true hex.workspace = true -# Only for the XMSS wire-size constants (PUB_KEY_SSZ_LEN / SIGNATURE_SSZ_LEN); -# the signing primitives themselves live in ethlambda-crypto. -xmss.workspace = true - libssz.workspace = true libssz-derive.workspace = true libssz-merkle.workspace = true diff --git a/crates/common/types/src/attestation.rs b/crates/common/types/src/attestation.rs index 562248a1..e4555235 100644 --- a/crates/common/types/src/attestation.rs +++ b/crates/common/types/src/attestation.rs @@ -56,11 +56,12 @@ pub struct SignedAttestation { /// Size of an SSZ-encoded XMSS signature in bytes. /// -/// Sourced from leanVM's xmss crate rather than hardcoded, so it tracks the -/// scheme parameters (`WOTS_SIG_SIZE_FE`, `LOG_LIFETIME`, `XMSS_DIGEST_LEN`). -/// This is the SSZ wire size, so it lives here rather than in -/// `ethlambda-crypto`: parent containers only need the length. -pub const SIGNATURE_SIZE: usize = xmss::SIGNATURE_SSZ_LEN; +/// Mirrors leanVM's `xmss::SIGNATURE_SSZ_LEN`, hardcoded so this crate stays +/// free of the signing backend: parent containers only need the length, not the +/// scheme. `ethlambda-crypto` static-asserts the two agree, so a leanVM bump +/// that changes the scheme parameters breaks the build rather than silently +/// producing blobs of the wrong length. +pub const SIGNATURE_SIZE: usize = 1208; /// XMSS signature as a fixed-length byte vector (`SIGNATURE_SIZE` bytes). pub type XmssSignature = SszVector; diff --git a/crates/common/types/src/state.rs b/crates/common/types/src/state.rs index f82abda4..e1525e01 100644 --- a/crates/common/types/src/state.rs +++ b/crates/common/types/src/state.rs @@ -86,11 +86,12 @@ where /// Size of an SSZ-encoded XMSS public key in bytes. /// -/// Sourced from leanVM's xmss crate rather than hardcoded, so it tracks the -/// scheme parameters. Lives here rather than in `ethlambda-crypto` because the -/// validator registry is a plain wire type that must not depend on the signing -/// backend. -pub const PUBLIC_KEY_SIZE: usize = xmss::PUB_KEY_SSZ_LEN; +/// Mirrors leanVM's `xmss::PUB_KEY_SSZ_LEN`, hardcoded so this crate stays free +/// of the signing backend: the validator registry is a plain wire type. +/// `ethlambda-crypto` static-asserts the two agree, so a leanVM bump that +/// changes the scheme parameters breaks the build rather than silently +/// producing keys of the wrong length. +pub const PUBLIC_KEY_SIZE: usize = 32; pub type ValidatorPubkeyBytes = [u8; PUBLIC_KEY_SIZE]; From 7fb26d29b7d87cec9cab744ef9e244d9a920b0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:39:18 -0300 Subject: [PATCH 07/14] refactor(crypto): report both sizes when the XMSS wire-size guard fires The const assertions named the mismatched constants but not their values: a const panic message must be a string literal, so it cannot interpolate the two numbers a reader needs in order to fix the constant. Express the check as an array-length mismatch instead. rustc then evaluates both sides and prints them ("expected an array with a size of 2536, found one with a size of 1208"), so the corrected value is right there in the error. A named constant would read better than `const _` but trips `dead_code`, which is denied in CI, so the intent lives in the comment -- including a warning not to take rustc's suggestion to edit the array length, which would silence the guard rather than fix the constant. --- crates/common/crypto/src/signature.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/common/crypto/src/signature.rs b/crates/common/crypto/src/signature.rs index ceb3f067..f4e5820b 100644 --- a/crates/common/crypto/src/signature.rs +++ b/crates/common/crypto/src/signature.rs @@ -16,14 +16,16 @@ use xmss::{ // parameters fails to compile here instead of silently corrupting the wire // format (every `XmssSignature` / `ValidatorPubkeyBytes` in `ethlambda-types` is // sized by these constants). -const _: () = assert!( - SIGNATURE_SIZE == SIGNATURE_SSZ_LEN, - "ethlambda_types::attestation::SIGNATURE_SIZE no longer matches xmss::SIGNATURE_SSZ_LEN", -); -const _: () = assert!( - PUBLIC_KEY_SIZE == PUB_KEY_SSZ_LEN, - "ethlambda_types::state::PUBLIC_KEY_SIZE no longer matches xmss::PUB_KEY_SSZ_LEN", -); +// +// Written as an array-length mismatch rather than `assert!` so that rustc prints +// both evaluated sizes; a const panic message has to be a string literal and so +// cannot name the numbers that actually differ. +// +// When one of these fires, correct the constant in `ethlambda-types` to the size +// rustc reports. Do not take rustc's `help:` suggestion to edit the length on +// these lines, which only silences the check. +const _: [(); SIGNATURE_SIZE] = [(); SIGNATURE_SSZ_LEN]; +const _: [(); PUBLIC_KEY_SIZE] = [(); PUB_KEY_SSZ_LEN]; /// The public key type from leanVM's xmss crate. pub type LeanSigPublicKey = XmssPublicKey; From 80080a0b07a1c8dfc1bf360d494f5118f2468b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:27:14 -0300 Subject: [PATCH 08/14] build(deps): track leanVM a5909d1 and prove on the system allocator leanVM's `setup_prover` engages a bump arena whose slabs never go back to the OS: `free` is a no-op and `begin_phase` only rewinds the per-thread bump pointers, so a node's RSS ratchets up to its allocation high-water mark and stays there whether or not it keeps proving. The new rev adds `setup_prover_without_arena`, which initializes the same prover on the system allocator. Proving is slower; memory is bounded and proofs are identical, which is the trade a long-lived node wants. --- Cargo.lock | 56 ++++++++++++++++----------------- Cargo.toml | 4 +-- crates/common/crypto/src/lib.rs | 14 ++++++--- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab8ee7a6..72117690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ dependencies = [ [[package]] name = "air" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "field", "koala-bear", @@ -184,7 +184,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -195,7 +195,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -662,7 +662,7 @@ dependencies = [ [[package]] name = "backend" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "air", "fiat-shamir", @@ -1943,7 +1943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2565,7 +2565,7 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "fiat-shamir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "field", "koala-bear", @@ -2578,7 +2578,7 @@ dependencies = [ [[package]] name = "field" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "paste", "rand 0.10.1", @@ -3647,7 +3647,7 @@ dependencies = [ [[package]] name = "koala-bear" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "field", "paste", @@ -3709,7 +3709,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lean-multisig" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "backend", "clap", @@ -3725,7 +3725,7 @@ dependencies = [ [[package]] name = "lean_compiler" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "backend", "include_dir", @@ -3739,7 +3739,7 @@ dependencies = [ [[package]] name = "lean_prover" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "backend", "lean_compiler", @@ -3754,7 +3754,7 @@ dependencies = [ [[package]] name = "lean_vm" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "backend", "tracing", @@ -4951,7 +4951,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5306,7 +5306,7 @@ dependencies = [ [[package]] name = "parallel" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "system-info", ] @@ -5501,7 +5501,7 @@ dependencies = [ [[package]] name = "poly" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "field", "koala-bear", @@ -6085,7 +6085,7 @@ dependencies = [ [[package]] name = "rec_aggregation" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "backend", "include_dir", @@ -6402,7 +6402,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6914,7 +6914,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7115,7 +7115,7 @@ dependencies = [ [[package]] name = "sub_protocols" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "backend", "lean_vm", @@ -7131,7 +7131,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sumcheck" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "air", "fiat-shamir", @@ -7146,7 +7146,7 @@ dependencies = [ [[package]] name = "symetric" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "field", "koala-bear", @@ -7234,7 +7234,7 @@ dependencies = [ [[package]] name = "system-info" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "libc", ] @@ -7261,7 +7261,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7767,7 +7767,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "parallel", "serde", @@ -8031,7 +8031,7 @@ dependencies = [ [[package]] name = "whir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "fiat-shamir", "field", @@ -8085,7 +8085,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8589,7 +8589,7 @@ dependencies = [ [[package]] name = "xmss" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "backend", "ethereum_ssz", @@ -8764,7 +8764,7 @@ dependencies = [ [[package]] name = "zk-alloc" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanVM.git?rev=380da82c330eac2841252a655a86ea48ec173bb6#380da82c330eac2841252a655a86ea48ec173bb6" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=a5909d18647de6aed38640c098d9177fab2bf36a#a5909d18647de6aed38640c098d9177fab2bf36a" dependencies = [ "libc", "parallel", diff --git a/Cargo.toml b/Cargo.toml index 327d0fc1..0e4a59e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,8 +59,8 @@ clap = { version = "4.3", features = ["derive", "env"] } # leanVM's `main` internalized XMSS (dropping the external leanSig dependency) # and reworked the aggregation API (see "Xmss api rework"). # Pinned to a `main` commit for reproducible builds; bump the revs to track main. -xmss = { git = "https://github.com/leanEthereum/leanVM.git", rev = "380da82c330eac2841252a655a86ea48ec173bb6" } -lean-multisig = { git = "https://github.com/leanEthereum/leanVM.git", rev = "380da82c330eac2841252a655a86ea48ec173bb6" } +xmss = { git = "https://github.com/leanEthereum/leanVM.git", rev = "a5909d18647de6aed38640c098d9177fab2bf36a" } +lean-multisig = { git = "https://github.com/leanEthereum/leanVM.git", rev = "a5909d18647de6aed38640c098d9177fab2bf36a" } # SSZ codec used by leanVM's xmss pubkey/signature types (ethereum_ssz, not libssz). ssz = { package = "ethereum_ssz", version = "0.10" } diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 10cb75c1..17d665f8 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -5,9 +5,9 @@ use crate::signature::{ }; use lean_multisig::{ MultiMessageAggregateSignature as LMType2, SingleMessageAggregateSignature as LMType1, - aggregate_single_message_signatures, merge_single_message_aggregates, setup_prover, - setup_verifier, split_multi_message_aggregate, verify_multi_message_aggregate, - verify_single_message_aggregate, + aggregate_single_message_signatures, merge_single_message_aggregates, + setup_prover_without_arena, setup_verifier, split_multi_message_aggregate, + verify_multi_message_aggregate, verify_single_message_aggregate, }; use std::sync::{Mutex, MutexGuard}; use thiserror::Error; @@ -33,9 +33,13 @@ fn prover_permit() -> MutexGuard<'static, ()> { }) } -/// Engages leanVM's arena; skipping it stays correct but slow. +/// Initializes the proving backend on the system allocator. +/// +/// leanVM's `setup_prover` enables a bump arena that never releases pages, so every +/// node's RSS climbs to the arena cap regardless of how much proving it does. We trade +/// the arena's throughput for bounded memory: proving is slower, results are identical. pub fn ensure_prover_ready() { - setup_prover(); + setup_prover_without_arena(); } /// Needed before any decode, not just before verifying. From 64b109ac877190c5681080954352c784202d6eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:53:56 -0300 Subject: [PATCH 09/14] refactor(crypto): hand out prover setup and the permit together Every prover entry point paired `ensure_prover_ready()` with `prover_permit()`, and neither is useful without the other: setup is a no-op after the first call, and proving without the permit is exactly the concurrent-proof case leanVM panics on. Fold them into `acquire_prover()`, which returns the guard, so a caller cannot reach the prover half-armed. Setup now runs under the permit, which serializes the one-time bytecode compile instead of racing every waiting caller into the same `OnceLock`. The idempotency test drops each guard before taking the next: the permit is not reentrant, so holding two at once in one thread would deadlock. --- crates/common/crypto/src/lib.rs | 55 +++++++++++++++++---------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 17d665f8..4bac5312 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -21,25 +21,30 @@ pub mod shadow_cost; /// log(1/rate) for the WHIR commitment scheme used inside the aggregation prover. const LOG_INV_RATE: usize = 2; -/// leanVM allows one proof at a time per process; a second one panics. -static PROVER_PERMIT: Mutex<()> = Mutex::new(()); - +/// Claims the right to prove, initializing the backend on first use. +/// +/// Setup and the permit are handed out together because every prover entry point needs +/// both: leanVM allows one proof at a time per process, and a second concurrent one panics. +/// Proving is legal only while the returned guard is alive. +/// +/// Setup deliberately skips leanVM's `setup_prover`, which engages a bump arena that never +/// returns pages to the OS: `free` is a no-op and a phase reset only rewinds the per-thread +/// bump pointers, so a node's RSS ratchets to its high-water mark and stays there. +/// `setup_prover_without_arena` runs the same prover on the system allocator: slower, +/// bounded, identical proofs. +/// /// The permit guards no data, so a poisoned lock is recovered rather than propagated: /// one panicking prover must not brick every later proof. It is still an incident. -fn prover_permit() -> MutexGuard<'static, ()> { - PROVER_PERMIT.lock().unwrap_or_else(|poisoned| { +fn acquire_prover() -> MutexGuard<'static, ()> { + static PROVER_PERMIT: Mutex<()> = Mutex::new(()); + let permit = PROVER_PERMIT.lock().unwrap_or_else(|poisoned| { error!("a previous proving job panicked while holding the permit; continuing"); poisoned.into_inner() - }) -} - -/// Initializes the proving backend on the system allocator. -/// -/// leanVM's `setup_prover` enables a bump arena that never releases pages, so every -/// node's RSS climbs to the arena cap regardless of how much proving it does. We trade -/// the arena's throughput for bounded memory: proving is slower, results are identical. -pub fn ensure_prover_ready() { + }); + // Idempotent, and cheap after the first call. Held under the permit so the one-time + // bytecode compile runs serialized rather than racing every waiting caller into it. setup_prover_without_arena(); + permit } /// Needed before any decode, not just before verifying. @@ -199,8 +204,7 @@ pub fn aggregate_signatures( return Ok(dummy); } - ensure_prover_ready(); - let _permit = prover_permit(); + let _permit = acquire_prover(); let raw_xmss: Vec<(LeanSigPublicKey, LeanSigSignature)> = public_keys .into_iter() @@ -255,8 +259,7 @@ pub fn aggregate_mixed( return Ok(dummy); } - ensure_prover_ready(); - let _permit = prover_permit(); + let _permit = acquire_prover(); let children_native: Vec = children .into_iter() @@ -310,8 +313,7 @@ pub fn aggregate_proofs( return Ok(dummy); } - ensure_prover_ready(); - let _permit = prover_permit(); + let _permit = acquire_prover(); let children_native: Vec = children .into_iter() @@ -406,8 +408,7 @@ pub fn merge_type_1s_into_type_2( return Ok(dummy); } - ensure_prover_ready(); - let _permit = prover_permit(); + let _permit = acquire_prover(); let type_1s_native: Vec = type_1s .into_iter() @@ -501,8 +502,7 @@ pub fn split_type_2_by_message( )); } - ensure_prover_ready(); - let _permit = prover_permit(); + let _permit = acquire_prover(); let pubkeys_per_info: Vec> = pubkeys_per_component .into_iter() @@ -570,8 +570,11 @@ mod tests { // Should not panic when called multiple times. The first call compiles // the self-referential aggregation bytecode; subsequent calls are cheap // (`OnceLock::get_or_init`). - ensure_prover_ready(); - ensure_prover_ready(); + // + // The permit is dropped between acquisitions: it is not reentrant, so holding + // both at once would deadlock. That also covers release-on-drop. + drop(acquire_prover()); + drop(acquire_prover()); ensure_verifier_ready(); ensure_verifier_ready(); } From 97cbe1dab9643c46b51ceb7864a56ee4fb074c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:59:36 -0300 Subject: [PATCH 10/14] refactor(crypto): hold the prover permit only across the prove call The permit was taken at the top of each entry point, so a caller sat on the process-wide prover lock while it converted pubkeys, zipped signatures and postcard-decoded child proofs: work that touches no prover state and that grows with the number of aggregated signatures. Take it immediately before the aggregate/merge/split call instead. Decoding does need one thing setup provided: a Type-2 decode rebuilds the bytecode claim, and without the aggregation bytecode it returns `None`, which surfaces as a bogus `DeserializationFailed`. The decoding paths now call `ensure_verifier_ready()`, its documented prerequisite, which is a `OnceLock` init rather than a lock. --- crates/common/crypto/src/lib.rs | 37 ++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 4bac5312..dff662dd 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -27,6 +27,11 @@ const LOG_INV_RATE: usize = 2; /// both: leanVM allows one proof at a time per process, and a second concurrent one panics. /// Proving is legal only while the returned guard is alive. /// +/// Call this immediately before the prove call, not at the top of the function: decoding +/// and argument conversion need no permit, and holding it across them serializes callers +/// for no reason. Decoding does need the aggregation bytecode, which is what +/// [`ensure_verifier_ready`] is for. +/// /// Setup deliberately skips leanVM's `setup_prover`, which engages a bump arena that never /// returns pages to the OS: `free` is a no-op and a phase reset only rewinds the per-thread /// bump pointers, so a node's RSS ratchets to its high-water mark and stays there. @@ -41,8 +46,7 @@ fn acquire_prover() -> MutexGuard<'static, ()> { error!("a previous proving job panicked while holding the permit; continuing"); poisoned.into_inner() }); - // Idempotent, and cheap after the first call. Held under the permit so the one-time - // bytecode compile runs serialized rather than racing every waiting caller into it. + // Idempotent, and cheap after the first call (each step is `Once`/`OnceLock` guarded). setup_prover_without_arena(); permit } @@ -204,14 +208,14 @@ pub fn aggregate_signatures( return Ok(dummy); } - let _permit = acquire_prover(); - let raw_xmss: Vec<(LeanSigPublicKey, LeanSigSignature)> = public_keys .into_iter() .zip(signatures) .map(|(pk, sig)| (pk.into_inner(), sig.into_inner())) .collect(); + let _permit = acquire_prover(); + let proof = aggregate_single_message_signatures(&[], raw_xmss, message.0, slot, LOG_INV_RATE) .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; @@ -259,7 +263,9 @@ pub fn aggregate_mixed( return Ok(dummy); } - let _permit = acquire_prover(); + // Decoding needs the aggregation bytecode, but not the permit: no proving + // happens until the aggregate call below. + ensure_verifier_ready(); let children_native: Vec = children .into_iter() @@ -273,6 +279,8 @@ pub fn aggregate_mixed( .map(|(pk, sig)| (pk.into_inner(), sig.into_inner())) .collect(); + let _permit = acquire_prover(); + let proof = aggregate_single_message_signatures( &children_native, raw_xmss, @@ -313,7 +321,9 @@ pub fn aggregate_proofs( return Ok(dummy); } - let _permit = acquire_prover(); + // Decoding needs the aggregation bytecode, but not the permit: no proving + // happens until the aggregate call below. + ensure_verifier_ready(); let children_native: Vec = children .into_iter() @@ -321,6 +331,8 @@ pub fn aggregate_proofs( .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) .collect::>()?; + let _permit = acquire_prover(); + let proof = aggregate_single_message_signatures( &children_native, vec![], @@ -408,7 +420,9 @@ pub fn merge_type_1s_into_type_2( return Ok(dummy); } - let _permit = acquire_prover(); + // Decoding needs the aggregation bytecode, but not the permit: no proving + // happens until the merge call below. + ensure_verifier_ready(); let type_1s_native: Vec = type_1s .into_iter() @@ -416,6 +430,8 @@ pub fn merge_type_1s_into_type_2( .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) .collect::>()?; + let _permit = acquire_prover(); + let merged = merge_single_message_aggregates(type_1s_native, LOG_INV_RATE) .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; @@ -502,7 +518,10 @@ pub fn split_type_2_by_message( )); } - let _permit = acquire_prover(); + // Type-2 decode rebuilds the bytecode claim, so it needs the aggregation bytecode: + // without it the decode returns `None` and looks like a corrupt proof. It does not + // need the permit, though; no proving happens until the split call below. + ensure_verifier_ready(); let pubkeys_per_info: Vec> = pubkeys_per_component .into_iter() @@ -524,6 +543,8 @@ pub fn split_type_2_by_message( _ => return Err(AggregationError::MultipleMessages), }; + let _permit = acquire_prover(); + let component = split_multi_message_aggregate(type_2, index, LOG_INV_RATE) .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; From 80930d25e97eae3100cb50de93319da77d01db22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:02:48 -0300 Subject: [PATCH 11/14] refactor(crypto): let the decoding paths acquire the prover up front The previous commit had the decoding entry points call `ensure_verifier_ready()` and then `acquire_prover()`, which is the two-call pattern the merge was meant to remove: it splits one setup across two names and invites a caller to assume decoding is verifier-only work. Since those paths need setup before they can decode, they take the permit from the start and let it cover the decode. `aggregate_signatures` keeps the narrow scope: it only reshapes its inputs and never decodes, so nothing there needs setup. --- crates/common/crypto/src/lib.rs | 43 ++++++++++++++------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index dff662dd..305df550 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -27,10 +27,11 @@ const LOG_INV_RATE: usize = 2; /// both: leanVM allows one proof at a time per process, and a second concurrent one panics. /// Proving is legal only while the returned guard is alive. /// -/// Call this immediately before the prove call, not at the top of the function: decoding -/// and argument conversion need no permit, and holding it across them serializes callers -/// for no reason. Decoding does need the aggregation bytecode, which is what -/// [`ensure_verifier_ready`] is for. +/// Take it as late as the setup allows. Callers that only reshape their inputs claim it +/// right before the prove call, since conversion touches no prover state. Callers that +/// decode a stored proof claim it up front instead: decoding needs the aggregation +/// bytecode that setup installs, and a Type-2 decode without it returns `None`, which +/// surfaces as a bogus `DeserializationFailed`. /// /// Setup deliberately skips leanVM's `setup_prover`, which engages a bump arena that never /// returns pages to the OS: `free` is a no-op and a phase reset only rewinds the per-thread @@ -263,9 +264,9 @@ pub fn aggregate_mixed( return Ok(dummy); } - // Decoding needs the aggregation bytecode, but not the permit: no proving - // happens until the aggregate call below. - ensure_verifier_ready(); + // Held from here rather than from the aggregate call: decoding the children needs + // the aggregation bytecode that `acquire_prover` installs. + let _permit = acquire_prover(); let children_native: Vec = children .into_iter() @@ -279,8 +280,6 @@ pub fn aggregate_mixed( .map(|(pk, sig)| (pk.into_inner(), sig.into_inner())) .collect(); - let _permit = acquire_prover(); - let proof = aggregate_single_message_signatures( &children_native, raw_xmss, @@ -321,9 +320,9 @@ pub fn aggregate_proofs( return Ok(dummy); } - // Decoding needs the aggregation bytecode, but not the permit: no proving - // happens until the aggregate call below. - ensure_verifier_ready(); + // Held from here rather than from the aggregate call: decoding the children needs + // the aggregation bytecode that `acquire_prover` installs. + let _permit = acquire_prover(); let children_native: Vec = children .into_iter() @@ -331,8 +330,6 @@ pub fn aggregate_proofs( .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) .collect::>()?; - let _permit = acquire_prover(); - let proof = aggregate_single_message_signatures( &children_native, vec![], @@ -420,9 +417,9 @@ pub fn merge_type_1s_into_type_2( return Ok(dummy); } - // Decoding needs the aggregation bytecode, but not the permit: no proving - // happens until the merge call below. - ensure_verifier_ready(); + // Held from here rather than from the merge call: decoding the inputs needs + // the aggregation bytecode that `acquire_prover` installs. + let _permit = acquire_prover(); let type_1s_native: Vec = type_1s .into_iter() @@ -430,8 +427,6 @@ pub fn merge_type_1s_into_type_2( .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) .collect::>()?; - let _permit = acquire_prover(); - let merged = merge_single_message_aggregates(type_1s_native, LOG_INV_RATE) .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; @@ -518,10 +513,10 @@ pub fn split_type_2_by_message( )); } - // Type-2 decode rebuilds the bytecode claim, so it needs the aggregation bytecode: - // without it the decode returns `None` and looks like a corrupt proof. It does not - // need the permit, though; no proving happens until the split call below. - ensure_verifier_ready(); + // Held from here rather than from the split call: the Type-2 decode below rebuilds + // the bytecode claim, so it needs the aggregation bytecode that `acquire_prover` + // installs. Without it the decode returns `None` and looks like a corrupt proof. + let _permit = acquire_prover(); let pubkeys_per_info: Vec> = pubkeys_per_component .into_iter() @@ -543,8 +538,6 @@ pub fn split_type_2_by_message( _ => return Err(AggregationError::MultipleMessages), }; - let _permit = acquire_prover(); - let component = split_multi_message_aggregate(type_2, index, LOG_INV_RATE) .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; From 3760b9de3a5b5f9af744e0f45c0676fb061bbcf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:39:05 -0300 Subject: [PATCH 12/14] feat(crypto): make the prover arena an opt-in The system allocator is the right default (bounded RSS), but it costs proving throughput, and a host with memory to spare should be able to buy that back. A `OnceLock` carries the choice: `enable_prover_arena()` sets it, and the first `acquire_prover()` latches it via `get_or_init(|| false)`, so the allocator cannot change under a prover that already ran. The setter reports whether it won the latch instead of failing silently, since being called too late is the likely mistake and it is invisible otherwise. Covered by an integration test rather than a lib test: the latch is process-wide, so enabling the arena in the lib binary would change the allocator under every other test and make the result ordering-dependent. --- crates/common/crypto/src/lib.rs | 42 ++++++++++++--------- crates/common/crypto/tests/arena.rs | 58 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 crates/common/crypto/tests/arena.rs diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 305df550..ecc94930 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -5,11 +5,11 @@ use crate::signature::{ }; use lean_multisig::{ MultiMessageAggregateSignature as LMType2, SingleMessageAggregateSignature as LMType1, - aggregate_single_message_signatures, merge_single_message_aggregates, + aggregate_single_message_signatures, merge_single_message_aggregates, setup_prover, setup_prover_without_arena, setup_verifier, split_multi_message_aggregate, verify_multi_message_aggregate, verify_single_message_aggregate, }; -use std::sync::{Mutex, MutexGuard}; +use std::sync::{Mutex, MutexGuard, OnceLock}; use thiserror::Error; use tracing::error; @@ -21,26 +21,27 @@ pub mod shadow_cost; /// log(1/rate) for the WHIR commitment scheme used inside the aggregation prover. const LOG_INV_RATE: usize = 2; +/// Whether to prove on leanVM's arena. Latched by the first [`acquire_prover`]. +static USE_ARENA: OnceLock = OnceLock::new(); + +/// Opts the prover into leanVM's bump arena: faster proving, unbounded RSS. +/// +/// The arena never returns pages to the OS, so a node's memory ratchets to its +/// allocation high-water mark and stays there. Off by default; worth it only where +/// memory is plentiful and proving latency matters. +/// +/// Must be called before the first prove. Returns `false` if the choice was already +/// latched, meaning the call had no effect. +#[must_use = "the arena is not enabled if the choice was already latched"] +pub fn enable_prover_arena() -> bool { + USE_ARENA.set(true).is_ok() +} + /// Claims the right to prove, initializing the backend on first use. /// /// Setup and the permit are handed out together because every prover entry point needs /// both: leanVM allows one proof at a time per process, and a second concurrent one panics. /// Proving is legal only while the returned guard is alive. -/// -/// Take it as late as the setup allows. Callers that only reshape their inputs claim it -/// right before the prove call, since conversion touches no prover state. Callers that -/// decode a stored proof claim it up front instead: decoding needs the aggregation -/// bytecode that setup installs, and a Type-2 decode without it returns `None`, which -/// surfaces as a bogus `DeserializationFailed`. -/// -/// Setup deliberately skips leanVM's `setup_prover`, which engages a bump arena that never -/// returns pages to the OS: `free` is a no-op and a phase reset only rewinds the per-thread -/// bump pointers, so a node's RSS ratchets to its high-water mark and stays there. -/// `setup_prover_without_arena` runs the same prover on the system allocator: slower, -/// bounded, identical proofs. -/// -/// The permit guards no data, so a poisoned lock is recovered rather than propagated: -/// one panicking prover must not brick every later proof. It is still an incident. fn acquire_prover() -> MutexGuard<'static, ()> { static PROVER_PERMIT: Mutex<()> = Mutex::new(()); let permit = PROVER_PERMIT.lock().unwrap_or_else(|poisoned| { @@ -48,7 +49,12 @@ fn acquire_prover() -> MutexGuard<'static, ()> { poisoned.into_inner() }); // Idempotent, and cheap after the first call (each step is `Once`/`OnceLock` guarded). - setup_prover_without_arena(); + // This also latches the allocator choice: `enable_prover_arena` is a no-op afterwards. + if *USE_ARENA.get_or_init(|| false) { + setup_prover(); + } else { + setup_prover_without_arena(); + } permit } diff --git a/crates/common/crypto/tests/arena.rs b/crates/common/crypto/tests/arena.rs new file mode 100644 index 00000000..eca96bc5 --- /dev/null +++ b/crates/common/crypto/tests/arena.rs @@ -0,0 +1,58 @@ +//! Coverage for the arena opt-in. +//! +//! The choice latches process-wide on the first prove, so this cannot live in the lib +//! test binary: enabling the arena there would change the allocator under every other +//! test, and whether the call won the latch would depend on test ordering. An +//! integration test gets its own process. + +use ethlambda_crypto::{ + aggregate_signatures, enable_prover_arena, + signature::{ValidatorPublicKey, ValidatorSignature}, + verify_aggregated_signature, +}; +use ethlambda_types::primitives::H256; +use lean_multisig::{xmss_key_gen_from_seed, xmss_sign}; +use ssz::Encode as _; + +/// Mirrors the lib tests' helper: a small active range keeps key generation fast. +fn keypair_and_signature( + seed: u64, + activation_epoch: u32, + signing_epoch: u32, + message: &H256, +) -> (ValidatorPublicKey, ValidatorSignature) { + let mut seed_bytes = [0u8; 32]; + seed_bytes[..8].copy_from_slice(&seed.to_le_bytes()); + + let num_active_slots = 64u64; + let (pk, sk) = xmss_key_gen_from_seed(seed_bytes, activation_epoch as u64, num_active_slots) + .expect("valid activation range"); + let sig = xmss_sign(&sk, signing_epoch, &message.0).expect("sign"); + + ( + ValidatorPublicKey::from_bytes(&pk.as_ssz_bytes()).unwrap(), + ValidatorSignature::from_bytes(&sig.as_ssz_bytes()).unwrap(), + ) +} + +#[test] +#[ignore = "too slow"] +fn aggregates_on_the_arena_when_enabled() { + assert!( + enable_prover_arena(), + "nothing has proved yet, so the choice must still be open" + ); + assert!( + !enable_prover_arena(), + "the choice is latched once set, so a second call reports no effect" + ); + + let message = H256::from([7u8; 32]); + let slot = 10u32; + let (pk, sig) = keypair_and_signature(1, 5, slot, &message); + + // Proves on the arena: the same round trip the lib tests run on the system allocator. + let proof = aggregate_signatures(vec![pk.clone()], vec![sig], &message, slot) + .expect("aggregation on the arena"); + verify_aggregated_signature(&proof, vec![pk], &message, slot).expect("verification"); +} From 31817c8d9f4d56d13ca2da98dfeacd5f89b2c2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:43:17 -0300 Subject: [PATCH 13/14] feat(cli): add --prover-arena to opt into the leanVM arena Gives operators the memory/throughput trade at boot: off keeps RSS bounded on the system allocator, on buys proving speed with memory that is never returned to the OS. Applied immediately after parsing, ahead of every other init, since the allocator choice latches on the first proof. The too-late branch warns rather than passing silently; it should be unreachable from here, but a future init that proves earlier would otherwise flip the node to the wrong allocator with no trace. --- bin/ethlambda/src/cli.rs | 11 +++++++++++ bin/ethlambda/src/main.rs | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/bin/ethlambda/src/cli.rs b/bin/ethlambda/src/cli.rs index 4bbe1683..61b58d3d 100644 --- a/bin/ethlambda/src/cli.rs +++ b/bin/ethlambda/src/cli.rs @@ -97,6 +97,17 @@ pub(crate) struct CliOptions { /// coverage. #[arg(long, default_value = "false")] pub(crate) enable_proposer_aggregation: bool, + /// Prove on leanVM's bump arena instead of the system allocator. + /// + /// Buys proving throughput with memory: the arena never returns pages to + /// the OS, so RSS ratchets up to the process's allocation high-water mark + /// and stays there for the lifetime of the node. Off by default so a + /// long-lived node keeps bounded memory; worth enabling on hosts with + /// memory to spare where proving latency is the constraint. + /// + /// Read once at startup: the allocator is fixed before the first proof. + #[arg(long, default_value = "false")] + pub(crate) prover_arena: bool, /// Maximum number of distinct attestations to pack when building a block. /// /// Bounds how many distinct `AttestationData` entries the proposer includes diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 091c715e..1060dc45 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -85,6 +85,15 @@ async fn main() -> eyre::Result<()> { #[cfg(feature = "shadow-integration")] init_shadow_cost(&options.shadow); + // Before anything can prove: the allocator choice latches on the first proof. + if options.prover_arena { + if ethlambda_crypto::enable_prover_arena() { + info!("Proving on leanVM's arena; memory grows to the high-water mark"); + } else { + warn!("--prover-arena had no effect: something proved before startup finished"); + } + } + // Initialize metrics ethlambda_blockchain::metrics::init(); ethlambda_blockchain::metrics::set_node_info("ethlambda", version::CLIENT_VERSION); From e2a66a84ac282c6282d22bb4ce64df742889c167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:08:58 -0300 Subject: [PATCH 14/14] refactor(crypto): initialize leanVM once at startup instead of lazily Setup was reached through `acquire_prover`, which coupled two unrelated things: a caller wanting the prove lock also triggered backend init, so the decoding paths had to hold the lock across work that needs no lock purely to get the bytecode compiled first. `init_leanvm(use_arena)` now does both halves in one place and binaries call it right after parsing arguments. `acquire_prover` is just the permit again, so every entry point takes it immediately before proving, and `ensure_verifier_ready` is gone: nothing needs a lazy hook anymore. The arena `OnceLock` goes too, since the choice is a parameter rather than latched global state. Ahead of the Hive test-driver branch in main, which verifies signatures. Tests that touch a proof call `init_leanvm` themselves, standing in for the startup call. This moves ~2.3s of bytecode compilation from the first proof or verification into startup. Nodes paid it either way; it is now off the first duty's critical path, at the cost of a slower boot for nodes that never prove. --- bin/ethlambda/src/main.rs | 15 ++- .../blockchain/tests/signature_spectests.rs | 4 + crates/common/crypto/src/lib.rs | 116 +++++++++--------- crates/common/crypto/tests/arena.rs | 18 +-- 4 files changed, 77 insertions(+), 76 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 1060dc45..943308c9 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -85,14 +85,13 @@ async fn main() -> eyre::Result<()> { #[cfg(feature = "shadow-integration")] init_shadow_cost(&options.shadow); - // Before anything can prove: the allocator choice latches on the first proof. - if options.prover_arena { - if ethlambda_crypto::enable_prover_arena() { - info!("Proving on leanVM's arena; memory grows to the high-water mark"); - } else { - warn!("--prover-arena had no effect: something proved before startup finished"); - } - } + // Compiles the aggregation bytecode and fixes the prover's allocator. Ahead of the + // test-driver branch below, which verifies signatures, and of every consensus path. + info!( + arena = options.prover_arena, + "Initializing leanVM prover and verifier" + ); + ethlambda_crypto::init_leanvm(options.prover_arena); // Initialize metrics ethlambda_blockchain::metrics::init(); diff --git a/crates/blockchain/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs index a11a58cc..d45f7c62 100644 --- a/crates/blockchain/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -20,6 +20,10 @@ const SUPPORTED_FIXTURE_FORMAT: &str = "verify_signatures_test"; const SKIP_TESTS: &[&str] = &[]; fn run(path: &Path) -> datatest_stable::Result<()> { + // These fixtures verify real signatures, so they need the backend a binary would set + // up at startup. Idempotent, so calling it per fixture costs nothing after the first. + ethlambda_crypto::init_leanvm(false); + let tests = VerifySignaturesTestVector::from_file(path)?; for (name, test) in tests.tests { diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index ecc94930..fbcc0cc3 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -1,3 +1,14 @@ +//! XMSS signature aggregation and verification, wrapping leanVM. +//! +//! [`init_leanvm`] must be called once at startup, before anything else here: it compiles +//! the aggregation bytecode and fixes the prover's allocator. Proving panics without it, +//! and decoding a stored proof misreports as a corrupt proof. Binaries call it straight +//! after argument parsing; tests that touch a proof call it themselves. +//! +//! Everything else assumes it has run. Aggregation produces Type-1 proofs (one message, +//! one slot) and Type-2 proofs (several messages merged); both travel without their +//! participant pubkeys, which a receiver rebuilds from its own validator registry. + use ethlambda_types::{block::ByteList512KiB, primitives::H256}; use crate::signature::{ @@ -9,7 +20,7 @@ use lean_multisig::{ setup_prover_without_arena, setup_verifier, split_multi_message_aggregate, verify_multi_message_aggregate, verify_single_message_aggregate, }; -use std::sync::{Mutex, MutexGuard, OnceLock}; +use std::sync::{Mutex, MutexGuard}; use thiserror::Error; use tracing::error; @@ -21,46 +32,41 @@ pub mod shadow_cost; /// log(1/rate) for the WHIR commitment scheme used inside the aggregation prover. const LOG_INV_RATE: usize = 2; -/// Whether to prove on leanVM's arena. Latched by the first [`acquire_prover`]. -static USE_ARENA: OnceLock = OnceLock::new(); - -/// Opts the prover into leanVM's bump arena: faster proving, unbounded RSS. +/// Initializes the leanVM backend. Call once at startup, before any other function here. /// -/// The arena never returns pages to the OS, so a node's memory ratchets to its -/// allocation high-water mark and stays there. Off by default; worth it only where -/// memory is plentiful and proving latency matters. +/// Everything downstream assumes this has run: proving panics without the aggregation +/// bytecode, and decoding a stored proof needs it too (a Type-2 decode rebuilds the +/// bytecode claim and returns `None` without it, which surfaces as a bogus +/// `DeserializationFailed`). Doing it up front keeps the cost off the first duty. /// -/// Must be called before the first prove. Returns `false` if the choice was already -/// latched, meaning the call had no effect. -#[must_use = "the arena is not enabled if the choice was already latched"] -pub fn enable_prover_arena() -> bool { - USE_ARENA.set(true).is_ok() -} - -/// Claims the right to prove, initializing the backend on first use. +/// `use_arena` picks the prover's allocator. leanVM's arena is faster but never returns +/// pages to the OS, so a node's RSS ratchets to its allocation high-water mark and stays +/// there; the system allocator trades throughput for bounded memory. /// -/// Setup and the permit are handed out together because every prover entry point needs -/// both: leanVM allows one proof at a time per process, and a second concurrent one panics. -/// Proving is legal only while the returned guard is alive. -fn acquire_prover() -> MutexGuard<'static, ()> { - static PROVER_PERMIT: Mutex<()> = Mutex::new(()); - let permit = PROVER_PERMIT.lock().unwrap_or_else(|poisoned| { - error!("a previous proving job panicked while holding the permit; continuing"); - poisoned.into_inner() - }); - // Idempotent, and cheap after the first call (each step is `Once`/`OnceLock` guarded). - // This also latches the allocator choice: `enable_prover_arena` is a no-op afterwards. - if *USE_ARENA.get_or_init(|| false) { +/// Idempotent: every step is `Once`/`OnceLock` guarded. +pub fn init_leanvm(use_arena: bool) { + if use_arena { setup_prover(); } else { setup_prover_without_arena(); } - permit + setup_verifier(); } -/// Needed before any decode, not just before verifying. -pub fn ensure_verifier_ready() { - setup_verifier(); +/// Claims the exclusive right to prove. +/// +/// leanVM allows one proof at a time per process; a second concurrent one panics. +/// Proving is legal only while the returned guard is alive, so take it immediately +/// before the prove call: decoding and argument conversion need no permit. +/// +/// The permit guards no data, so a poisoned lock is recovered rather than propagated: +/// one panicking prover must not brick every later proof. It is still an incident. +fn acquire_prover() -> MutexGuard<'static, ()> { + static PROVER_PERMIT: Mutex<()> = Mutex::new(()); + PROVER_PERMIT.lock().unwrap_or_else(|poisoned| { + error!("a previous proving job panicked while holding the permit; continuing"); + poisoned.into_inner() + }) } /// Error type for signature aggregation operations. @@ -270,10 +276,6 @@ pub fn aggregate_mixed( return Ok(dummy); } - // Held from here rather than from the aggregate call: decoding the children needs - // the aggregation bytecode that `acquire_prover` installs. - let _permit = acquire_prover(); - let children_native: Vec = children .into_iter() .enumerate() @@ -286,6 +288,8 @@ pub fn aggregate_mixed( .map(|(pk, sig)| (pk.into_inner(), sig.into_inner())) .collect(); + let _permit = acquire_prover(); + let proof = aggregate_single_message_signatures( &children_native, raw_xmss, @@ -326,16 +330,14 @@ pub fn aggregate_proofs( return Ok(dummy); } - // Held from here rather than from the aggregate call: decoding the children needs - // the aggregation bytecode that `acquire_prover` installs. - let _permit = acquire_prover(); - let children_native: Vec = children .into_iter() .enumerate() .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) .collect::>()?; + let _permit = acquire_prover(); + let proof = aggregate_single_message_signatures( &children_native, vec![], @@ -369,7 +371,6 @@ pub fn verify_aggregated_signature( crate::shadow_cost::sleep(crate::shadow_cost::verify_delay(verify_n)); return Ok(()); } - ensure_verifier_ready(); let lean_pubkeys = into_lean_pubkeys(public_keys); let sig = LMType1::from_bytes_without_pubkeys(proof_data.iter().as_slice(), lean_pubkeys) @@ -423,16 +424,14 @@ pub fn merge_type_1s_into_type_2( return Ok(dummy); } - // Held from here rather than from the merge call: decoding the inputs needs - // the aggregation bytecode that `acquire_prover` installs. - let _permit = acquire_prover(); - let type_1s_native: Vec = type_1s .into_iter() .enumerate() .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) .collect::>()?; + let _permit = acquire_prover(); + let merged = merge_single_message_aggregates(type_1s_native, LOG_INV_RATE) .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; @@ -462,8 +461,6 @@ pub fn verify_type_2_signature( return Ok(()); } - ensure_verifier_ready(); - let pubkeys_per_info: Vec> = pubkeys_per_component .into_iter() .map(into_lean_pubkeys) @@ -519,11 +516,6 @@ pub fn split_type_2_by_message( )); } - // Held from here rather than from the split call: the Type-2 decode below rebuilds - // the bytecode claim, so it needs the aggregation bytecode that `acquire_prover` - // installs. Without it the decode returns `None` and looks like a corrupt proof. - let _permit = acquire_prover(); - let pubkeys_per_info: Vec> = pubkeys_per_component .into_iter() .map(into_lean_pubkeys) @@ -544,6 +536,8 @@ pub fn split_type_2_by_message( _ => return Err(AggregationError::MultipleMessages), }; + let _permit = acquire_prover(); + let component = split_multi_message_aggregate(type_2, index, LOG_INV_RATE) .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; @@ -584,24 +578,31 @@ mod tests { (validator_pk, validator_sig) } + /// Stands in for the startup call every binary makes. Without it the prover panics + /// on the missing aggregation bytecode. + fn init() { + init_leanvm(false); + } + #[test] #[ignore = "slow: compiles the leanVM aggregation bytecode (needs a release-sized stack)"] fn test_setup_is_idempotent() { // Should not panic when called multiple times. The first call compiles // the self-referential aggregation bytecode; subsequent calls are cheap // (`OnceLock::get_or_init`). - // + init_leanvm(false); + init_leanvm(false); + // The permit is dropped between acquisitions: it is not reentrant, so holding // both at once would deadlock. That also covers release-on-drop. drop(acquire_prover()); drop(acquire_prover()); - ensure_verifier_ready(); - ensure_verifier_ready(); } #[test] #[ignore = "too slow"] fn test_aggregate_single_signature() { + init(); let message = H256::from([42u8; 32]); let slot = 10u32; let activation_epoch = 5u32; @@ -626,6 +627,7 @@ mod tests { #[test] #[ignore = "too slow"] fn test_aggregate_multiple_signatures() { + init(); let message = H256::from([42u8; 32]); let slot = 15u32; @@ -662,6 +664,7 @@ mod tests { #[test] #[ignore = "too slow"] fn test_verify_wrong_message_fails() { + init(); let message = H256::from([42u8; 32]); let wrong_message = H256::from([43u8; 32]); let slot = 10u32; @@ -683,6 +686,7 @@ mod tests { #[test] #[ignore = "too slow"] fn test_verify_wrong_slot_fails() { + init(); let message = H256::from([42u8; 32]); let slot = 10u32; let wrong_slot = 11u32; @@ -708,6 +712,7 @@ mod tests { #[test] #[ignore = "too slow"] fn test_verify_wrong_pubkey_set_fails() { + init(); let message = H256::from([42u8; 32]); let slot = 10u32; @@ -730,6 +735,7 @@ mod tests { #[test] #[ignore = "too slow"] fn test_type_2_merge_verify_split_round_trip() { + init(); let msg_a = H256::from([0x11u8; 32]); let msg_b = H256::from([0x22u8; 32]); let slot_a: u32 = 7; diff --git a/crates/common/crypto/tests/arena.rs b/crates/common/crypto/tests/arena.rs index eca96bc5..1655b71d 100644 --- a/crates/common/crypto/tests/arena.rs +++ b/crates/common/crypto/tests/arena.rs @@ -1,12 +1,11 @@ -//! Coverage for the arena opt-in. +//! Coverage for the arena path of [`init_leanvm`]. //! -//! The choice latches process-wide on the first prove, so this cannot live in the lib -//! test binary: enabling the arena there would change the allocator under every other -//! test, and whether the call won the latch would depend on test ordering. An +//! leanVM's arena engages process-wide and cannot be disengaged, so this cannot live in +//! the lib test binary: it would change the allocator under every other test. An //! integration test gets its own process. use ethlambda_crypto::{ - aggregate_signatures, enable_prover_arena, + aggregate_signatures, init_leanvm, signature::{ValidatorPublicKey, ValidatorSignature}, verify_aggregated_signature, }; @@ -38,14 +37,7 @@ fn keypair_and_signature( #[test] #[ignore = "too slow"] fn aggregates_on_the_arena_when_enabled() { - assert!( - enable_prover_arena(), - "nothing has proved yet, so the choice must still be open" - ); - assert!( - !enable_prover_arena(), - "the choice is latched once set, so a second call reports no effect" - ); + init_leanvm(true); let message = H256::from([7u8; 32]); let slot = 10u32;