diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index adb2c381..ea8e60ea 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -23,7 +23,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -35,7 +35,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -76,22 +76,13 @@ dependencies = [ [[package]] name = "air" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "multilinear-toolkit", - "p3-util 0.3.0", + "backend", "tracing", "utils", ] -[[package]] -name = "air" -version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lean-vm-simple#e06cba2e214879c00c7fbc0e5b12908ddfcba588" -dependencies = [ - "p3-field 0.3.0", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -122,7 +113,7 @@ dependencies = [ "ruint", "rustc-hash", "serde", - "sha3", + "sha3 0.10.8", "tiny-keccak", ] @@ -535,6 +526,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -636,14 +636,18 @@ dependencies = [ [[package]] name = "backend" -version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lean-vm-simple#e06cba2e214879c00c7fbc0e5b12908ddfcba588" -dependencies = [ - "fiat-shamir", - "itertools 0.14.0", - "p3-field 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" +dependencies = [ + "mt-air", + "mt-fiat-shamir", + "mt-field", + "mt-koala-bear", + "mt-poly", + "mt-sumcheck", + "mt-symetric", + "mt-utils", + "mt-whir", "rayon", "tracing", ] @@ -697,15 +701,6 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -778,6 +773,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bls" version = "0.0.0" @@ -910,7 +914,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", ] [[package]] @@ -920,7 +935,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -948,7 +963,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -994,19 +1009,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] -name = "colorchoice" -version = "1.0.4" +name = "cobs" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] [[package]] -name = "colored" -version = "3.0.0" +name = "colorchoice" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" -dependencies = [ - "windows-sys 0.59.0", -] +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -1024,7 +1039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -1035,6 +1050,12 @@ 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" @@ -1061,16 +1082,6 @@ dependencies = [ "unicode-xid 0.2.6", ] -[[package]] -name = "constraints-folder" -version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lean-vm-simple#e06cba2e214879c00c7fbc0e5b12908ddfcba588" -dependencies = [ - "air 0.3.0", - "fiat-shamir", - "p3-field 0.3.0", -] - [[package]] name = "containers" version = "0.0.0" @@ -1082,6 +1093,7 @@ dependencies = [ "hex", "metrics", "pretty_assertions", + "rayon", "rstest", "serde", "serde_json", @@ -1138,6 +1150,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1207,6 +1228,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1223,7 +1253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", @@ -1335,7 +1365,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "zeroize", ] @@ -1418,12 +1448,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", +] + [[package]] name = "discv5" version = "0.10.2" @@ -1571,6 +1612,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "enr" version = "0.13.0" @@ -1586,7 +1639,7 @@ dependencies = [ "log", "rand 0.8.5", "serde", - "sha3", + "sha3 0.10.8", "zeroize", ] @@ -1816,17 +1869,6 @@ 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/fiat-shamir.git?branch=lean-vm-simple#9d4dc22f06cfa65f15bf5f1b07912a64c7feff0f" -dependencies = [ - "p3-challenger 0.3.0", - "p3-field 0.3.0", - "p3-koala-bear 0.3.0", - "serde", -] - [[package]] name = "find-msvc-tools" version = "0.1.5" @@ -1872,8 +1914,8 @@ dependencies = [ "containers", "env-config", "metrics", - "rand 0.9.2", - "rand_chacha 0.9.0", + "rand 0.10.0", + "rand_chacha 0.10.0", "serde", "serde_json", "ssz", @@ -2058,11 +2100,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -2115,6 +2171,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2172,6 +2237,20 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version 0.4.1", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -2356,6 +2435,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -2525,6 +2613,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2805,7 +2899,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", ] [[package]] @@ -2824,20 +2928,6 @@ 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/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" -dependencies = [ - "clap", - "lean_vm", - "multilinear-toolkit", - "p3-koala-bear 0.3.0", - "rand 0.9.2", - "rec_aggregation", - "whir-p3", -] - [[package]] name = "lean_client" version = "0.1.0" @@ -2868,99 +2958,113 @@ dependencies = [ [[package]] name = "lean_compiler" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "air 0.1.0", + "air", + "backend", "lean_vm", - "lookup", - "multilinear-toolkit", - "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", "pest", "pest_derive", - "rand 0.9.2", + "rand 0.10.0", "sub_protocols", "tracing", "utils", - "whir-p3", ] [[package]] name = "lean_prover" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "air 0.1.0", + "air", + "backend", "itertools 0.14.0", "lean_compiler", "lean_vm", - "lookup", - "multilinear-toolkit", - "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", "pest", "pest_derive", - "rand 0.9.2", + "rand 0.10.0", "sub_protocols", "tracing", "utils", - "whir-p3", - "witness_generation", ] [[package]] name = "lean_vm" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "air 0.1.0", - "colored", - "derive_more", + "air", + "backend", "itertools 0.14.0", - "lookup", - "multilinear-toolkit", - "num_enum", - "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", + "leansig_wrapper", "pest", "pest_derive", - "rand 0.9.2", - "sub_protocols", - "thiserror 2.0.17", + "rand 0.10.0", + "serde", "tracing", "utils", - "whir-p3", ] [[package]] name = "leansig" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanSig?rev=73bedc26ed961b110df7ac2e234dc11361a4bf25#73bedc26ed961b110df7ac2e234dc11361a4bf25" +source = "git+https://github.com/leanEthereum/leanSig?branch=devnet4#5cc7e37480362f94e86695428a9ceb9a96b66b97" dependencies = [ "dashmap", "ethereum_ssz", "num-bigint", "num-traits", - "p3-baby-bear 0.4.1", - "p3-field 0.4.1", - "p3-koala-bear 0.4.1", - "p3-symmetric 0.4.1", - "rand 0.9.2", + "p3-baby-bear", + "p3-field", + "p3-koala-bear", + "p3-symmetric", + "rand 0.10.0", + "rayon", + "serde", + "sha3 0.10.8", + "thiserror 2.0.17", +] + +[[package]] +name = "leansig_fast_keygen" +version = "0.1.0" +source = "git+https://github.com/TomWambsgans/leanSig?branch=devnet4-fast-keygen#5b86867a4d3c1d4a8add840f70fa047ea1506188" +dependencies = [ + "dashmap", + "ethereum_ssz", + "num-bigint", + "num-traits", + "p3-baby-bear", + "p3-field", + "p3-koala-bear", + "p3-symmetric", + "rand 0.10.0", "rayon", "serde", - "sha3", + "sha3 0.10.8", "thiserror 2.0.17", ] +[[package]] +name = "leansig_wrapper" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" +dependencies = [ + "backend", + "ethereum_ssz", + "leansig", + "leansig_fast_keygen", + "p3-field", + "rand 0.10.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lib-c" version = "0.13.0" @@ -3441,21 +3545,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "lookup" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" -dependencies = [ - "multilinear-toolkit", - "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", - "tracing", - "utils", - "whir-p3", -] - [[package]] name = "lru" version = "0.12.5" @@ -3471,6 +3560,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lz4_flex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" +dependencies = [ + "twox-hash", +] + [[package]] name = "match-lookup" version = "0.1.1" @@ -3578,63 +3676,178 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" [[package]] -name = "multiaddr" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b36f567c7099511fa8612bbbb52dda2419ce0bdbacf31714e3a5ffdb766d3bd" +name = "mt-air" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "arrayref", - "byteorder", - "data-encoding", - "log", - "multibase", - "multihash 0.17.0", - "percent-encoding", - "serde", - "static_assertions", - "unsigned-varint 0.7.2", - "url", + "mt-field", + "mt-poly", ] [[package]] -name = "multiaddr" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +name = "mt-fiat-shamir" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "arrayref", - "byteorder", - "data-encoding", - "libp2p-identity 0.2.13", - "multibase", - "multihash 0.19.3", - "percent-encoding", + "mt-field", + "mt-koala-bear", + "mt-symetric", + "mt-utils", + "rayon", "serde", - "static_assertions", - "unsigned-varint 0.8.0", - "url", ] [[package]] -name = "multibase" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +name = "mt-field" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "base-x", - "base256emoji", - "data-encoding", - "data-encoding-macro", + "itertools 0.14.0", + "mt-utils", + "num-bigint", + "paste", + "rand 0.10.0", + "rayon", + "serde", + "tracing", ] [[package]] -name = "multihash" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" +name = "mt-koala-bear" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "core2", - "multihash-derive", + "itertools 0.14.0", + "mt-field", + "mt-utils", + "num-bigint", + "paste", + "rand 0.10.0", + "rayon", + "serde", + "tracing", +] + +[[package]] +name = "mt-poly" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" +dependencies = [ + "itertools 0.14.0", + "mt-field", + "mt-utils", + "rand 0.10.0", + "rayon", + "serde", +] + +[[package]] +name = "mt-sumcheck" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" +dependencies = [ + "mt-air", + "mt-fiat-shamir", + "mt-field", + "mt-poly", + "rayon", + "tracing", +] + +[[package]] +name = "mt-symetric" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" +dependencies = [ + "mt-field", + "mt-koala-bear", + "rayon", +] + +[[package]] +name = "mt-utils" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" +dependencies = [ + "serde", +] + +[[package]] +name = "mt-whir" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" +dependencies = [ + "itertools 0.14.0", + "mt-fiat-shamir", + "mt-field", + "mt-koala-bear", + "mt-poly", + "mt-sumcheck", + "mt-symetric", + "mt-utils", + "rand 0.10.0", + "rayon", + "tracing", +] + +[[package]] +name = "multiaddr" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b36f567c7099511fa8612bbbb52dda2419ce0bdbacf31714e3a5ffdb766d3bd" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "log", + "multibase", + "multihash 0.17.0", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint 0.7.2", + "url", +] + +[[package]] +name = "multiaddr" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "libp2p-identity 0.2.13", + "multibase", + "multihash 0.19.3", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint 0.8.0", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" +dependencies = [ + "core2", + "multihash-derive", "unsigned-varint 0.7.2", ] @@ -3662,22 +3875,6 @@ dependencies = [ "synstructure 0.12.6", ] -[[package]] -name = "multilinear-toolkit" -version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lean-vm-simple#e06cba2e214879c00c7fbc0e5b12908ddfcba588" -dependencies = [ - "air 0.3.0", - "backend", - "constraints-folder", - "fiat-shamir", - "p3-field 0.3.0", - "p3-util 0.3.0", - "rayon", - "sumcheck", - "tracing", -] - [[package]] name = "multistream-select" version = "0.12.1" @@ -3791,7 +3988,7 @@ dependencies = [ "num-bigint", "num-traits", "parking_lot", - "rand 0.9.2", + "rand 0.10.0", "serde", "serde_yaml", "sha2 0.10.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3891,28 +4088,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2 1.0.103", - "quote 1.0.42", - "syn 2.0.111", -] - [[package]] name = "object" version = "0.37.3" @@ -3955,355 +4130,170 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p3-baby-bear" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" -dependencies = [ - "p3-field 0.3.0", - "p3-mds 0.3.0", - "p3-monty-31 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "rand 0.9.2", -] - -[[package]] -name = "p3-baby-bear" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" -dependencies = [ - "p3-challenger 0.4.1", - "p3-field 0.4.1", - "p3-mds 0.4.1", - "p3-monty-31 0.4.1", - "p3-poseidon2 0.4.1", - "p3-symmetric 0.4.1", - "rand 0.9.2", -] - -[[package]] -name = "p3-challenger" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ - "p3-field 0.3.0", - "p3-maybe-rayon 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "tracing", + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.0", ] [[package]] name = "p3-challenger" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" -dependencies = [ - "p3-field 0.4.1", - "p3-maybe-rayon 0.4.1", - "p3-monty-31 0.4.1", - "p3-symmetric 0.4.1", - "p3-util 0.4.1", - "tracing", -] - -[[package]] -name = "p3-commit" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" -dependencies = [ - "itertools 0.14.0", - "p3-challenger 0.3.0", - "p3-dft 0.3.0", - "p3-field 0.3.0", - "p3-matrix 0.3.0", - "p3-util 0.3.0", - "serde", -] - -[[package]] -name = "p3-dft" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ - "itertools 0.14.0", - "p3-field 0.3.0", - "p3-matrix 0.3.0", - "p3-maybe-rayon 0.3.0", - "p3-util 0.3.0", + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", "tracing", ] [[package]] name = "p3-dft" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" -dependencies = [ - "itertools 0.14.0", - "p3-field 0.4.1", - "p3-matrix 0.4.1", - "p3-maybe-rayon 0.4.1", - "p3-util 0.4.1", - "spin", - "tracing", -] - -[[package]] -name = "p3-field" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ "itertools 0.14.0", - "num-bigint", - "p3-maybe-rayon 0.3.0", - "p3-util 0.3.0", - "paste", - "rand 0.9.2", - "serde", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin 0.10.0", "tracing", ] [[package]] name = "p3-field" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ "itertools 0.14.0", "num-bigint", - "p3-maybe-rayon 0.4.1", - "p3-util 0.4.1", + "p3-maybe-rayon", + "p3-util", "paste", - "rand 0.9.2", + "rand 0.10.0", "serde", "tracing", ] -[[package]] -name = "p3-interpolation" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" -dependencies = [ - "p3-field 0.3.0", - "p3-matrix 0.3.0", - "p3-maybe-rayon 0.3.0", - "p3-util 0.3.0", -] - [[package]] name = "p3-koala-bear" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" -dependencies = [ - "itertools 0.14.0", - "num-bigint", - "p3-field 0.3.0", - "p3-monty-31 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", - "serde", -] - -[[package]] -name = "p3-koala-bear" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" -dependencies = [ - "p3-challenger 0.4.1", - "p3-field 0.4.1", - "p3-monty-31 0.4.1", - "p3-poseidon2 0.4.1", - "p3-symmetric 0.4.1", - "rand 0.9.2", -] - -[[package]] -name = "p3-matrix" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ - "itertools 0.14.0", - "p3-field 0.3.0", - "p3-maybe-rayon 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", - "serde", - "tracing", - "transpose", + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.0", ] [[package]] name = "p3-matrix" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ "itertools 0.14.0", - "p3-field 0.4.1", - "p3-maybe-rayon 0.4.1", - "p3-util 0.4.1", - "rand 0.9.2", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.0", "serde", "tracing", - "transpose", ] [[package]] name = "p3-maybe-rayon" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" -dependencies = [ - "rayon", -] - -[[package]] -name = "p3-maybe-rayon" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" - -[[package]] -name = "p3-mds" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" -dependencies = [ - "p3-dft 0.3.0", - "p3-field 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", -] +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" [[package]] name = "p3-mds" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" -dependencies = [ - "p3-dft 0.4.1", - "p3-field 0.4.1", - "p3-symmetric 0.4.1", - "p3-util 0.4.1", - "rand 0.9.2", -] - -[[package]] -name = "p3-merkle-tree" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" -dependencies = [ - "itertools 0.14.0", - "p3-commit", - "p3-field 0.3.0", - "p3-matrix 0.3.0", - "p3-maybe-rayon 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", - "serde", - "tracing", -] - -[[package]] -name = "p3-monty-31" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ - "itertools 0.14.0", - "num-bigint", - "p3-dft 0.3.0", - "p3-field 0.3.0", - "p3-matrix 0.3.0", - "p3-maybe-rayon 0.3.0", - "p3-mds 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "paste", - "rand 0.9.2", - "serde", - "tracing", - "transpose", + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.0", ] [[package]] name = "p3-monty-31" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ "itertools 0.14.0", "num-bigint", - "p3-dft 0.4.1", - "p3-field 0.4.1", - "p3-matrix 0.4.1", - "p3-maybe-rayon 0.4.1", - "p3-mds 0.4.1", - "p3-poseidon2 0.4.1", - "p3-symmetric 0.4.1", - "p3-util 0.4.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", "paste", - "rand 0.9.2", + "rand 0.10.0", "serde", - "spin", + "spin 0.10.0", "tracing", - "transpose", ] [[package]] -name = "p3-poseidon2" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" +name = "p3-poseidon1" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ - "p3-field 0.3.0", - "p3-mds 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", + "p3-field", + "p3-symmetric", + "rand 0.10.0", ] [[package]] name = "p3-poseidon2" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" -dependencies = [ - "p3-field 0.4.1", - "p3-mds 0.4.1", - "p3-symmetric 0.4.1", - "p3-util 0.4.1", - "rand 0.9.2", -] - -[[package]] -name = "p3-symmetric" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ - "itertools 0.14.0", - "p3-field 0.3.0", - "serde", + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.0", ] [[package]] name = "p3-symmetric" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ "itertools 0.14.0", - "p3-field 0.4.1", + "p3-field", + "p3-util", "serde", ] [[package]] name = "p3-util" -version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" -dependencies = [ - "rayon", - "serde", -] - -[[package]] -name = "p3-util" -version = "0.4.1" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +version = "0.5.1" +source = "git+https://github.com/Plonky3/Plonky3.git#090b134a7e4f73191dad8849c410fcc38967a814" dependencies = [ "serde", + "transpose", ] [[package]] @@ -4515,7 +4505,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4527,7 +4517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4538,6 +4528,19 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -4572,6 +4575,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2 1.0.103", + "syn 2.0.111", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -4860,6 +4873,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -4888,6 +4907,17 @@ dependencies = [ "serde", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4908,6 +4938,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.0", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -4927,6 +4967,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -4990,30 +5036,22 @@ dependencies = [ [[package]] name = "rec_aggregation" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "air 0.1.0", - "bincode", - "ethereum_ssz", - "hex", + "air", + "backend", "lean_compiler", "lean_prover", "lean_vm", - "leansig", - "lookup", - "multilinear-toolkit", - "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", + "leansig_wrapper", + "lz4_flex", + "postcard", + "rand 0.10.0", "serde", - "serde_json", + "sha3 0.11.0", "sub_protocols", "tracing", "utils", - "whir-p3", ] [[package]] @@ -5586,7 +5624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5596,7 +5634,7 @@ version = "0.10.9" source = "git+https://github.com/grandinetech/universal-precompiles.git?tag=sha2-v0.10.9-up.2#7d57ea01cd5fe5f6458142ce6ac269cc44b425bd" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", "ziskos", ] @@ -5608,7 +5646,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest 0.10.7", - "keccak", + "keccak 0.1.5", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.2", + "keccak 0.2.0", ] [[package]] @@ -5723,6 +5771,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spin" version = "0.10.0" @@ -5868,15 +5925,12 @@ dependencies = [ [[package]] name = "sub_protocols" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" dependencies = [ - "derive_more", - "lookup", - "multilinear-toolkit", - "p3-util 0.3.0", + "backend", + "lean_vm", "tracing", "utils", - "whir-p3", ] [[package]] @@ -5885,20 +5939,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "sumcheck" -version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lean-vm-simple#e06cba2e214879c00c7fbc0e5b12908ddfcba588" -dependencies = [ - "air 0.3.0", - "backend", - "constraints-folder", - "fiat-shamir", - "p3-field 0.3.0", - "p3-util 0.3.0", - "rayon", -] - [[package]] name = "syn" version = "0.15.44" @@ -6297,9 +6337,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -6320,26 +6360,14 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", ] -[[package]] -name = "tracing-forest" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3298fe855716711a00474eceb89cc7dc254bbe67f6bc4afafdeec5f0c538771c" -dependencies = [ - "smallvec", - "thiserror 2.0.17", - "tracing", - "tracing-subscriber", -] - [[package]] name = "tracing-forest" version = "0.3.0" @@ -6366,9 +6394,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -6413,6 +6441,12 @@ name = "try_from_iterator" version = "0.0.0" source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +[[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.19.0" @@ -6522,7 +6556,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -6581,16 +6615,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" -dependencies = [ - "multilinear-toolkit", - "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01#fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" +dependencies = [ + "backend", "tracing", - "tracing-forest 0.3.0", + "tracing-forest", "tracing-subscriber", ] @@ -6615,6 +6644,7 @@ dependencies = [ "ethereum-types", "fork_choice", "metrics", + "serde", "serde_yaml", "ssz", "tracing", @@ -6683,7 +6713,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -6744,6 +6783,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.12.1", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "semver 1.0.27", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -6773,33 +6846,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "whir-p3" -version = "0.1.0" -source = "git+https://github.com/TomWambsgans/whir-p3?branch=lean-vm-simple#f74bc197415a597b1ca316a4ee207f43c8adee85" -dependencies = [ - "itertools 0.14.0", - "multilinear-toolkit", - "p3-baby-bear 0.3.0", - "p3-challenger 0.3.0", - "p3-commit", - "p3-dft 0.3.0", - "p3-field 0.3.0", - "p3-interpolation", - "p3-koala-bear 0.3.0", - "p3-matrix 0.3.0", - "p3-maybe-rayon 0.3.0", - "p3-merkle-tree", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "rand 0.9.2", - "rayon", - "thiserror 2.0.17", - "tracing", - "tracing-forest 0.2.0", - "tracing-subscriber", -] - [[package]] name = "widestring" version = "1.2.1" @@ -6934,15 +6980,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -7173,29 +7210,91 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] -name = "witness_generation" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "air 0.1.0", - "derive_more", - "lean_compiler", - "lean_vm", - "lookup", - "multilinear-toolkit", - "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0", - "p3-monty-31 0.3.0", - "p3-poseidon2 0.3.0", - "p3-symmetric 0.3.0", - "p3-util 0.3.0", - "pest", - "pest_derive", - "rand 0.9.2", - "sub_protocols", - "tracing", - "utils", - "whir-p3", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.12.1", + "prettyplease", + "syn 2.0.111", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.12.1", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.12.1", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid 0.2.6", + "wasmparser", ] [[package]] @@ -7266,11 +7365,12 @@ dependencies = [ "ethereum-types", "ethereum_ssz", "hex", - "lean-multisig", "leansig", + "leansig_wrapper", "metrics", - "rand 0.9.2", - "rand_chacha 0.9.0", + "rand 0.10.0", + "rand_chacha 0.10.0", + "rec_aggregation", "serde", "ssz", "typenum", diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index 02af6e16..7683714b 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -246,8 +246,9 @@ features = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6 hex = "0.4.3" http_api_utils = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" } k256 = "0.13" -lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig", rev = "e4474138487eeb1ed7c2e1013674fe80ac9f3165" } -leansig = { git = "https://github.com/leanEthereum/leanSig", rev = "73bedc26ed961b110df7ac2e234dc11361a4bf25" } +rec_aggregation = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" } +leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" } +leansig_wrapper = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" } libp2p = { version = "0.56.0", default-features = false, features = [ 'dns', 'gossipsub', @@ -269,8 +270,9 @@ parking_lot = "0.12" paste = "1.0.15" pretty_assertions = "1.4" prometheus = "0.14" -rand = "0.9" -rand_chacha = "0.9" +rand = "0.10" +rand_chacha = "0.10" +rayon = "1" rstest = "0.18" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index 7198cd18..d31818f0 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -9,6 +9,7 @@ bls = { workspace = true } env-config = { workspace = true } hex = { workspace = true } metrics = { workspace = true } +rayon = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 263ea32a..3524598f 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -17,15 +17,14 @@ pub type AttestationSignatures = PersistentList, signatures: impl IntoIterator, message: H256, - epoch: u32, + slot: u32, + log_inv_rate: usize, ) -> Result { Ok(Self { participants, - proof_data: AggregatedSignature::aggregate(public_keys, signatures, message, epoch)?, + proof_data: AggregatedSignature::aggregate( + public_keys, + signatures, + message, + slot, + log_inv_rate, + )?, + }) + } + + /// Aggregate with optional recursive child proofs for proof compaction. + /// + /// `children` is a list of `(public_keys_covered, child_proof)` pairs where + /// each child proof previously aggregated the listed keys. + pub fn aggregate_with_children( + participants: AggregationBits, + children: &[(&[PublicKey], &AggregatedSignatureProof)], + public_keys: impl IntoIterator, + signatures: impl IntoIterator, + message: H256, + slot: u32, + log_inv_rate: usize, + ) -> Result { + let xmss_children: Vec<(&[PublicKey], &AggregatedSignature)> = children + .iter() + .map(|(pks, proof)| (*pks, &proof.proof_data)) + .collect(); + Ok(Self { + participants, + proof_data: AggregatedSignature::aggregate_with_children( + &xmss_children, + public_keys, + signatures, + message, + slot, + log_inv_rate, + )?, }) } @@ -52,9 +88,9 @@ impl AggregatedSignatureProof { &self, public_keys: impl IntoIterator, message: H256, - epoch: u32, + slot: u32, ) -> Result<()> { - self.proof_data.verify(public_keys, message, epoch) + self.proof_data.verify(public_keys, message, slot) } } @@ -83,10 +119,6 @@ impl AggregationBits { let mut bits = BitList::::with_length((max_id + 1) as usize); - for i in 0..=max_id { - bits.set(i as usize, false); - } - for &i in indices { bits.set(i as usize, true); } @@ -223,7 +255,6 @@ impl AggregatedAttestation { } /// Aggregated attestation bundled with aggregated signature proof. -/// Structure matches ream/zeam for devnet-3 interoperability. #[derive(Clone, Debug, Ssz)] pub struct SignedAggregatedAttestation { /// The attestation data being attested to. diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 7c8ff853..2fb69b90 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -1,6 +1,7 @@ -use crate::{Attestation, Slot, State}; +use crate::{Slot, State}; use anyhow::{Context, Result, ensure}; use metrics::METRICS; +use rayon::prelude::*; use serde::{Deserialize, Serialize}; use ssz::{H256, Ssz, SszHash}; use xmss::Signature; @@ -38,16 +39,6 @@ pub struct Block { pub body: BlockBody, } -/// Bundle containing a block and the proposer's attestation. -#[derive(Clone, Debug, Ssz, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BlockWithAttestation { - /// The proposed block message. - pub block: Block, - /// The proposer's attestation corresponding to this block. - pub proposer_attestation: Attestation, -} - // todo(containers): default implementation doesn't make sense here #[derive(Debug, Clone, Ssz, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] @@ -57,77 +48,30 @@ pub struct BlockSignatures { pub proposer_signature: Signature, } -/// Envelope carrying a block, an attestation from proposer, and aggregated signatures. +/// Signed block for devnet4: block body + aggregated signatures. +/// Proposer attestation is no longer embedded in the block message. #[derive(Clone, Debug, Ssz, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SignedBlockWithAttestation { - /// The block plus an attestation from proposer being signed. - pub message: BlockWithAttestation, - /// Aggregated signature payload for the block. - /// - /// Signatures remain in attestation order followed by the proposer signature. - pub signature: BlockSignatures, -} - -/// Legacy signed block structure (kept for backwards compatibility). -#[derive(Clone, Debug, Ssz)] pub struct SignedBlock { - pub message: Block, - pub signature: Signature, + /// The proposed block. + pub block: Block, + /// Aggregated signature payload (attestation proofs + proposer signature). + #[serde(alias = "signatures")] + pub signature: BlockSignatures, } -impl SignedBlockWithAttestation { +impl SignedBlock { /// Verify all XMSS signatures in this signed block. /// - /// This function ensures that every attestation included in the block - /// (both on-chain attestations from the block body and the proposer's - /// own attestation) is properly signed by the claimed validator using - /// their registered XMSS public key. - /// - /// # XMSS Verification - /// - /// ## Without feature flag (default): - /// The function performs structural validation only: - /// - Verifies signature count matches attestation count - /// - Validates validator indices are within bounds - /// - Prepares all data for verification - /// - /// ## With `xmss-verify` feature flag: - /// Enables cryptographic XMSS signature verification using the leanSig library. - /// - /// To enable: `cargo build --features xmss-verify` - /// - /// # Arguments - /// - /// * `parent_state` - The state at the parent block, used to retrieve - /// validator public keys and verify signatures. - /// - /// # Returns - /// - /// `true` if all signatures are cryptographically valid (or verification is disabled). - /// - /// # Panics - /// - /// Panics if validation fails: - /// - Signature count mismatch - /// - Validator index out of range - /// - XMSS signature verification failure (when feature enabled) - /// - /// # References - /// - /// - Spec: - /// - XMSS Library: - /// Verifies all attestation signatures using lean-multisig aggregated proofs. - /// Each attestation has a single `MultisigAggregatedSignature` proof that covers - /// all participating validators. + /// Verifies each aggregated attestation proof against the participant + /// validator public keys from parent state. /// /// Returns `Ok(())` if all signatures are valid, or an error describing the failure. pub fn verify_signatures(&self, parent_state: State) -> Result<()> { - // Unpack the signed block components - let block = &self.message.block; - let signatures = &self.signature; + let block = &self.block; + let signature = &self.signature; let aggregated_attestations = &block.body.attestations; - let attestation_signatures = &signatures.attestation_signatures; + let attestation_signatures = &signature.attestation_signatures; // Verify signature count matches aggregated attestation count ensure!( @@ -140,65 +84,60 @@ impl SignedBlockWithAttestation { let validators = &parent_state.validators; let num_validators = validators.len_u64(); - // Verify each aggregated attestation's zkVM proof - for (aggregated_attestation, aggregated_signature) in aggregated_attestations + // Phase 1: collect all verification inputs (serial - reads from State) + let verification_tasks = aggregated_attestations .into_iter() .zip(attestation_signatures.into_iter()) - { - let validator_ids = aggregated_attestation - .aggregation_bits - .to_validator_indices(); - - // Ensure all validators exist in the active set - for validator_id in &validator_ids { - ensure!( - *validator_id < num_validators, - "validator index {validator_id} out of range (max {num_validators})" - ); - } - - let attestation_data_root = aggregated_attestation.data.hash_tree_root(); - - // Collect validators, returning error if any not found - let public_keys = validator_ids - .into_iter() - .map(|id| { - validators - .get(id) - .map(|validator| validator.pubkey.clone()) - .map_err(Into::into) - }) - .collect::>>()?; - - // Verify the lean-multisig aggregated proof for this attestation - // - // The proof verifies that all validators in aggregation_bits signed - // the same attestation_data_root at the given epoch (slot). - aggregated_signature - .verify( - public_keys, - attestation_data_root, - aggregated_attestation.data.slot.0 as u32, - ) - .context("attestation aggregated signature verification failed")?; - } - - // Verify the proposer attestation signature (outside the attestation loop) - let proposer_attestation = &self.message.proposer_attestation; - let proposer_signature = &signatures.proposer_signature; - + .map(|(aggregated_attestation, aggregated_signature)| { + let validator_ids = aggregated_attestation + .aggregation_bits + .to_validator_indices(); + + // Ensure all validators exist in the active set + for validator_id in &validator_ids { + ensure!( + *validator_id < num_validators, + "validator index {validator_id} out of range (max {num_validators})" + ); + } + + let attestation_data_root = aggregated_attestation.data.hash_tree_root(); + let slot = aggregated_attestation.data.slot.0 as u32; + + // Collect validators, returning error if any not found + let public_keys = validator_ids + .into_iter() + .map(|id| { + validators + .get(id) + .map(|validator| validator.attestation_pubkey.clone()) + .map_err(Into::into) + }) + .collect::>>()?; + + Ok((public_keys, attestation_data_root, slot, aggregated_signature)) + }) + .collect::>>()?; + + // Phase 2: verify all proofs in parallel (CPU-intensive XMSS verification) + verification_tasks + .into_par_iter() + .try_for_each(|(public_keys, attestation_data_root, slot, aggregated_signature)| { + aggregated_signature + .verify(public_keys, attestation_data_root, slot) + .context("attestation aggregated signature verification failed") + })?; + + // Verify the proposer's XMSS signature over the block root + let proposer_index = block.proposer_index; ensure!( - proposer_attestation.validator_id < num_validators, - "proposer index {} out of range (max {num_validators})", - proposer_attestation.validator_id + proposer_index < num_validators, + "proposer index {proposer_index} out of range (max {num_validators})" ); let proposer = validators - .get(proposer_attestation.validator_id) - .context(format!( - "proposer {} not found in state", - proposer_attestation.validator_id - ))?; + .get(proposer_index) + .context(format!("proposer {proposer_index} not found in state"))?; let _timer = METRICS.get().map(|metrics| { metrics @@ -206,13 +145,14 @@ impl SignedBlockWithAttestation { .start_timer() }); - proposer_signature + signature + .proposer_signature .verify( - &proposer.pubkey, - proposer_attestation.data.slot.0 as u32, - proposer_attestation.data.hash_tree_root(), + &proposer.proposal_pubkey, + block.slot.0 as u32, + block.hash_tree_root(), ) - .context("Proposer signature verification failed")?; + .context("proposer signature verification failed")?; Ok(()) } diff --git a/lean_client/containers/src/config.rs b/lean_client/containers/src/config.rs index 59cd838a..568b64de 100644 --- a/lean_client/containers/src/config.rs +++ b/lean_client/containers/src/config.rs @@ -2,19 +2,39 @@ use serde::{Deserialize, Serialize}; use ssz::Ssz; use std::{fs::File, io::BufReader, path::Path}; -#[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] +fn default_log_inv_rate() -> u8 { + 2 +} + +fn default_attestation_committee_count() -> u64 { + 1 +} + +#[derive(Clone, Debug, PartialEq, Eq, Ssz, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct Config { pub genesis_time: u64, } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct GenesisValidatorEntry { + pub attestation_pubkey: String, + pub proposal_pubkey: String, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub struct GenesisConfig { pub genesis_time: u64, + #[serde(default)] pub active_epoch: u64, + #[serde(default)] pub validator_count: u64, - pub genesis_validators: Vec, + pub genesis_validators: Vec, + #[serde(default = "default_log_inv_rate")] + pub log_inv_rate: u8, + #[serde(default = "default_attestation_committee_count")] + pub attestation_committee_count: u64, } impl GenesisConfig { diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index d724689f..44ee387f 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -13,12 +13,9 @@ pub use attestation::{ Attestation, AttestationData, AttestationSignatures, Attestations, SignatureKey, SignedAggregatedAttestation, SignedAttestation, }; -pub use block::{ - Block, BlockBody, BlockHeader, BlockSignatures, BlockWithAttestation, SignedBlock, - SignedBlockWithAttestation, -}; +pub use block::{Block, BlockBody, BlockHeader, BlockSignatures, SignedBlock}; pub use checkpoint::Checkpoint; -pub use config::{Config, GenesisConfig}; +pub use config::{Config, GenesisConfig, GenesisValidatorEntry}; pub use slot::Slot; pub use state::{ HistoricalBlockHashes, JustificationRoots, JustificationValidators, JustifiedSlots, State, diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 963949bb..e56229b4 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -4,7 +4,7 @@ use metrics::METRICS; use serde::{Deserialize, Serialize}; use ssz::{BitList, H256, PersistentList, Ssz, SszHash}; use std::collections::{BTreeMap, HashMap, HashSet}; -use tracing::{info, trace}; +use tracing::{info, trace, warn}; use try_from_iterator::TryFromIterator; use typenum::{Prod, U262144}; use xmss::{PublicKey, Signature}; @@ -12,10 +12,13 @@ use xmss::{PublicKey, Signature}; use crate::{ AggregatedSignatureProof, Attestation, Checkpoint, Config, SignatureKey, Slot, attestation::{AggregatedAttestation, AggregatedAttestations, AggregationBits}, - block::{Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, + block::{Block, BlockBody, BlockHeader, SignedBlock}, validator::{Validator, ValidatorRegistryLimit, Validators}, }; +/// Maximum number of distinct AttestationData entries per block (spec: chain/config.py:36). +const MAX_ATTESTATIONS_DATA: usize = 16; + type HistoricalRootsLimit = U262144; // 2^18 type JustificationValidatorsLimit = Prod; @@ -188,7 +191,8 @@ impl State { let mut validators = PersistentList::default(); for i in 0..num_validators { let validator = Validator { - pubkey: PublicKey::default(), + attestation_pubkey: PublicKey::default(), + proposal_pubkey: PublicKey::default(), index: i, }; validators.push(validator).expect("Failed to add validator"); @@ -215,7 +219,6 @@ impl State { } pub fn get_justifications(&self) -> BTreeMap> { - // Use actual validator count, matching leanSpec let num_validators = self.validators.len_usize(); (&self.justifications_roots) .into_iter() @@ -238,7 +241,6 @@ impl State { } pub fn with_justifications(mut self, map: BTreeMap>) -> Self { - // Use actual validator count, matching leanSpec let num_validators = self.validators.len_usize(); let mut roots: Vec<_> = map.keys().cloned().collect(); roots.sort(); @@ -250,7 +252,6 @@ impl State { } // Build BitList: create with length, then set bits - // Each root has num_validators votes (matching leanSpec) let total_bits = roots.len() * num_validators; let mut new_validators = JustificationValidators::new(false, total_bits); @@ -285,7 +286,7 @@ impl State { pub fn state_transition( &self, - signed_block: SignedBlockWithAttestation, + signed_block: SignedBlock, valid_signatures: bool, ) -> Result { ensure!(valid_signatures, "invalid block signatures"); @@ -293,7 +294,7 @@ impl State { let _timer = METRICS .get() .map(|metrics| metrics.lean_state_transition_time_seconds.start_timer()); - let block = &signed_block.message.block; + let block = &signed_block.block; let mut state = self.process_slots(block.slot)?; state = state.process_block(block)?; @@ -633,7 +634,8 @@ impl State { available_attestations: Option>, known_block_roots: Option<&HashSet>, gossip_signatures: Option<&HashMap>, - aggregated_payloads: Option<&HashMap>>, + aggregated_payloads: Option<&HashMap>>, + log_inv_rate: usize, ) -> Result<( Block, Self, @@ -675,6 +677,13 @@ impl State { // Find new valid attestations matching post-state justification let mut new_attestations = Vec::new(); + // Track distinct AttestationData roots already accepted and newly added this iteration + let accepted_data_roots: HashSet = attestations + .iter() + .map(|a| a.data.hash_tree_root()) + .collect(); + let mut new_att_data_roots: HashSet = HashSet::new(); + for attestation in available_attestations { let data = &attestation.data; let validator_id = attestation.validator_id; @@ -696,6 +705,15 @@ impl State { continue; } + // Enforce MAX_ATTESTATIONS_DATA: only admit a new AttestationData if under the limit + let is_existing_data = accepted_data_roots.contains(&data_root) + || new_att_data_roots.contains(&data_root); + if !is_existing_data + && accepted_data_roots.len() + new_att_data_roots.len() >= MAX_ATTESTATIONS_DATA + { + continue; + } + // We can only include an attestation if we have some way to later provide // an aggregated proof for its group: // - either a per validator XMSS signature from gossip, or @@ -704,9 +722,10 @@ impl State { let has_gossip_sig = gossip_signatures.is_some_and(|sigs| sigs.contains_key(&sig_key)); let has_block_proof = - aggregated_payloads.is_some_and(|payloads| payloads.contains_key(&sig_key)); + aggregated_payloads.is_some_and(|payloads| payloads.contains_key(&data_root)); if has_gossip_sig || has_block_proof { + new_att_data_roots.insert(data_root); new_attestations.push(attestation.clone()); } } @@ -724,6 +743,7 @@ impl State { &attestations, gossip_signatures, aggregated_payloads, + log_inv_rate, )?; METRICS.get().map(|metrics| { @@ -765,7 +785,8 @@ impl State { &self, attestations: &[Attestation], gossip_signatures: Option<&HashMap>, - aggregated_payloads: Option<&HashMap>>, + aggregated_payloads: Option<&HashMap>>, + log_inv_rate: usize, ) -> Result<(Vec, Vec)> { let mut results: Vec<(AggregatedAttestation, AggregatedSignatureProof)> = Vec::new(); @@ -791,7 +812,7 @@ impl State { gossip_keys.push( self.validators .get(vid) - .map(|v| v.pubkey.clone()) + .map(|v| v.attestation_pubkey.clone()) .context(format!("invalid validator id {vid}"))?, ); gossip_ids.push(vid); @@ -805,11 +826,6 @@ impl State { } // If we collected any gossip signatures, create an aggregated proof - // NOTE: This matches Python leanSpec behavior (test_mode=True). - // Python also uses test_mode=True with TODO: "Remove test_mode once leanVM - // supports correct signature encoding." - // Once lean-multisig is fully integrated, this will call: - // MultisigAggregatedSignature::aggregate(public_keys, signatures, message, epoch) if !gossip_ids.is_empty() { let participants = AggregationBits::from_validator_indices(&gossip_ids); @@ -819,6 +835,7 @@ impl State { gossip_sigs, data_root, data.slot.0 as u32, + log_inv_rate, )?; results.push(( @@ -830,76 +847,213 @@ impl State { )); } - // Phase 2: Fallback to block proofs using greedy set-cover - // Goal: Cover remaining validators with minimum number of proofs - loop { - let Some(payloads) = aggregated_payloads else { - break; - }; + // Phase 2: Fallback to block proofs using greedy set-cover. + // Collect all selected proofs as children, then compress into ONE + // recursive proof via aggregate_with_children (spec intent). + if let Some(payloads) = aggregated_payloads { + if !remaining.is_empty() { + if let Some(candidates) = payloads.get(&data_root) { + if !candidates.is_empty() { + let mut phase2_children: Vec<&AggregatedSignatureProof> = Vec::new(); + + loop { + if remaining.is_empty() { + break; + } + + let Some((best_proof, covered_set)) = candidates + .iter() + .map(|proof| { + let proof_validators: HashSet = + proof.get_participant_indices().into_iter().collect(); + let intersection: HashSet = + remaining.intersection(&proof_validators).copied().collect(); + (proof, intersection) + }) + .max_by_key(|(_, intersection)| intersection.len()) + else { + break; + }; + + if covered_set.is_empty() { + break; + } + + phase2_children.push(best_proof); + for vid in &covered_set { + remaining.remove(vid); + } + } + + if !phase2_children.is_empty() { + let child_pk_vecs: Vec> = phase2_children + .iter() + .map(|child| { + child + .get_participant_indices() + .into_iter() + .filter_map(|vid| { + self.validators + .get(vid) + .ok() + .map(|v| v.attestation_pubkey.clone()) + }) + .collect() + }) + .collect(); + + let children_arg: Vec<(&[PublicKey], &AggregatedSignatureProof)> = + child_pk_vecs + .iter() + .zip(phase2_children.iter()) + .map(|(pks, proof)| (pks.as_slice(), *proof)) + .collect(); + + let mut phase2_validator_ids: Vec = phase2_children + .iter() + .flat_map(|child| child.get_participant_indices()) + .collect(); + phase2_validator_ids.sort(); + phase2_validator_ids.dedup(); + + let phase2_participants = + AggregationBits::from_validator_indices(&phase2_validator_ids); + + match AggregatedSignatureProof::aggregate_with_children( + phase2_participants.clone(), + &children_arg, + Vec::::new(), + Vec::::new(), + data_root, + data.slot.0 as u32, + log_inv_rate, + ) { + Ok(proof) => { + info!( + slot = data.slot.0, + children = phase2_children.len(), + validators = phase2_validator_ids.len(), + "Phase 2: recursive block proof via aggregate_with_children" + ); + results.push(( + AggregatedAttestation { + aggregation_bits: phase2_participants, + data: data.clone(), + }, + proof, + )); + } + Err(e) => { + warn!( + error = %e, + "Phase 2 recursive aggregation failed, skipping" + ); + } + } + } + } + } + } + } + } - // Pick any remaining validator to find candidate proofs - let Some(target_id) = remaining.iter().next().copied() else { - break; - }; + // Handle empty case + if results.is_empty() { + return Ok((Vec::new(), Vec::new())); + } - let key = SignatureKey::new(target_id, data_root); + // Post-loop compaction: the main loop may have emitted a Phase 1 (gossip) proof + // AND a Phase 2 (children) proof for the same AttestationData. + // Per spec, each AttestationData must appear at most once in the block body — + // merge any such pairs into a single recursive proof via aggregate_with_children. + let mut proof_groups: HashMap> = + HashMap::new(); + for (att, proof) in results { + proof_groups + .entry(att.data.hash_tree_root()) + .or_default() + .push((att, proof)); + } - let Some(candidates) = payloads.get(&key) else { - // No proofs found for this validator - break; - }; + let mut compacted: Vec<(AggregatedAttestation, AggregatedSignatureProof)> = Vec::new(); - if candidates.is_empty() { - // Same as before, no proofs found for this validator - break; - } + for (data_root, group) in proof_groups { + if group.len() == 1 { + // Only one proof for this data — no merge needed + compacted.extend(group); + } else { + // Multiple proofs (e.g. Phase 1 gossip + Phase 2 children) — + // merge into one recursive proof so each AttestationData appears once. + let data = group[0].0.data.clone(); - // Greedy selection: find proof covering most remaining validators - // For each candidate proof, compute intersection with remaining validators - let (best_proof, covered_set) = candidates + let child_pk_vecs: Vec> = group .iter() - .map(|proof| { - let proof_validators: HashSet = - proof.get_participant_indices().into_iter().collect(); - let intersection: HashSet = - remaining.intersection(&proof_validators).copied().collect(); - (proof, intersection) + .map(|(_, proof)| { + proof + .get_participant_indices() + .into_iter() + .filter_map(|vid| { + self.validators + .get(vid) + .ok() + .map(|v| v.attestation_pubkey.clone()) + }) + .collect() }) - .max_by_key(|(_, intersection)| intersection.len()) - .context("greedy algoritm failure: candidates were empty")?; - - // Guard: If best proof has zero overlap, stop - if covered_set.is_empty() { - break; - } - - // Record proof with its actual participants (from the proof itself) - let covered_validators: Vec = best_proof.get_participant_indices(); - let participants = AggregationBits::from_validator_indices(&covered_validators); + .collect(); - results.push(( - AggregatedAttestation { - aggregation_bits: participants, - data: data.clone(), - }, - best_proof.clone(), - )); + let children_arg: Vec<(&[PublicKey], &AggregatedSignatureProof)> = child_pk_vecs + .iter() + .zip(group.iter()) + .map(|(pks, (_, proof))| (pks.as_slice(), proof)) + .collect(); - // Remove covered validators from remaining - for vid in &covered_set { - remaining.remove(vid); + let mut all_validator_ids: Vec = group + .iter() + .flat_map(|(_, proof)| proof.get_participant_indices()) + .collect(); + all_validator_ids.sort(); + all_validator_ids.dedup(); + let all_participants = + AggregationBits::from_validator_indices(&all_validator_ids); + + match AggregatedSignatureProof::aggregate_with_children( + all_participants.clone(), + &children_arg, + Vec::::new(), + Vec::::new(), + data_root, + data.slot.0 as u32, + log_inv_rate, + ) { + Ok(merged_proof) => { + info!( + slot = data.slot.0, + children = group.len(), + validators = all_validator_ids.len(), + "Post-loop compaction: merged proofs into recursive proof" + ); + compacted.push(( + AggregatedAttestation { + aggregation_bits: all_participants, + data, + }, + merged_proof, + )); + } + Err(e) => { + warn!( + error = %e, + "Post-loop compaction failed, keeping proofs separate" + ); + compacted.extend(group); + } } } } - // Handle empty case - if results.is_empty() { - return Ok((Vec::new(), Vec::new())); - } - - // Unzip results into parallel lists let (aggregated_attestations, aggregated_proofs): (Vec<_>, Vec<_>) = - results.into_iter().unzip(); + compacted.into_iter().unzip(); Ok((aggregated_attestations, aggregated_proofs)) } diff --git a/lean_client/containers/src/validator.rs b/lean_client/containers/src/validator.rs index c4ed9857..431ca526 100644 --- a/lean_client/containers/src/validator.rs +++ b/lean_client/containers/src/validator.rs @@ -6,7 +6,8 @@ use xmss::PublicKey; // todo(containers): default implementation doesn't make sense here #[derive(Clone, Debug, Ssz, Serialize, Deserialize, Default)] pub struct Validator { - pub pubkey: PublicKey, + pub attestation_pubkey: PublicKey, + pub proposal_pubkey: PublicKey, #[serde(default)] pub index: u64, } diff --git a/lean_client/containers/tests/test_vectors/mod.rs b/lean_client/containers/tests/test_vectors/mod.rs index b753ea47..26571278 100644 --- a/lean_client/containers/tests/test_vectors/mod.rs +++ b/lean_client/containers/tests/test_vectors/mod.rs @@ -8,7 +8,7 @@ pub mod genesis; pub mod runner; pub mod verify_signatures; -use containers::{Block, SignedBlockWithAttestation, Slot, State}; +use containers::{Block, SignedBlock, Slot, State}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -97,7 +97,7 @@ pub struct VerifySignaturesTestVectorFile { pub struct VerifySignaturesTestCase { pub network: String, pub anchor_state: State, - pub signed_block_with_attestation: SignedBlockWithAttestation, + pub signed_block: SignedBlock, #[serde(default)] pub expect_exception: Option, #[serde(rename = "_info")] diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index 0bd7c85b..334b7665 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -642,21 +642,14 @@ impl TestRunner { println!("\n{}: {}", test_name, test_case.info.description); let anchor_state = test_case.anchor_state; - let signed_block = test_case.signed_block_with_attestation; + let signed_block = test_case.signed_block; // Print some debug info about what we're verifying - println!(" Block slot: {}", signed_block.message.block.slot.0); - println!( - " Proposer index: {}", - signed_block.message.block.proposer_index - ); + println!(" Block slot: {}", signed_block.block.slot.0); + println!(" Proposer index: {}", signed_block.block.proposer_index); - let attestation_count = signed_block.message.block.body.attestations.len_u64(); + let attestation_count = signed_block.block.body.attestations.len_u64(); println!(" Attestations in block: {}", attestation_count); - println!( - " Proposer attestation validator: {}", - signed_block.message.proposer_attestation.validator_id - ); // Check if we expect this test to fail if let Some(ref exception) = test_case.expect_exception { diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 43e99381..828db39d 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -5,8 +5,7 @@ use containers::{ AggregatedAttestation, Attestation, Attestations, Block, BlockBody, BlockHeader, - BlockSignatures, BlockWithAttestation, Checkpoint, Config, SignedBlockWithAttestation, Slot, - State, Validators, + BlockSignatures, Checkpoint, Config, SignedBlock, Slot, State, Validators, }; use containers::{ HistoricalBlockHashes, JustificationRoots, JustificationValidators, JustifiedSlots, Validator, @@ -26,7 +25,7 @@ pub fn create_block( slot: u64, parent_header: &mut BlockHeader, attestations: Option, -) -> SignedBlockWithAttestation { +) -> SignedBlock { let body = BlockBody { attestations: { let attestations_vec = attestations.unwrap_or_default(); @@ -61,11 +60,8 @@ pub fn create_block( body: body, }; - let return_value = SignedBlockWithAttestation { - message: BlockWithAttestation { - block: block_message, - proposer_attestation: Attestation::default(), - }, + let return_value = SignedBlock { + block: block_message, signature: BlockSignatures { attestation_signatures: PersistentList::default(), proposer_signature: Signature::default(), @@ -111,7 +107,8 @@ pub fn base_state_with_validators(config: Config, num_validators: usize) -> Stat let mut validators = Validators::default(); for i in 0..num_validators { let validator = Validator { - pubkey: Default::default(), + attestation_pubkey: Default::default(), + proposal_pubkey: Default::default(), index: i as u64, }; validators.push(validator).expect("within limit"); @@ -132,5 +129,5 @@ pub fn base_state_with_validators(config: Config, num_validators: usize) -> Stat } pub fn sample_config() -> Config { - Config { genesis_time: 0 } + Config::default() } diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index 1d52d186..06ccb3b6 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -43,8 +43,8 @@ fn test_process_block_header_valid() { let mut state_at_slot_1 = genesis_state.process_slots(Slot(1)).unwrap(); let genesis_header_root = state_at_slot_1.latest_block_header.hash_tree_root(); - let block = create_block(1, &mut state_at_slot_1.latest_block_header, None).message; - let new_state = state_at_slot_1.process_block_header(&block.block).unwrap(); + let signed_block = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let new_state = state_at_slot_1.process_block_header(&signed_block.block).unwrap(); assert_eq!(new_state.latest_finalized.root, genesis_header_root); assert_eq!(new_state.latest_justified.root, genesis_header_root); diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index c7cb259b..3b1d9595 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -4,10 +4,7 @@ //! and state root verification. // tests/state_transition.rs -use containers::{ - Attestation, Block, BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation, Slot, - State, -}; +use containers::{Block, BlockSignatures, SignedBlock, Slot, State}; use pretty_assertions::assert_eq; use rstest::fixture; use ssz::{H256, PersistentList, SszHash}; @@ -28,9 +25,8 @@ fn test_state_transition_full() { let state = genesis_state(); let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - let signed_block_with_attestation = - create_block(1, &mut state_at_slot_1.latest_block_header, None); - let block = signed_block_with_attestation.message.block.clone(); + let signed_block = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let block = signed_block.block.clone(); // Use process_block_header + process_operations to avoid state root validation during setup let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); @@ -44,17 +40,12 @@ fn test_state_transition_full() { ..block }; - let final_signed_block_with_attestation = SignedBlockWithAttestation { - message: BlockWithAttestation { - block: block_with_correct_root, - proposer_attestation: signed_block_with_attestation.message.proposer_attestation, - }, - signature: signed_block_with_attestation.signature, + let final_signed_block = SignedBlock { + block: block_with_correct_root, + signature: signed_block.signature, }; - let final_state = state - .state_transition(final_signed_block_with_attestation, true) - .unwrap(); + let final_state = state.state_transition(final_signed_block, true).unwrap(); assert_eq!( final_state.hash_tree_root(), @@ -67,9 +58,8 @@ fn test_state_transition_invalid_signatures() { let state = genesis_state(); let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - let signed_block_with_attestation = - create_block(1, &mut state_at_slot_1.latest_block_header, None); - let block = signed_block_with_attestation.message.block.clone(); + let signed_block = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let block = signed_block.block.clone(); // Use process_block_header + process_operations to avoid state root validation during setup let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); @@ -83,15 +73,12 @@ fn test_state_transition_invalid_signatures() { ..block }; - let final_signed_block_with_attestation = SignedBlockWithAttestation { - message: BlockWithAttestation { - block: block_with_correct_root, - proposer_attestation: signed_block_with_attestation.message.proposer_attestation, - }, - signature: signed_block_with_attestation.signature, + let final_signed_block = SignedBlock { + block: block_with_correct_root, + signature: signed_block.signature, }; - let result = state.state_transition(final_signed_block_with_attestation, false); + let result = state.state_transition(final_signed_block, false); assert!(result.is_err()); } @@ -101,24 +88,20 @@ fn test_state_transition_bad_state_root() { let state = genesis_state(); let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - let signed_block_with_attestation = - create_block(1, &mut state_at_slot_1.latest_block_header, None); - let mut block = signed_block_with_attestation.message.block.clone(); + let signed_block = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let mut block = signed_block.block.clone(); block.state_root = H256::zero(); - let final_signed_block_with_attestation = SignedBlockWithAttestation { - message: BlockWithAttestation { - block, - proposer_attestation: Attestation::default(), - }, + let final_signed_block = SignedBlock { + block, signature: BlockSignatures { attestation_signatures: PersistentList::default(), proposer_signature: Signature::default(), }, }; - let result = state.state_transition(final_signed_block_with_attestation, true); + let result = state.state_transition(final_signed_block, true); assert!(result.is_err()); } @@ -128,9 +111,8 @@ fn test_state_transition_devnet2() { let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); // Create a block with attestations for devnet2 - let signed_block_with_attestation = - create_block(1, &mut state_at_slot_1.latest_block_header, None); - let block = signed_block_with_attestation.message.block.clone(); + let signed_block = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let block = signed_block.block.clone(); // Process the block header and attestations let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); @@ -145,18 +127,13 @@ fn test_state_transition_devnet2() { ..block }; - let final_signed_block_with_attestation = SignedBlockWithAttestation { - message: BlockWithAttestation { - block: block_with_correct_root, - proposer_attestation: signed_block_with_attestation.message.proposer_attestation, - }, - signature: signed_block_with_attestation.signature, + let final_signed_block = SignedBlock { + block: block_with_correct_root, + signature: signed_block.signature, }; // Perform the state transition and validate the result - let final_state = state - .state_transition(final_signed_block_with_attestation, true) - .unwrap(); + let final_state = state.state_transition(final_signed_block, true).unwrap(); assert_eq!( final_state.hash_tree_root(), diff --git a/lean_client/fork_choice/src/block_cache.rs b/lean_client/fork_choice/src/block_cache.rs index 99907544..db29c913 100644 --- a/lean_client/fork_choice/src/block_cache.rs +++ b/lean_client/fork_choice/src/block_cache.rs @@ -1,14 +1,14 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::time::Instant; -use containers::{SignedBlockWithAttestation, Slot}; +use containers::{SignedBlock, Slot}; use ssz::H256; pub const MAX_CACHED_BLOCKS: usize = 1024; #[derive(Debug, Clone)] pub struct PendingBlock { - pub block: SignedBlockWithAttestation, + pub block: SignedBlock, pub root: H256, pub parent_root: H256, pub slot: Slot, @@ -54,7 +54,7 @@ impl BlockCache { pub fn add( &mut self, - block: SignedBlockWithAttestation, + block: SignedBlock, root: H256, parent_root: H256, slot: Slot, diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 5a0b24ff..6eeb6148 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -1,7 +1,6 @@ use anyhow::{Context, Result, anyhow, bail, ensure}; use containers::{ - AttestationData, SignatureKey, SignedAggregatedAttestation, SignedAttestation, - SignedBlockWithAttestation, + AttestationData, SignatureKey, SignedAggregatedAttestation, SignedAttestation, SignedBlock, }; use metrics::METRICS; use ssz::{H256, SszHash}; @@ -14,7 +13,7 @@ use crate::store::{ #[inline] pub fn on_tick(store: &mut Store, time_millis: u64, has_proposal: bool) { - // Calculate target time in intervals using milliseconds (devnet-3: 800ms intervals) + // Calculate target time in intervals using milliseconds (800ms intervals) // genesis_time is in seconds, convert to milliseconds for calculation let genesis_millis = store.config.genesis_time * 1000; let elapsed_millis = time_millis.saturating_sub(genesis_millis); @@ -35,7 +34,7 @@ pub fn on_tick(store: &mut Store, time_millis: u64, has_proposal: bool) { /// 3. A vote cannot be for a future slot. /// 4. Checkpoint slots must match block slots. fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<()> { - // Topology: history is linear and monotonic — source <= target <= head (store.py:310-311). + // Topology: history is linear and monotonic — source <= target <= head. ensure!( data.source.slot <= data.target.slot, "Source checkpoint slot {} must not exceed target slot {}", @@ -159,6 +158,13 @@ pub fn on_gossip_attestation( }); })?; + // Non-aggregators validate attestation data but do not store or verify individual + // signatures. Per leanSpec: only aggregators import gossip attestations for aggregation. + // Subnet filtering is already enforced at the p2p subscription layer. + if !store.is_aggregator { + return Ok(()); + } + let data_root = attestation_data.hash_tree_root(); let sig_key = SignatureKey::new(signed_attestation.validator_id, data_root); @@ -185,7 +191,7 @@ pub fn on_gossip_attestation( let pubkey = key_state .validators .get(validator_id) - .map(|v| v.pubkey.clone()) + .map(|v| v.attestation_pubkey.clone()) .map_err(|e| anyhow!("{e}"))?; signed_attestation @@ -240,7 +246,7 @@ pub fn on_gossip_attestation( /// Process an attestation and place it into the correct attestation stage /// /// Attestation processing logic that updates the attestation -/// maps used for fork choice. Per devnet-2, we store AttestationData only (not signatures). +/// maps used for fork choice. We store AttestationData only (not signatures). /// /// Attestations can come from: /// - a block body (on-chain, `is_from_block=True`), or @@ -296,8 +302,10 @@ pub fn on_attestation( .attestation_data_by_root .insert(data_root, attestation_data.clone()); - if !is_from_block { - // Store signature for later aggregation during block building + if !is_from_block && store.is_aggregator { + // Store signature for later aggregation during block building. + // Per leanSpec: only aggregators store gossip signatures, including own attestations. + // Non-aggregator validators produce and gossip attestations but do not store the sig. let sig_key = SignatureKey::new(signed_attestation.validator_id, data_root); store .gossip_signatures @@ -328,13 +336,11 @@ pub fn on_attestation( }) } -/// Devnet-3: Process an aggregated attestation from the aggregation topic +/// Process an aggregated attestation from the aggregation topic. /// -/// Per leanSpec: Aggregated attestations are stored as proofs in -/// `latest_new_aggregated_payloads`. At interval 3, these are merged with +/// Verifies the aggregated XMSS proof against participant public keys and stores +/// it in `latest_new_aggregated_payloads`. At interval 3, these are merged with /// `latest_known_aggregated_payloads` (from blocks) to compute safe target. -/// -/// Verifies the aggregated XMSS proof against participant public keys before storing. #[inline] pub fn on_aggregated_attestation( store: &mut Store, @@ -393,7 +399,7 @@ pub fn on_aggregated_attestation( key_state .validators .get(id) - .map(|v| v.pubkey.clone()) + .map(|v| v.attestation_pubkey.clone()) .map_err(Into::into) }) .collect::>>()?; @@ -402,19 +408,12 @@ pub fn on_aggregated_attestation( .verify(public_keys, data_root, attestation_data.slot.0 as u32) .context("aggregated attestation proof verification failed")?; - // Per leanSpec: Store the verified proof in latest_new_aggregated_payloads - // Each participating validator gets an entry via their SignatureKey - for (bit_idx, bit) in proof.participants.0.iter().enumerate() { - if *bit { - let validator_id = bit_idx as u64; - let sig_key = SignatureKey::new(validator_id, data_root); - store - .latest_new_aggregated_payloads - .entry(sig_key) - .or_default() - .push(proof.clone()); - } - } + // Store the verified proof in latest_new_aggregated_payloads, keyed by data_root + store + .latest_new_aggregated_payloads + .entry(data_root) + .or_default() + .push(proof); METRICS.get().map(|metrics| { metrics @@ -488,15 +487,15 @@ fn on_attestation_internal( pub fn on_block( store: &mut Store, cache: &mut BlockCache, - signed_block: SignedBlockWithAttestation, + signed_block: SignedBlock, ) -> Result<()> { - let block_root = signed_block.message.block.hash_tree_root(); + let block_root = signed_block.block.hash_tree_root(); if store.blocks.contains_key(&block_root) { return Ok(()); } - let parent_root = signed_block.message.block.parent_root; + let parent_root = signed_block.block.parent_root; if !store.states.contains_key(&parent_root) && !parent_root.is_zero() { bail!( @@ -513,7 +512,7 @@ pub fn on_block( fn process_block_internal( store: &mut Store, - signed_block: SignedBlockWithAttestation, + signed_block: SignedBlock, block_root: H256, ) -> Result<()> { let _timer = METRICS.get().map(|metrics| { @@ -522,7 +521,7 @@ fn process_block_internal( .start_timer() }); - let block = signed_block.message.block.clone(); + let block = signed_block.block.clone(); let attestations_count = block.body.attestations.len_u64(); // Get parent state for validation @@ -569,7 +568,7 @@ fn process_block_internal( .remove(&block_root) .unwrap_or_default(); for signed_att in pending { - if let Err(err) = on_attestation(store, signed_att, false) { + if let Err(err) = on_gossip_attestation(store, signed_att) { warn!(%err, "Pending attestation retry failed after block arrival"); } } @@ -627,23 +626,22 @@ fn process_block_internal( store.states.retain(|_, state| state.slot.0 >= keep_from); store.blocks.retain(|_, block| block.slot.0 >= keep_from); - // Prune stale attestation data — mirrors leanSpec store.prune_stale_attestation_data() - // (store.py:230-278), called at store.py:565-566 whenever finalization advances. + // Prune stale attestation data whenever finalization advances. // Criterion: target.slot <= finalized_slot → stale, no longer affects fork choice. // attestation_data_by_root is the secondary index used for target.slot lookup and - // must be pruned last so the three retain calls above can still resolve target.slot. + // must be pruned last so the retain calls above can still resolve target.slot. let finalized_slot = store.latest_finalized.slot.0; let adr = &store.attestation_data_by_root; store.gossip_signatures.retain(|key, _| { adr.get(&key.data_root) .map_or(true, |data| data.target.slot.0 > finalized_slot) }); - store.latest_known_aggregated_payloads.retain(|key, _| { - adr.get(&key.data_root) + store.latest_known_aggregated_payloads.retain(|data_root, _| { + adr.get(data_root) .map_or(true, |data| data.target.slot.0 > finalized_slot) }); - store.latest_new_aggregated_payloads.retain(|key, _| { - adr.get(&key.data_root) + store.latest_new_aggregated_payloads.retain(|data_root, _| { + adr.get(data_root) .map_or(true, |data| data.target.slot.0 > finalized_slot) }); store @@ -669,10 +667,8 @@ fn process_block_internal( // Process block body attestations as on-chain (is_from_block=true) let signatures = &signed_block.signature; let aggregated_attestations = &block.body.attestations; - let proposer_attestation = &signed_block.message.proposer_attestation; - // Store aggregated proofs for future block building - // Each attestation_signature proof is indexed by (validator_id, data_root) for each participating validator + // Store aggregated proofs indexed by (validator_id, data_root) for future block building for (att_idx, aggregated_attestation) in aggregated_attestations.into_iter().enumerate() { let data_root = aggregated_attestation.data.hash_tree_root(); @@ -682,20 +678,13 @@ fn process_block_internal( .attestation_data_by_root .insert(data_root, aggregated_attestation.data.clone()); - // Get the corresponding proof from attestation_signatures + // Get the corresponding proof from attestation_signatures and store it keyed by data_root if let Ok(proof_data) = signatures.attestation_signatures.get(att_idx as u64) { - // Store proof for each validator in the aggregation - for (bit_idx, bit) in aggregated_attestation.aggregation_bits.0.iter().enumerate() { - if *bit { - let validator_id = bit_idx as u64; - let sig_key = SignatureKey::new(validator_id, data_root); - store - .latest_known_aggregated_payloads - .entry(sig_key) - .or_default() - .push(proof_data.clone()); - } - } + store + .latest_known_aggregated_payloads + .entry(data_root) + .or_default() + .push(proof_data.clone()); } } @@ -734,43 +723,14 @@ fn process_block_internal( } } - // Update head BEFORE processing proposer attestation update_head(store); - // Store proposer's signature for later block building - let proposer_data_root = proposer_attestation.data.hash_tree_root(); - let proposer_sig_key = SignatureKey::new(proposer_attestation.validator_id, proposer_data_root); - store - .gossip_signatures - .insert(proposer_sig_key, signed_block.signature.proposer_signature); - METRICS.get().map(|metrics| { - metrics - .lean_gossip_signatures - .set(store.gossip_signatures.len() as i64) - }); - store - .attestation_data_by_root - .insert(proposer_data_root, proposer_attestation.data.clone()); - METRICS.get().map(|m| { - m.grandine_attestation_data_by_root - .set(store.attestation_data_by_root.len() as i64) - }); - - // Process proposer attestation as if received via gossip (is_from_block=false) - // This ensures it goes to "new" attestations and doesn't immediately affect fork choice - on_attestation_internal( - store, - proposer_attestation.validator_id, - proposer_attestation.data.clone(), - false, // is_from_block - )?; - Ok(()) } pub fn process_pending_blocks(store: &mut Store, cache: &mut BlockCache, mut roots: Vec) { while let Some(parent_root) = roots.pop() { - let children: Vec<(H256, SignedBlockWithAttestation)> = cache + let children: Vec<(H256, SignedBlock)> = cache .get_children(&parent_root) .into_iter() .map(|p| (p.root, p.block.clone())) diff --git a/lean_client/fork_choice/src/lib.rs b/lean_client/fork_choice/src/lib.rs index 9bb2e0bc..0def96c0 100644 --- a/lean_client/fork_choice/src/lib.rs +++ b/lean_client/fork_choice/src/lib.rs @@ -3,8 +3,7 @@ pub mod handlers; pub mod store; pub mod sync_state; -// dirty hack to avoid issues compiling grandine dependencies. by default, bls -// crate has no features enabled, and thus compilation fails (as exactly one -// backend must be enabled). So we include bls crate with one feature enabled, -// to make everything work. +// The bls crate requires exactly one backend feature to be enabled; without it +// compilation fails. Force-include it here with a feature enabled so the +// dependency tree resolves correctly. use bls as _; diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 2cf760a0..6320c657 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use anyhow::{Result, anyhow, ensure}; use containers::{ AggregatedSignatureProof, Attestation, AttestationData, Block, BlockHeader, Checkpoint, Config, - SignatureKey, SignedAggregatedAttestation, SignedAttestation, SignedBlockWithAttestation, Slot, + SignatureKey, SignedAggregatedAttestation, SignedAttestation, SignedBlock, Slot, State, }; use metrics::{METRICS, set_gauge_u64}; @@ -26,6 +26,12 @@ pub struct Store { pub config: Config, + /// Whether this node performs aggregation duties. + /// Only aggregators import and store individual gossip attestation signatures. + /// Non-aggregators validate attestation data but drop signatures immediately. + /// Per leanSpec: subnet filtering is at the p2p layer; this flag is the store-layer gate. + pub is_aggregator: bool, + pub head: H256, pub safe_target: H256, @@ -61,15 +67,17 @@ pub struct Store { pub gossip_signatures: HashMap, - /// Devnet-3: Aggregated signature proofs from block bodies (on-chain). + /// Aggregated signature proofs from block bodies (on-chain). /// These are attestations that have been included in blocks and are part of /// the "known" pool for safe target computation. - pub latest_known_aggregated_payloads: HashMap>, + /// Keyed by attestation data root (H256). + pub latest_known_aggregated_payloads: HashMap>, - /// Devnet-3: Aggregated signature proofs from gossip aggregation topic. + /// Aggregated signature proofs from gossip aggregation topic. /// These are newly received aggregations that haven't been migrated to "known" yet. /// At interval 3, we merge this with latest_known_aggregated_payloads for safe target. - pub latest_new_aggregated_payloads: HashMap>, + /// Keyed by attestation data root (H256). + pub latest_new_aggregated_payloads: HashMap>, /// Attestation data indexed by hash (data_root). /// Used to look up the exact attestation data that was signed when @@ -187,11 +195,12 @@ impl Store { /// Initialize forkchoice store from an anchor state and block pub fn get_forkchoice_store( anchor_state: State, - anchor_block: SignedBlockWithAttestation, + anchor_block: SignedBlock, config: Config, + is_aggregator: bool, ) -> Store { // Extract the plain Block from the signed block - let block = anchor_block.message.block.clone(); + let block = anchor_block.block.clone(); let block_slot = block.slot; // Compute block root differently for genesis vs checkpoint sync: @@ -213,10 +222,10 @@ pub fn get_forkchoice_store( block_header.hash_tree_root() }; - // Per leanSpec: substitute anchor_root for the checkpoint roots (the - // historical justified/finalized blocks are not in our store), but keep - // the actual slots from the downloaded state. validate_attestation_data - // skips the block-slot match when the source root is a known checkpoint. + // Substitute anchor_root for the checkpoint roots (the historical + // justified/finalized blocks are not in our store), but keep the actual + // slots from the downloaded state. validate_attestation_data skips + // the block-slot match when the source root is a known checkpoint. let latest_justified = Checkpoint { root: block_root, slot: anchor_state.latest_justified.slot, @@ -233,11 +242,12 @@ pub fn get_forkchoice_store( Store { time: block_slot.0 * INTERVALS_PER_SLOT, config, + is_aggregator, head: block_root, safe_target: block_root, latest_justified, latest_finalized, - justified_ever_updated: false, + justified_ever_updated: block_slot.0 == 0, finalized_ever_updated: false, blocks: { let mut m = HashMap::new(); @@ -423,17 +433,17 @@ pub fn update_head(store: &mut Store) { /// Extract per-validator attestations from aggregated payloads. /// -/// Per leanSpec: walks through all aggregated proofs and extracts the latest -/// attestation data for each validator based on their participation bits. +/// Walks through all aggregated proofs and extracts the latest attestation +/// data for each validator based on their participation bits. fn extract_attestations_from_aggregated_payloads( - payloads: &HashMap>, + payloads: &HashMap>, attestation_data_by_root: &HashMap, ) -> HashMap { let mut attestations: HashMap = HashMap::new(); - for (sig_key, proofs) in payloads { - // Look up the attestation data for this signature key's data_root - let Some(attestation_data) = attestation_data_by_root.get(&sig_key.data_root) else { + for (data_root, proofs) in payloads { + // Look up the attestation data for this data root + let Some(attestation_data) = attestation_data_by_root.get(data_root) else { continue; }; @@ -457,16 +467,14 @@ fn extract_attestations_from_aggregated_payloads( attestations } -/// Devnet-3: Update safe target from aggregated attestations +/// Update safe target from aggregated attestations. /// -/// Per leanSpec: Safe target is computed by merging BOTH aggregated payload pools: +/// Safe target is computed by merging both aggregated payload pools: /// - latest_known_aggregated_payloads: from block bodies (on-chain) /// - latest_new_aggregated_payloads: from gossip aggregation topic /// -/// This merge is critical because at interval 3 (when this runs), the migration -/// to "known" (interval 4) hasn't happened yet. Without merging: -/// - Proposer's own attestation in block body (goes directly to known) would be invisible -/// - Node's self-attestation (goes directly to known) would be invisible +/// Both pools are merged because at interval 3 (when this runs), the migration +/// to "known" (interval 4) hasn't happened yet. pub fn update_safe_target(store: &mut Store) { let n_validators = if let Some(state) = store.states.get(&store.head) { state.validators.len_usize() @@ -479,14 +487,13 @@ pub fn update_safe_target(store: &mut Store) { let min_score = (n_validators * 2 + 2) / 3; let root = store.latest_justified.root; - // Per leanSpec: Merge both aggregated payload pools - // This ensures we see all attestations including proposer's own and self-attestations - let mut all_payloads: HashMap> = + // Merge both aggregated payload pools to see all attestations + let mut all_payloads: HashMap> = store.latest_known_aggregated_payloads.clone(); - for (sig_key, proofs) in &store.latest_new_aggregated_payloads { + for (data_root, proofs) in &store.latest_new_aggregated_payloads { all_payloads - .entry(sig_key.clone()) + .entry(*data_root) .or_default() .extend(proofs.clone()); } @@ -501,10 +508,6 @@ pub fn update_safe_target(store: &mut Store) { let new_safe_target = get_fork_choice_head(store, root, &attestations, min_score); store.safe_target = new_safe_target; - // Clear the "new" pool after processing (will be repopulated by gossip) - // Note: We do NOT clear latest_known_aggregated_payloads as those persist - store.latest_new_aggregated_payloads.clear(); - set_gauge_u64( |metrics| &metrics.lean_safe_target_slot, || { @@ -522,6 +525,15 @@ pub fn accept_new_attestations(store: &mut Store) { store .latest_known_attestations .extend(store.latest_new_attestations.drain()); + // Promote gossip-received aggregated proofs to the known pool so they + // are available for block production at the next interval 0. + for (data_root, proofs) in store.latest_new_aggregated_payloads.drain() { + store + .latest_known_aggregated_payloads + .entry(data_root) + .or_default() + .extend(proofs); + } update_head(store); METRICS.get().map(|m| { m.grandine_fork_choice_known_attestations @@ -534,7 +546,7 @@ pub fn accept_new_attestations(store: &mut Store) { pub fn tick_interval(store: &mut Store, has_proposal: bool) { store.time += 1; // Calculate current interval within slot: time % INTERVALS_PER_SLOT - // Devnet-3: 5 intervals per slot (800ms each) + // 5 intervals per slot (800ms each) let curr_interval = store.time % INTERVALS_PER_SLOT; match curr_interval { @@ -565,13 +577,15 @@ pub struct BlockProductionInputs { pub available_attestations: Vec, pub known_block_roots: HashSet, pub gossip_signatures: HashMap, - pub aggregated_payloads: HashMap>, + pub aggregated_payloads: HashMap>, + pub log_inv_rate: usize, } pub fn prepare_block_production( store: &mut Store, slot: Slot, validator_index: u64, + log_inv_rate: usize, ) -> Result { let head_root = get_proposal_head(store, slot); let head_state = store @@ -590,7 +604,8 @@ pub fn prepare_block_production( expected_proposer ); - let available_attestations: Vec = store + // Step 1: individual attestations already tracked per-validator. + let mut available_attestations: Vec = store .latest_known_attestations .iter() .map(|(validator_idx, attestation_data)| Attestation { @@ -599,6 +614,33 @@ pub fn prepare_block_production( }) .collect(); + // Step 2: synthesize entries for validators that arrived *only* via + // on_aggregated_attestation. Those validators have proofs in + // latest_known_aggregated_payloads but were never inserted into + // latest_known_attestations, so build_block's fixed-point loop would + // otherwise silently skip them. + { + let known_validators: HashSet = + store.latest_known_attestations.keys().copied().collect(); + let mut seen_synthesized: HashSet<(u64, H256)> = HashSet::new(); + for (data_root, proofs) in &store.latest_known_aggregated_payloads { + if let Some(att_data) = store.attestation_data_by_root.get(data_root) { + for proof in proofs { + for vid in proof.participants.to_validator_indices() { + if !known_validators.contains(&vid) + && seen_synthesized.insert((vid, *data_root)) + { + available_attestations.push(Attestation { + validator_id: vid, + data: att_data.clone(), + }); + } + } + } + } + } + } + let known_block_roots: HashSet = store.blocks.keys().copied().collect(); let gossip_signatures = store.gossip_signatures.clone(); let aggregated_payloads = store.latest_known_aggregated_payloads.clone(); @@ -612,6 +654,7 @@ pub fn prepare_block_production( known_block_roots, gossip_signatures, aggregated_payloads, + log_inv_rate, }) } @@ -627,6 +670,7 @@ pub fn execute_block_production( known_block_roots, gossip_signatures, aggregated_payloads, + log_inv_rate, } = inputs; let (final_block, _final_post_state, _aggregated_attestations, signatures) = head_state @@ -639,6 +683,7 @@ pub fn execute_block_production( Some(&known_block_roots), Some(&gossip_signatures), Some(&aggregated_payloads), + log_inv_rate, )?; let block_root = final_block.hash_tree_root(); @@ -650,7 +695,8 @@ pub fn produce_block_with_signatures( store: &mut Store, slot: Slot, validator_index: u64, + log_inv_rate: usize, ) -> Result<(H256, Block, Vec)> { - let inputs = prepare_block_production(store, slot, validator_index)?; + let inputs = prepare_block_production(store, slot, validator_index, log_inv_rate)?; execute_block_production(inputs) } diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index 310ad5a1..9585c6ee 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -1,8 +1,7 @@ use containers::{ AggregatedAttestation, AggregationBits, Attestation, AttestationData, Block, BlockBody, - BlockHeader, BlockSignatures, BlockWithAttestation, Checkpoint, Config, HistoricalBlockHashes, - JustificationRoots, JustificationValidators, JustifiedSlots, SignedBlockWithAttestation, Slot, - State, Validator, Validators, + BlockHeader, BlockSignatures, Checkpoint, Config, HistoricalBlockHashes, JustificationRoots, + JustificationValidators, JustifiedSlots, SignedBlock, Slot, State, Validator, Validators, }; use fork_choice::{ block_cache::BlockCache, @@ -86,12 +85,18 @@ impl Into for TestAnchorState { let mut validators = Validators::default(); for test_validator in &self.validators.data { - let pubkey: PublicKey = test_validator - .pubkey + let attestation_pubkey: PublicKey = test_validator + .attestation_pubkey .parse() - .expect("Failed to parse validator pubkey"); + .expect("Failed to parse validator attestation_pubkey"); + let proposal_pubkey: PublicKey = test_validator + .proposal_pubkey + .as_deref() + .map(|s| s.parse().expect("Failed to parse validator proposal_pubkey")) + .unwrap_or_default(); let validator = Validator { - pubkey, + attestation_pubkey, + proposal_pubkey, index: test_validator.index, }; validators.push(validator).expect("Failed to add validator"); @@ -171,7 +176,11 @@ struct TestDataWrapper { #[derive(Debug, Deserialize)] struct TestValidator { #[allow(dead_code)] - pubkey: String, + #[serde(alias = "pubkey")] + attestation_pubkey: String, + #[allow(dead_code)] + #[serde(default)] + proposal_pubkey: Option, #[allow(dead_code)] #[serde(default)] index: u64, @@ -187,8 +196,8 @@ struct TestAnchorBlock { body: TestBlockBody, } -impl Into for TestAnchorBlock { - fn into(self) -> SignedBlockWithAttestation { +impl Into for TestAnchorBlock { + fn into(self) -> SignedBlock { let mut attestations = ssz::PersistentList::default(); for (i, attestation) in self.body.attestations.data.into_iter().enumerate() { @@ -205,31 +214,8 @@ impl Into for TestAnchorBlock { body: BlockBody { attestations }, }; - // Create proposer attestation - let proposer_attestation = Attestation { - validator_id: self.proposer_index, - data: AttestationData { - slot: Slot(self.slot), - head: Checkpoint { - root: parse_root(&self.parent_root), - slot: Slot(self.slot), - }, - target: Checkpoint { - root: parse_root(&self.parent_root), - slot: Slot(self.slot), - }, - source: Checkpoint { - root: parse_root(&self.parent_root), - slot: Slot(0), - }, - }, - }; - - SignedBlockWithAttestation { - message: BlockWithAttestation { - block, - proposer_attestation, - }, + SignedBlock { + block, signature: BlockSignatures::default(), } } @@ -261,17 +247,16 @@ impl Into for TestBlock { #[serde(rename_all = "camelCase")] struct TestBlockWithAttestation { block: TestBlock, - proposer_attestation: TestAttestation, + /// Ignored in devnet4 — proposer attestation removed from block format. + #[serde(default)] + proposer_attestation: Option, #[serde(default)] block_root_label: Option, } -impl Into for TestBlockWithAttestation { - fn into(self) -> BlockWithAttestation { - BlockWithAttestation { - block: self.block.into(), - proposer_attestation: self.proposer_attestation.into(), - } +impl Into for TestBlockWithAttestation { + fn into(self) -> Block { + self.block.into() } } @@ -537,14 +522,14 @@ fn forkchoice(spec_file: &str) { }; let mut anchor_state: State = case.anchor_state.into(); - let anchor_block: SignedBlockWithAttestation = case.anchor_block.into(); + let anchor_block: SignedBlock = case.anchor_block.into(); - let body_root = anchor_block.message.block.body.hash_tree_root(); + let body_root = anchor_block.block.body.hash_tree_root(); anchor_state.latest_block_header = BlockHeader { - slot: anchor_block.message.block.slot, - proposer_index: anchor_block.message.block.proposer_index, - parent_root: anchor_block.message.block.parent_root, - state_root: anchor_block.message.block.state_root, + slot: anchor_block.block.slot, + proposer_index: anchor_block.block.proposer_index, + parent_root: anchor_block.block.parent_root, + state_root: anchor_block.block.state_root, body_root, }; @@ -562,17 +547,17 @@ fn forkchoice(spec_file: &str) { let block_root_label = test_block.block_root_label.clone(); let result = std::panic::catch_unwind(AssertUnwindSafe(|| { - let block: BlockWithAttestation = test_block.into(); - let signed_block: SignedBlockWithAttestation = SignedBlockWithAttestation { - message: block, + let block: Block = test_block.into(); + let signed_block = SignedBlock { + block, signature: BlockSignatures::default(), }; - let block_root = signed_block.message.block.hash_tree_root(); + let block_root = signed_block.block.hash_tree_root(); // Advance time to the block's slot to ensure attestations are processable // SECONDS_PER_SLOT is 4. Convert to milliseconds for devnet-3 let block_time_millis = (store.config.genesis_time - + (signed_block.message.block.slot.0 * 4)) + + (signed_block.block.slot.0 * 4)) * 1000; on_tick(&mut store, block_time_millis, false); diff --git a/lean_client/fork_choice/tests/unit_tests/common.rs b/lean_client/fork_choice/tests/unit_tests/common.rs index 661f0449..bdc7b53a 100644 --- a/lean_client/fork_choice/tests/unit_tests/common.rs +++ b/lean_client/fork_choice/tests/unit_tests/common.rs @@ -1,7 +1,4 @@ -use containers::{ - Attestation, Block, BlockBody, BlockWithAttestation, Config, SignedBlockWithAttestation, Slot, - State, Validator, -}; +use containers::{Block, BlockBody, Config, SignedBlock, Slot, State, Validator}; use fork_choice::store::{Store, get_forkchoice_store}; use ssz::{H256, SszHash}; @@ -20,13 +17,8 @@ pub fn create_test_store() -> Store { body: BlockBody::default(), }; - let block_with_attestation = BlockWithAttestation { - block: block.clone(), - proposer_attestation: Attestation::default(), - }; - - let signed_block = SignedBlockWithAttestation { - message: block_with_attestation, + let signed_block = SignedBlock { + block, signature: Default::default(), }; diff --git a/lean_client/fork_choice/tests/unit_tests/validator.rs b/lean_client/fork_choice/tests/unit_tests/validator.rs index d945bda7..5b3e7ae6 100644 --- a/lean_client/fork_choice/tests/unit_tests/validator.rs +++ b/lean_client/fork_choice/tests/unit_tests/validator.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; use crate::unit_tests::common::create_test_store; use containers::{ - Attestation, AttestationData, Block, BlockBody, BlockWithAttestation, Checkpoint, Config, - SignatureKey, SignedBlockWithAttestation, Slot, State, Validator, + Attestation, AttestationData, Block, BlockBody, Checkpoint, Config, SignatureKey, SignedBlock, + Slot, State, Validator, }; use fork_choice::store::{Store, get_forkchoice_store, produce_block_with_signatures, update_head}; use rand::SeedableRng; @@ -21,9 +21,17 @@ fn create_test_store_with_signers() -> (Store, HashMap) { let mut rng = ChaChaRng::seed_from_u64(1337); let (validators, keys) = (0..10) .map(|index| { - let (pubkey, secret_key) = SecretKey::generate_key_pair(&mut rng, 0, 10); - - (Validator { index, pubkey }, (index, secret_key)) + let (attestation_pubkey, attest_sk) = SecretKey::generate_key_pair(&mut rng, 0, 10); + let (proposal_pubkey, _proposal_sk) = SecretKey::generate_key_pair(&mut rng, 0, 10); + + ( + Validator { + index, + attestation_pubkey, + proposal_pubkey, + }, + (index, attest_sk), + ) }) .unzip(); @@ -37,13 +45,8 @@ fn create_test_store_with_signers() -> (Store, HashMap) { body: BlockBody::default(), }; - let block_with_attestation = BlockWithAttestation { - block: block.clone(), - proposer_attestation: Attestation::default(), - }; - - let signed_block = SignedBlockWithAttestation { - message: block_with_attestation, + let signed_block = SignedBlock { + block, signature: Default::default(), }; @@ -62,7 +65,7 @@ fn test_produce_block_basic() { let validator_idx = 1; let (block_root, block, _signatures) = - produce_block_with_signatures(&mut store, slot, validator_idx) + produce_block_with_signatures(&mut store, slot, validator_idx, 1) .expect("block production should succeed"); // Verify block structure @@ -82,7 +85,7 @@ fn test_produce_block_unauthorized_proposer() { let slot = Slot(1); let wrong_validator = 2; // Not proposer for slot 1 - let result = produce_block_with_signatures(&mut store, slot, wrong_validator); + let result = produce_block_with_signatures(&mut store, slot, wrong_validator, 1); assert!(result.is_err()); let err = format!("{:?}", result.unwrap_err()); assert!( @@ -128,7 +131,7 @@ fn test_produce_block_with_attestations() { let slot = Slot(2); let validator_idx = 2; - let (_root, block, signatures) = produce_block_with_signatures(&mut store, slot, validator_idx) + let (_root, block, signatures) = produce_block_with_signatures(&mut store, slot, validator_idx, 1) .expect("block production should succeed"); // Block should include the 2 attestations we added (validators 5 and 6). @@ -159,7 +162,7 @@ fn test_produce_block_with_attestations() { .validators .get(vid) .expect("validator index out of range") - .pubkey + .attestation_pubkey .clone() }) .collect(); @@ -176,7 +179,7 @@ fn test_produce_block_sequential_slots() { // Produce block for slot 1 let (block1_root, block1, _sig1) = - produce_block_with_signatures(&mut store, Slot(1), 1).expect("block1 should succeed"); + produce_block_with_signatures(&mut store, Slot(1), 1, 1).expect("block1 should succeed"); // Verify first block is properly created assert_eq!(block1.slot, Slot(1)); @@ -190,7 +193,7 @@ fn test_produce_block_sequential_slots() { // Produce block for slot 2 (will build on genesis due to forkchoice) let (block2_root, block2, _sig2) = - produce_block_with_signatures(&mut store, Slot(2), 2).expect("block2 should succeed"); + produce_block_with_signatures(&mut store, Slot(2), 2, 1).expect("block2 should succeed"); // Verify block properties assert_eq!(block2.slot, Slot(2)); @@ -216,7 +219,7 @@ fn test_produce_block_empty_attestations() { let slot = Slot(3); let validator_idx = 3; - let (_root, block, _sig) = produce_block_with_signatures(&mut store, slot, validator_idx) + let (_root, block, _sig) = produce_block_with_signatures(&mut store, slot, validator_idx, 1) .expect("block production should succeed"); // Should produce valid block with empty attestations @@ -260,7 +263,7 @@ fn test_produce_block_state_consistency() { let validator_idx = 4; let (block_root, block, signatures) = - produce_block_with_signatures(&mut store, slot, validator_idx) + produce_block_with_signatures(&mut store, slot, validator_idx, 1) .expect("block production should succeed"); // Verify the stored state matches the block's state root @@ -289,7 +292,7 @@ fn test_produce_block_state_consistency() { .validators .get(vid) .expect("validator index out of range") - .pubkey + .attestation_pubkey .clone() }) .collect(); @@ -311,7 +314,7 @@ fn test_block_production_then_attestation() { // Proposer produces block for slot 1 let (_root, _block, _sig) = - produce_block_with_signatures(&mut store, Slot(1), 1).expect("block should succeed"); + produce_block_with_signatures(&mut store, Slot(1), 1, 1).expect("block should succeed"); // Update store state after block production update_head(&mut store); @@ -351,7 +354,7 @@ fn test_multiple_validators_coordination() { // Validator 1 produces block for slot 1 let (block1_root, block1, _sig1) = - produce_block_with_signatures(&mut store, Slot(1), 1).expect("block1 should succeed"); + produce_block_with_signatures(&mut store, Slot(1), 1, 1).expect("block1 should succeed"); let block1_hash = block1_root; // Validators 2-5 create attestations for slot 2 @@ -380,7 +383,7 @@ fn test_multiple_validators_coordination() { // After processing block1, head should be block1 (fork choice walks the tree) // So block2 will build on block1 let (block2_root, block2, _sig2) = - produce_block_with_signatures(&mut store, Slot(2), 2).expect("block2 should succeed"); + produce_block_with_signatures(&mut store, Slot(2), 2, 1).expect("block2 should succeed"); // Verify block properties assert_eq!(block2.slot, Slot(2)); @@ -412,7 +415,7 @@ fn test_validator_edge_cases() { let slot = Slot(9); // This validator's slot // Should be able to produce block - let (_root, block, _sig) = produce_block_with_signatures(&mut store, slot, max_validator) + let (_root, block, _sig) = produce_block_with_signatures(&mut store, slot, max_validator, 1) .expect("max validator block should succeed"); assert_eq!(block.proposer_index, max_validator); @@ -444,13 +447,8 @@ fn test_validator_operations_empty_store() { body: genesis_body, }; - let block_with_attestation = BlockWithAttestation { - block: genesis.clone(), - proposer_attestation: Attestation::default(), - }; - - let signed_block = SignedBlockWithAttestation { - message: block_with_attestation, + let signed_block = SignedBlock { + block: genesis, signature: Default::default(), }; @@ -458,7 +456,7 @@ fn test_validator_operations_empty_store() { // Should be able to produce block and attestation let (_root, block, _sig) = - produce_block_with_signatures(&mut store, Slot(1), 1).expect("block should succeed"); + produce_block_with_signatures(&mut store, Slot(1), 1, 1).expect("block should succeed"); let attestation_data = store .produce_attestation_data(Slot(1)) .expect("failed to produce attestation data"); @@ -481,7 +479,7 @@ fn test_produce_block_wrong_proposer() { let slot = Slot(5); let wrong_proposer = 3; // Should be validator 5 for slot 5 - let result = produce_block_with_signatures(&mut store, slot, wrong_proposer); + let result = produce_block_with_signatures(&mut store, slot, wrong_proposer, 1); assert!(result.is_err()); assert!(format!("{:?}", result.unwrap_err()).contains("is not the proposer for slot")); } @@ -509,7 +507,7 @@ fn test_produce_block_missing_parent_state() { // Missing head in get_proposal_head -> KeyError equivalent let result = std::panic::catch_unwind(|| { let mut s = store; - produce_block_with_signatures(&mut s, Slot(1), 1) + produce_block_with_signatures(&mut s, Slot(1), 1, 1) }); assert!(result.is_err()); } diff --git a/lean_client/networking/src/discovery/mod.rs b/lean_client/networking/src/discovery/mod.rs index 5d05d0b0..60580aa6 100644 --- a/lean_client/networking/src/discovery/mod.rs +++ b/lean_client/networking/src/discovery/mod.rs @@ -16,7 +16,7 @@ use libp2p_identity::{Keypair, PeerId}; use tokio::sync::mpsc; use tracing::{debug, info, warn}; -use crate::enr_ext::EnrExt; +use crate::enr_ext::{EnrExt, QUIC6_ENR_KEY, QUIC_ENR_KEY}; pub use config::DiscoveryConfig; @@ -130,13 +130,14 @@ impl DiscoveryService { .map(IpAddr::V4) .or_else(|| enr.ip6().map(IpAddr::V6))?; - // Try TCP ports first (lean_client stores QUIC port in TCP field), - // then fall back to QUIC ports (genesis tools may use quic field directly) + // Prefer the standard quic/quic6 ENR fields; fall back to tcp4/tcp6 for + // compatibility with peers running older lean_client builds that stored the + // QUIC port in the tcp field. let libp2p_port = enr - .tcp4() - .or_else(|| enr.tcp6()) - .or_else(|| enr.quic4()) - .or_else(|| enr.quic6())?; + .quic4() + .or_else(|| enr.quic6()) + .or_else(|| enr.tcp4()) + .or_else(|| enr.tcp6())?; let peer_id = enr_to_peer_id(enr)?; @@ -196,17 +197,16 @@ fn build_enr( ) -> Result> { let mut builder = EnrBuilder::default(); - // libp2p port is stored in tcp field, since Enr doesn't have a field for a quic port match ip { IpAddr::V4(ipv4) => { builder.ip4(ipv4); builder.udp4(udp_port); - builder.tcp4(libp2p_port); + builder.add_value(QUIC_ENR_KEY, &libp2p_port); } IpAddr::V6(ipv6) => { builder.ip6(ipv6); builder.udp6(udp_port); - builder.tcp6(libp2p_port); + builder.add_value(QUIC6_ENR_KEY, &libp2p_port); } } diff --git a/lean_client/networking/src/gossipsub/message.rs b/lean_client/networking/src/gossipsub/message.rs index 3f98e464..debc95c5 100644 --- a/lean_client/networking/src/gossipsub/message.rs +++ b/lean_client/networking/src/gossipsub/message.rs @@ -1,12 +1,12 @@ use crate::gossipsub::topic::GossipsubKind; use crate::gossipsub::topic::GossipsubTopic; -use containers::{SignedAggregatedAttestation, SignedAttestation, SignedBlockWithAttestation}; +use containers::{SignedAggregatedAttestation, SignedAttestation, SignedBlock}; use libp2p::gossipsub::TopicHash; use ssz::SszReadDefault as _; /// Devnet-3 gossipsub message types pub enum GossipsubMessage { - Block(SignedBlockWithAttestation), + Block(SignedBlock), /// Attestation from a specific subnet (devnet-3) AttestationSubnet { subnet_id: u64, @@ -20,7 +20,7 @@ impl GossipsubMessage { pub fn decode(topic: &TopicHash, data: &[u8]) -> Result { match GossipsubTopic::decode(topic)?.kind { GossipsubKind::Block => Ok(Self::Block( - SignedBlockWithAttestation::from_ssz_default(data) + SignedBlock::from_ssz_default(data) .map_err(|e| format!("{:?}", e))?, )), GossipsubKind::AttestationSubnet(subnet_id) => Ok(Self::AttestationSubnet { diff --git a/lean_client/networking/src/gossipsub/tests/config.rs b/lean_client/networking/src/gossipsub/tests/config.rs index a0fde9ae..4fa00d07 100644 --- a/lean_client/networking/src/gossipsub/tests/config.rs +++ b/lean_client/networking/src/gossipsub/tests/config.rs @@ -1,5 +1,5 @@ use crate::gossipsub::config::GossipsubConfig; -use crate::gossipsub::topic::{ATTESTATION_SUBNET_COUNT, GossipsubKind, get_subscription_topics}; +use crate::gossipsub::topic::{GossipsubKind, get_subscription_topics}; #[test] fn test_default_parameters() { @@ -39,13 +39,14 @@ fn test_default_parameters() { #[test] fn test_set_topics() { let mut config = GossipsubConfig::new(); - // Use aggregator mode to get all subnets for this test - let topics = get_subscription_topics("genesis".to_string(), Some(0), true); + // Use aggregator mode with validator 0, subnet_count=1 for this test + let subnet_count = 1u64; + let topics = get_subscription_topics("genesis".to_string(), &[0u64], true, &[], subnet_count); config.set_topics(topics.clone()); - // Block + Aggregation + ATTESTATION_SUBNET_COUNT subnets (no legacy Attestation) - let expected_count = 2 + ATTESTATION_SUBNET_COUNT as usize; + // Block + Aggregation + subnet_count subnets (no legacy Attestation) + let expected_count = 2 + subnet_count as usize; assert_eq!(config.topics.len(), expected_count); // Verify topics @@ -55,7 +56,7 @@ fn test_set_topics() { assert_eq!(config.topics[1].kind, GossipsubKind::Aggregation); // Verify subnet topics - for i in 0..ATTESTATION_SUBNET_COUNT as usize { + for i in 0..subnet_count as usize { assert_eq!(config.topics[2 + i].fork, "genesis"); assert_eq!( config.topics[2 + i].kind, diff --git a/lean_client/networking/src/gossipsub/tests/topic.rs b/lean_client/networking/src/gossipsub/tests/topic.rs index 97ee96e6..0e06ff74 100644 --- a/lean_client/networking/src/gossipsub/tests/topic.rs +++ b/lean_client/networking/src/gossipsub/tests/topic.rs @@ -1,8 +1,9 @@ use crate::gossipsub::topic::{ - AGGREGATION_TOPIC, ATTESTATION_SUBNET_COUNT, ATTESTATION_SUBNET_PREFIX, BLOCK_TOPIC, - GossipsubKind, GossipsubTopic, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX, compute_subnet_id, - get_subscription_topics, + AGGREGATION_TOPIC, ATTESTATION_SUBNET_PREFIX, BLOCK_TOPIC, GossipsubKind, GossipsubTopic, + SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX, compute_subnet_id, get_subscription_topics, }; + +const TEST_SUBNET_COUNT: u64 = 1; use libp2p::gossipsub::TopicHash; #[test] @@ -231,22 +232,62 @@ fn test_topic_hash_conversion() { } #[test] -fn test_get_subscription_topics_aggregator() { - // Aggregator should subscribe to ALL attestation subnets - let topics = get_subscription_topics("myfork".to_string(), Some(0), true); +fn test_get_subscription_topics_aggregator_with_validator() { + // Aggregator with a registered validator subscribes to the validator-derived subnet. + let topics = + get_subscription_topics("myfork".to_string(), &[0u64], true, &[], TEST_SUBNET_COUNT); + + // Block + Aggregation + derived attestation subnet(s) + let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect(); + assert!(kinds.contains(&GossipsubKind::Block)); + assert!(kinds.contains(&GossipsubKind::Aggregation)); + assert!(kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id( + 0, + TEST_SUBNET_COUNT + )))); - // Block + Aggregation + all attestation subnets - let expected_count = 2 + ATTESTATION_SUBNET_COUNT as usize; - assert_eq!(topics.len(), expected_count); + for topic in &topics { + assert_eq!(topic.fork, "myfork"); + } +} + +#[test] +fn test_get_subscription_topics_aggregator_no_validator_fallback() { + // Aggregator with no registered validators falls back to subnet 0. + let topics = + get_subscription_topics("myfork".to_string(), &[], true, &[], TEST_SUBNET_COUNT); let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect(); assert!(kinds.contains(&GossipsubKind::Block)); assert!(kinds.contains(&GossipsubKind::Aggregation)); + assert!(kinds.contains(&GossipsubKind::AttestationSubnet(0))); - // All attestation subnets should be present - for subnet_id in 0..ATTESTATION_SUBNET_COUNT { - assert!(kinds.contains(&GossipsubKind::AttestationSubnet(subnet_id))); + for topic in &topics { + assert_eq!(topic.fork, "myfork"); } +} + +#[test] +fn test_get_subscription_topics_aggregator_explicit_subnets() { + // Aggregator with explicit aggregate_subnet_ids subscribes to both validator-derived + // and the explicit subnets. + let topics = get_subscription_topics( + "myfork".to_string(), + &[0u64], + true, + &[1u64, 2u64], + TEST_SUBNET_COUNT, + ); + + let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect(); + assert!(kinds.contains(&GossipsubKind::Block)); + assert!(kinds.contains(&GossipsubKind::Aggregation)); + assert!(kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id( + 0, + TEST_SUBNET_COUNT + )))); + assert!(kinds.contains(&GossipsubKind::AttestationSubnet(1))); + assert!(kinds.contains(&GossipsubKind::AttestationSubnet(2))); for topic in &topics { assert_eq!(topic.fork, "myfork"); @@ -255,21 +296,48 @@ fn test_get_subscription_topics_aggregator() { #[test] fn test_get_subscription_topics_non_aggregator_validator() { - // Non-aggregator validator should subscribe to only their own subnet + // Non-aggregator validator subscribes only to their own derived subnet. let validator_id = 5u64; - let topics = get_subscription_topics("myfork".to_string(), Some(validator_id), false); + let topics = get_subscription_topics( + "myfork".to_string(), + &[validator_id], + false, + &[], + TEST_SUBNET_COUNT, + ); // Block + Aggregation + only one attestation subnet - let expected_count = 3; - assert_eq!(topics.len(), expected_count); + assert_eq!(topics.len(), 3); let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect(); assert!(kinds.contains(&GossipsubKind::Block)); assert!(kinds.contains(&GossipsubKind::Aggregation)); + assert!(kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id( + validator_id, + TEST_SUBNET_COUNT + )))); + + for topic in &topics { + assert_eq!(topic.fork, "myfork"); + } +} + +#[test] +fn test_get_subscription_topics_non_aggregator_multi_validator() { + // Non-aggregator with multiple validators subscribes to each derived subnet (deduped). + let topics = get_subscription_topics( + "myfork".to_string(), + &[0u64, 1u64, 2u64], + false, + &[], + TEST_SUBNET_COUNT, + ); - // Only the validator's own subnet should be present - let expected_subnet = compute_subnet_id(validator_id); - assert!(kinds.contains(&GossipsubKind::AttestationSubnet(expected_subnet))); + let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect(); + assert!(kinds.contains(&GossipsubKind::Block)); + assert!(kinds.contains(&GossipsubKind::Aggregation)); + // All three validators map to the same subnet (N % 1 = 0) with subnet_count=1 + assert!(kinds.contains(&GossipsubKind::AttestationSubnet(0))); for topic in &topics { assert_eq!(topic.fork, "myfork"); @@ -277,21 +345,22 @@ fn test_get_subscription_topics_non_aggregator_validator() { } #[test] -fn test_get_subscription_topics_non_validator() { - // Non-validator node (no validator_id) should subscribe to ALL subnets for general sync - let topics = get_subscription_topics("myfork".to_string(), None, false); +fn test_get_subscription_topics_non_validator_skips_attestation() { + // Non-validator, non-aggregator node subscribes to NO attestation topics (saves bandwidth). + // This aligns with leanSpec PR #482: subnet filtering at the p2p subscription layer. + let topics = + get_subscription_topics("myfork".to_string(), &[], false, &[], TEST_SUBNET_COUNT); - // Block + Aggregation + all attestation subnets - let expected_count = 2 + ATTESTATION_SUBNET_COUNT as usize; - assert_eq!(topics.len(), expected_count); + // Block + Aggregation only — no attestation subnets + assert_eq!(topics.len(), 2); let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect(); assert!(kinds.contains(&GossipsubKind::Block)); assert!(kinds.contains(&GossipsubKind::Aggregation)); - // All attestation subnets should be present - for subnet_id in 0..ATTESTATION_SUBNET_COUNT { - assert!(kinds.contains(&GossipsubKind::AttestationSubnet(subnet_id))); + // Confirm no attestation topic is present + for kind in &kinds { + assert!(!matches!(kind, GossipsubKind::AttestationSubnet(_))); } for topic in &topics { diff --git a/lean_client/networking/src/gossipsub/topic.rs b/lean_client/networking/src/gossipsub/topic.rs index f5766687..e30872c7 100644 --- a/lean_client/networking/src/gossipsub/topic.rs +++ b/lean_client/networking/src/gossipsub/topic.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use libp2p::gossipsub::{IdentTopic, TopicHash}; pub const TOPIC_PREFIX: &str = "leanconsensus"; @@ -7,13 +9,10 @@ pub const BLOCK_TOPIC: &str = "block"; pub const ATTESTATION_SUBNET_PREFIX: &str = "attestation_"; pub const AGGREGATION_TOPIC: &str = "aggregation"; -/// Number of attestation subnets (devnet-3) -pub const ATTESTATION_SUBNET_COUNT: u64 = 1; - /// Compute the subnet ID for a validator (devnet-3) -/// Subnet assignment: validator_id % ATTESTATION_SUBNET_COUNT -pub fn compute_subnet_id(validator_id: u64) -> u64 { - validator_id % ATTESTATION_SUBNET_COUNT +/// Subnet assignment: validator_id % subnet_count +pub fn compute_subnet_id(validator_id: u64, subnet_count: u64) -> u64 { + validator_id % subnet_count.max(1) } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -46,17 +45,20 @@ impl GossipsubKind { /// Get gossipsub topics for subscription based on validator role. /// -/// Topic subscription rules: -/// - Block and Aggregation topics: Always subscribed +/// Subscription rules (aligned with leanSpec PR #482): +/// - Block and Aggregation topics: always subscribed. /// - Attestation subnet topics: -/// - If `is_aggregator` is true: Subscribe to ALL attestation subnets (needed for aggregation) -/// - If `is_aggregator` is false and `validator_id` is Some: Subscribe only to the validator's -/// own subnet (validator_id % ATTESTATION_SUBNET_COUNT) for publishing attestations -/// - If `validator_id` is None: Subscribe to all subnets (non-validator node for general sync) +/// - All nodes with registered validators subscribe to each validator's derived subnet. +/// - Aggregators additionally subscribe to any explicit `aggregate_subnet_ids`. +/// - Aggregators with no registered validators fall back to subnet 0. +/// - Non-aggregator nodes with no validators subscribe to no attestation topics +/// (subnet filtering happens at the p2p subscription layer). pub fn get_subscription_topics( fork: String, - validator_id: Option, + validator_ids: &[u64], is_aggregator: bool, + aggregate_subnet_ids: &[u64], + subnet_count: u64, ) -> Vec { let mut topics = vec![ GossipsubTopic { @@ -69,29 +71,34 @@ pub fn get_subscription_topics( }, ]; + // Build the set of attestation subnets to subscribe to. + let mut subscription_subnets: HashSet = HashSet::new(); + + // All nodes with registered validators subscribe to each validator's derived subnet. + for &vid in validator_ids { + subscription_subnets.insert(compute_subnet_id(vid, subnet_count)); + } + + // Aggregators add explicit subnet IDs on top of validator-derived ones. + // If the aggregator has no registered validators, fall back to subnet 0. if is_aggregator { - // Aggregators subscribe to ALL attestation subnets to collect attestations for aggregation - for subnet_id in 0..ATTESTATION_SUBNET_COUNT { - topics.push(GossipsubTopic { - fork: fork.clone(), - kind: GossipsubKind::AttestationSubnet(subnet_id), - }); + if subscription_subnets.is_empty() { + subscription_subnets.insert(0); } - } else if let Some(vid) = validator_id { - // Non-aggregator validators subscribe only to their own subnet for publishing attestations - let subnet_id = compute_subnet_id(vid); + for &sid in aggregate_subnet_ids { + subscription_subnets.insert(sid); + } + } + + // Subscribe to each resolved attestation subnet. + // Non-validator/non-aggregator nodes end up with an empty set → no attestation topics. + let mut sorted_subnets: Vec = subscription_subnets.into_iter().collect(); + sorted_subnets.sort_unstable(); + for subnet_id in sorted_subnets { topics.push(GossipsubTopic { fork: fork.clone(), kind: GossipsubKind::AttestationSubnet(subnet_id), }); - } else { - // Non-validator nodes subscribe to all subnets for general network participation - for subnet_id in 0..ATTESTATION_SUBNET_COUNT { - topics.push(GossipsubTopic { - fork: fork.clone(), - kind: GossipsubKind::AttestationSubnet(subnet_id), - }); - } } topics diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index dee08a82..4c074aac 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -566,15 +566,15 @@ where Event::Message { message, .. } => { match GossipsubMessage::decode(&message.topic, &message.data) { - Ok(GossipsubMessage::Block(signed_block_with_attestation)) => { - info!(block_root = %signed_block_with_attestation.message.block.hash_tree_root(), "received block via gossip"); + Ok(GossipsubMessage::Block(signed_block)) => { + info!(block_root = %signed_block.block.hash_tree_root(), "received block via gossip"); - let slot = signed_block_with_attestation.message.block.slot.0; + let slot = signed_block.block.slot.0; if let Err(err) = self .chain_message_sink .send(ChainMessage::ProcessBlock { - signed_block_with_attestation, + signed_block, is_trusted: false, should_gossip: true, }) @@ -602,7 +602,7 @@ where .send(ChainMessage::ProcessAttestation { signed_attestation: attestation, is_trusted: false, - should_gossip: true, + should_gossip: false, }) .await { @@ -788,7 +788,7 @@ where { let mut provider = self.signed_block_provider.write(); for block in &blocks { - let root = block.message.block.hash_tree_root(); + let root = block.block.hash_tree_root(); provider.insert(root, block.clone()); } // Hard cap: evict lowest-slot blocks if still over limit. @@ -796,7 +796,7 @@ where let to_remove = provider.len() - MAX_BLOCK_CACHE_SIZE; let mut slots: Vec<(H256, u64)> = provider .iter() - .map(|(root, b)| (*root, b.message.block.slot.0)) + .map(|(root, b)| (*root, b.block.slot.0)) .collect(); slots.sort_by_key(|(_, slot)| *slot); for (root, _) in slots.into_iter().take(to_remove) { @@ -818,7 +818,7 @@ where blocks .iter() .filter_map(|block| { - let parent_root = block.message.block.parent_root; + let parent_root = block.block.parent_root; if parent_root.is_zero() { return None; } @@ -867,10 +867,10 @@ where let chain_sink = self.chain_message_sink.clone(); tokio::spawn(async move { for block in blocks { - let slot = block.message.block.slot.0; + let slot = block.block.slot.0; if let Err(e) = chain_sink .send(ChainMessage::ProcessBlock { - signed_block_with_attestation: block, + signed_block: block, is_trusted: false, should_gossip: false, }) @@ -1185,18 +1185,18 @@ where async fn dispatch_outbound_request(&mut self, request: OutboundP2pRequest) { match request { - OutboundP2pRequest::GossipBlockWithAttestation(signed_block_with_attestation) => { - let slot = signed_block_with_attestation.message.block.slot.0; - match signed_block_with_attestation.to_ssz() { + OutboundP2pRequest::GossipBlock(signed_block) => { + let slot = signed_block.block.slot.0; + match signed_block.to_ssz() { Ok(bytes) => { if let Err(err) = self.publish_to_topic(GossipsubKind::Block, bytes) { - warn!(slot = slot, ?err, "Publish block with attestation failed"); + warn!(slot = slot, ?err, "Publish block failed"); } else { - info!(slot = slot, "Broadcasted block with attestation"); + info!(slot = slot, "Broadcasted block"); } } Err(err) => { - warn!(slot = slot, ?err, "Serialize block with attestation failed"); + warn!(slot = slot, ?err, "Serialize block failed"); } } } diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index 1768e093..3cc1cdc3 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -2,7 +2,7 @@ use std::io; use std::io::{Read, Write}; use async_trait::async_trait; -use containers::{SignedBlockWithAttestation, Status}; +use containers::{SignedBlock, Status}; use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use libp2p::request_response::{ Behaviour as RequestResponse, Codec, Config, Event, ProtocolSupport, @@ -50,7 +50,7 @@ pub enum LeanRequest { #[derive(Debug, Clone)] pub enum LeanResponse { Status(Status), - BlocksByRoot(Vec), + BlocksByRoot(Vec), Empty, } @@ -395,7 +395,7 @@ impl LeanCodec { } let block = - SignedBlockWithAttestation::from_ssz_default(&ssz_bytes).map_err(|e| { + SignedBlock::from_ssz_default(&ssz_bytes).map_err(|e| { io::Error::new( io::ErrorKind::Other, format!("SSZ decode Block failed: {e:?}"), diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index a3690967..ef8e360c 100644 --- a/lean_client/networking/src/types.rs +++ b/lean_client/networking/src/types.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow}; use async_trait::async_trait; use containers::{ AggregatedSignatureProof, AttestationData, Block, SignedAggregatedAttestation, - SignedAttestation, SignedBlockWithAttestation, Slot, Status, + SignedAttestation, SignedBlock, Slot, Status, }; use metrics::METRICS; use parking_lot::{Mutex, RwLock}; @@ -21,7 +21,7 @@ pub const MAX_BLOCK_CACHE_SIZE: usize = 1024; /// Shared block provider for serving BlocksByRoot requests. /// Allows NetworkService to look up signed blocks for checkpoint sync backfill. -pub type SignedBlockProvider = Arc>>; +pub type SignedBlockProvider = Arc>>; /// Shared status provider for Status req/resp protocol. /// Allows NetworkService to send accurate finalized/head checkpoints to peers. @@ -145,7 +145,7 @@ impl PeerCount { #[derive(Debug, Clone)] pub enum ChainMessage { ProcessBlock { - signed_block_with_attestation: SignedBlockWithAttestation, + signed_block: SignedBlock, is_trusted: bool, should_gossip: bool, }, @@ -163,11 +163,9 @@ pub enum ChainMessage { } impl ChainMessage { - pub fn block_with_attestation( - signed_block_with_attestation: SignedBlockWithAttestation, - ) -> Self { + pub fn block(signed_block: SignedBlock) -> Self { ChainMessage::ProcessBlock { - signed_block_with_attestation, + signed_block, is_trusted: false, should_gossip: true, } @@ -186,14 +184,10 @@ impl Display for ChainMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ChainMessage::ProcessBlock { - signed_block_with_attestation, + signed_block, .. } => { - write!( - f, - "ProcessBlockWithAttestation(slot={})", - signed_block_with_attestation.message.block.slot.0 - ) + write!(f, "ProcessBlock(slot={})", signed_block.block.slot.0) } ChainMessage::ProcessAttestation { signed_attestation, .. @@ -244,7 +238,7 @@ pub enum ValidatorChainMessage { #[derive(Debug, Clone)] pub enum OutboundP2pRequest { - GossipBlockWithAttestation(SignedBlockWithAttestation), + GossipBlock(SignedBlock), /// Devnet-3: Gossip attestation to subnet-specific topic /// Contains (attestation, subnet_id) GossipAttestation(SignedAttestation, u64), diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index 43463ecd..c8fd3bed 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -1,15 +1,16 @@ use anyhow::{Context as _, Result}; use clap::Parser; use containers::{ - Attestation, AttestationData, Block, BlockBody, BlockHeader, BlockSignatures, - BlockWithAttestation, Checkpoint, Config, SignedBlockWithAttestation, Slot, State, Status, - Validator, + Block, BlockBody, BlockHeader, BlockSignatures, Checkpoint, Config, SignedBlock, Slot, State, + Status, Validator, }; use ethereum_types::H256; use features::Feature; use fork_choice::{ block_cache::BlockCache, - handlers::{on_aggregated_attestation, on_attestation, on_block, on_tick}, + handlers::{ + on_aggregated_attestation, on_attestation, on_block, on_gossip_attestation, on_tick, + }, store::{ INTERVALS_PER_SLOT, MILLIS_PER_INTERVAL, Store, execute_block_production, get_forkchoice_store, prepare_block_production, @@ -145,15 +146,12 @@ fn verify_checkpoint_state(state: &State, genesis_state: &State) -> Result<()> { // Verify each validator pubkey matches genesis for i in 0..state_validator_count { - let state_pubkey = &state.validators.get(i).expect("validator exists").pubkey; - let genesis_pubkey = &genesis_state - .validators - .get(i) - .expect("validator exists") - .pubkey; + let sv = state.validators.get(i).expect("validator exists"); + let gv = genesis_state.validators.get(i).expect("validator exists"); anyhow::ensure!( - state_pubkey == genesis_pubkey, + sv.attestation_pubkey == gv.attestation_pubkey + && sv.proposal_pubkey == gv.proposal_pubkey, "Validator pubkey mismatch at index {i}: checkpoint has different validator set. Wrong network?" ); } @@ -373,6 +371,11 @@ struct Args { #[arg(long = "is-aggregator", default_value_t = false)] is_aggregator: bool, + /// Comma-separated attestation subnet IDs to additionally subscribe and aggregate from + /// (e.g. "0,1,2"). Requires --is-aggregator. Additive to validator-derived subnets. + #[arg(long = "aggregate-subnet-ids", value_delimiter = ',')] + aggregate_subnet_ids: Vec, + /// Override attestation committee count (devnet-3) /// When set, uses this value instead of the hardcoded default #[arg(long = "attestation-committee-count")] @@ -418,13 +421,11 @@ async fn main() -> Result<()> { .transpose() .context("failed to set metrics on start")?; - // Record aggregator and network metrics on startup + // Record aggregator metric on startup (committee count set after genesis is loaded). METRICS.get().map(|metrics| { metrics .lean_is_aggregator .set(if args.is_aggregator { 1 } else { 0 }); - // ATTESTATION_SUBNET_COUNT is 1 in current implementation - metrics.lean_attestation_committee_count.set(1); }); let (outbound_p2p_sender, outbound_p2p_receiver) = @@ -437,34 +438,60 @@ async fn main() -> Result<()> { let (validator_chain_sender, mut validator_chain_receiver) = mpsc::unbounded_channel::(); - let (genesis_time, validators) = if let Some(genesis_path) = &args.genesis { - let genesis_config = containers::GenesisConfig::load_from_file(genesis_path) - .expect("Failed to load genesis config"); - - let validators: Vec = genesis_config - .genesis_validators - .iter() - .enumerate() - .map(|(i, v_str)| { - let pubkey: PublicKey = v_str.parse().expect("Invalid genesis validator pubkey"); - Validator { - pubkey, + let (genesis_time, validators, genesis_log_inv_rate, genesis_attestation_committee_count) = + if let Some(genesis_path) = &args.genesis { + let genesis_config = containers::GenesisConfig::load_from_file(genesis_path) + .expect("Failed to load genesis config"); + + let validators: Vec = genesis_config + .genesis_validators + .iter() + .enumerate() + .map(|(i, entry)| { + let attestation_pubkey: PublicKey = entry + .attestation_pubkey + .parse() + .expect("Invalid genesis validator attestation_pubkey"); + let proposal_pubkey: PublicKey = entry + .proposal_pubkey + .parse() + .expect("Invalid genesis validator proposal_pubkey"); + Validator { + attestation_pubkey, + proposal_pubkey, + index: i as u64, + } + }) + .collect(); + + ( + genesis_config.genesis_time, + validators, + genesis_config.log_inv_rate, + genesis_config.attestation_committee_count, + ) + } else { + let num_validators = 3; + let validators = (0..num_validators) + .map(|i| Validator { + attestation_pubkey: PublicKey::default(), + proposal_pubkey: PublicKey::default(), index: i as u64, - } - }) - .collect(); + }) + .collect(); + (1763757427, validators, 2u8, 1u64) + }; - (genesis_config.genesis_time, validators) - } else { - let num_validators = 3; - let validators = (0..num_validators) - .map(|i| Validator { - pubkey: PublicKey::default(), - index: i as u64, - }) - .collect(); - (1763757427, validators) - }; + // CLI --attestation-committee-count overrides genesis config; both fall back to 1. + let attestation_committee_count = args + .attestation_committee_count + .unwrap_or(genesis_attestation_committee_count) + .max(1); + METRICS.get().map(|metrics| { + metrics + .lean_attestation_committee_count + .set(attestation_committee_count as i64); + }); let genesis_state = State::generate_genesis_with_validators(genesis_time, validators); @@ -478,29 +505,8 @@ async fn main() -> Result<()> { }, }; - let genesis_proposer_attestation = Attestation { - validator_id: 0, - data: AttestationData { - slot: Slot(0), - head: Checkpoint { - root: H256::zero(), - slot: Slot(0), - }, - target: Checkpoint { - root: H256::zero(), - slot: Slot(0), - }, - source: Checkpoint { - root: H256::zero(), - slot: Slot(0), - }, - }, - }; - let genesis_signed_block = SignedBlockWithAttestation { - message: BlockWithAttestation { - block: genesis_block, - proposer_attestation: genesis_proposer_attestation, - }, + let genesis_signed_block = SignedBlock { + block: genesis_block, signature: BlockSignatures { attestation_signatures: PersistentList::default(), proposer_signature: Signature::default(), @@ -653,14 +659,22 @@ async fn main() -> Result<()> { // Validator task needs to send ProcessBlock / ProcessAttestation back to the chain task let chain_msg_sender_for_validator = chain_message_sender.clone(); - // Extract first validator ID for subnet subscription and metrics - let first_validator_id: Option = validator_service + // Validate: --aggregate-subnet-ids requires --is-aggregator + if !args.aggregate_subnet_ids.is_empty() && !args.is_aggregator { + eprintln!("error: --aggregate-subnet-ids requires --is-aggregator to be set"); + std::process::exit(1); + } + + // Collect all registered validator indices for subnet subscription. + // Per leanSpec PR #482: every validator's derived subnet is subscribed, not just the first. + let validator_ids: Vec = validator_service .as_ref() - .and_then(|service| service.config.validator_indices.first().copied()); + .map(|service| service.config.validator_indices.clone()) + .unwrap_or_default(); - // Record validator subnet metric if validator is configured - if let Some(validator_id) = first_validator_id { - let subnet_id = compute_subnet_id(validator_id); + // Record subnet metric for the first validator if available. + if let Some(&first_vid) = validator_ids.first() { + let subnet_id = compute_subnet_id(first_vid, attestation_committee_count); METRICS.get().map(|metrics| { metrics .lean_attestation_committee_subnet @@ -669,11 +683,18 @@ async fn main() -> Result<()> { } let fork = "devnet0".to_string(); - // Subscribe to topics based on validator role: - // - Aggregators: all attestation subnets - // - Non-aggregator validators: only their own subnet - // - Non-validators: all subnets for general sync - let gossipsub_topics = get_subscription_topics(fork, first_validator_id, args.is_aggregator); + // Subscribe to topics based on validator role (leanSpec PR #482): + // - All validators: subscribe to each validator's derived subnet + // - Aggregators: additionally subscribe to explicit aggregate_subnet_ids + // - Aggregators with no validators: fall back to subnet 0 + // - Non-validators/non-aggregators: skip attestation subscriptions entirely + let gossipsub_topics = get_subscription_topics( + fork, + &validator_ids, + args.is_aggregator, + &args.aggregate_subnet_ids, + attestation_committee_count, + ); let mut gossipsub_config = GossipsubConfig::new(); gossipsub_config.set_topics(gossipsub_topics); @@ -779,9 +800,7 @@ async fn main() -> Result<()> { // Abort with a clear error if no valid block arrives within the timeout. const ANCHOR_BLOCK_TIMEOUT_SECS: u64 = 300; - let anchor_block: SignedBlockWithAttestation = if let Some(expected_root) = - checkpoint_block_root - { + let anchor_block: SignedBlock = if let Some(expected_root) = checkpoint_block_root { info!( block_root = %format!("0x{:x}", expected_root), timeout_secs = ANCHOR_BLOCK_TIMEOUT_SECS, @@ -805,28 +824,26 @@ async fn main() -> Result<()> { "Chain message channel closed during anchor block wait" )); }; - if let ChainMessage::ProcessBlock { signed_block_with_attestation, .. } = msg { - let root = signed_block_with_attestation.message.block.hash_tree_root(); + if let ChainMessage::ProcessBlock { signed_block, .. } = msg { + let root = signed_block.block.hash_tree_root(); if root == expected_root { // Root match guarantees slot, proposer_index, parent_root, // state_root, and body contents (via body_root). // proposer_signature is NOT covered by the hash — verify it // explicitly so a peer serving a validly-hashed but unsigned // block cannot become our anchor. - match signed_block_with_attestation - .verify_signatures(anchor_state.clone()) - { + match signed_block.verify_signatures(anchor_state.clone()) { Ok(()) => { info!( - slot = signed_block_with_attestation.message.block.slot.0, + slot = signed_block.block.slot.0, block_root = %format!("0x{:x}", root), "Anchor block received and verified — initialising fork-choice store" ); - return Ok(signed_block_with_attestation); + return Ok(signed_block); } Err(e) => { warn!( - slot = signed_block_with_attestation.message.block.slot.0, + slot = signed_block.block.slot.0, block_root = %format!("0x{:x}", root), error = %e, "Anchor block signature verification failed — \ @@ -837,7 +854,7 @@ async fn main() -> Result<()> { } } else { debug!( - slot = signed_block_with_attestation.message.block.slot.0, + slot = signed_block.block.slot.0, root = %format!("0x{:x}", root), "Waiting for anchor block — discarding non-anchor block" ); @@ -880,7 +897,8 @@ async fn main() -> Result<()> { let store = Arc::new(RwLock::new(get_forkchoice_store( anchor_state.clone(), anchor_block, - config, + config.clone(), + args.is_aggregator, ))); // Seed the block provider so we can serve the anchor block to peers via BlocksByRoot. @@ -930,6 +948,7 @@ async fn main() -> Result<()> { Duration::from_millis((genesis_millis + next * MILLIS_PER_INTERVAL).saturating_sub(now)) }; + let chain_log_inv_rate = genesis_log_inv_rate as usize; let chain_handle = task::spawn(async move { let mut tick_interval = interval_at( Instant::now() + genesis_tick_delay, @@ -976,8 +995,23 @@ async fn main() -> Result<()> { match current_interval { 0 | 1 => {} 2 => { - if let Some(ref vs) = vs_for_chain { - if let Some(aggregations) = vs.maybe_aggregate(&*store.read(), Slot(current_slot)) { + if let Some(vs) = vs_for_chain.clone() { + // Snapshot the store before entering spawn_blocking so the + // read lock is released immediately. XMSS aggregation is + // CPU-intensive (1-3 s); offloading it to spawn_blocking + // keeps the async executor free for QUIC keepalives and + // network I/O, preventing peer timeouts. + let store_snapshot = store.read().clone(); + let maybe_agg = task::spawn_blocking(move || { + vs.maybe_aggregate( + &store_snapshot, + Slot(current_slot), + chain_log_inv_rate, + ) + }) + .await + .unwrap_or(None); + if let Some((aggregations, consumed_data_roots)) = maybe_agg { for aggregation in aggregations { if let Err(e) = chain_outbound_sender.send( OutboundP2pRequest::GossipAggregation(aggregation) @@ -985,6 +1019,11 @@ async fn main() -> Result<()> { warn!("Failed to gossip aggregation: {}", e); } } + // Remove consumed raw gossip signatures so + // they are not re-aggregated in future rounds. + store.write().gossip_signatures.retain(|key, _| { + !consumed_data_roots.contains(&key.data_root) + }); info!(slot = current_slot, tick = store.read().time, "Aggregation phase - broadcast aggregated attestations"); } else { info!(slot = current_slot, tick = store.read().time, "Aggregation phase - no aggregation duty or no attestations"); @@ -1018,23 +1057,23 @@ async fn main() -> Result<()> { let Some(message) = message else { break }; match message { ChainMessage::ProcessBlock { - signed_block_with_attestation, + signed_block, is_trusted, should_gossip, } => { if should_gossip && !is_trusted && !sync_state.accepts_gossip() { debug!( state = ?sync_state, - slot = signed_block_with_attestation.message.block.slot.0, + slot = signed_block.block.slot.0, "Dropping gossip block: sync state does not accept gossip" ); continue; } - let block_slot = signed_block_with_attestation.message.block.slot; - let proposer = signed_block_with_attestation.message.block.proposer_index; - let block_root = signed_block_with_attestation.message.block.hash_tree_root(); - let parent_root = signed_block_with_attestation.message.block.parent_root; + let block_slot = signed_block.block.slot; + let proposer = signed_block.block.proposer_index; + let block_root = signed_block.block.hash_tree_root(); + let parent_root = signed_block.block.parent_root; info!( slot = block_slot.0, @@ -1061,13 +1100,13 @@ async fn main() -> Result<()> { // requests a block we received but haven't incorporated yet. { let mut provider = signed_block_provider.write(); - provider.insert(block_root, signed_block_with_attestation.clone()); + provider.insert(block_root, signed_block.clone()); // Hard cap: evict lowest-slot blocks if still over limit. if provider.len() > MAX_BLOCK_CACHE_SIZE { let to_remove = provider.len() - MAX_BLOCK_CACHE_SIZE; let mut slots: Vec<(H256, u64)> = provider .iter() - .map(|(root, b)| (*root, b.message.block.slot.0)) + .map(|(root, b)| (*root, b.block.slot.0)) .collect(); slots.sort_by_key(|(_, slot)| *slot); for (root, _) in slots.into_iter().take(to_remove) { @@ -1078,7 +1117,7 @@ async fn main() -> Result<()> { if !parent_exists { block_cache.add( - signed_block_with_attestation.clone(), + signed_block.clone(), block_root, parent_root, block_slot, @@ -1115,7 +1154,7 @@ async fn main() -> Result<()> { continue; } - let result = {on_block(&mut *store.write(), &mut block_cache, signed_block_with_attestation.clone())}; + let result = {on_block(&mut *store.write(), &mut block_cache, signed_block.clone())}; match result { Ok(()) => { info!("Block processed successfully"); @@ -1132,7 +1171,7 @@ async fn main() -> Result<()> { if should_gossip { if let Err(e) = outbound_p2p_sender.send( - OutboundP2pRequest::GossipBlockWithAttestation(signed_block_with_attestation) + OutboundP2pRequest::GossipBlock(signed_block) ) { warn!("Failed to gossip block: {}", e); } else { @@ -1187,10 +1226,15 @@ async fn main() -> Result<()> { validator_id ); - match on_attestation(&mut *store.write(), signed_attestation.clone(), false) { + let result = if is_trusted { + on_attestation(&mut *store.write(), signed_attestation.clone(), false) + } else { + on_gossip_attestation(&mut *store.write(), signed_attestation.clone()) + }; + match result { Ok(()) => { if should_gossip { - let subnet_id = compute_subnet_id(validator_id); + let subnet_id = compute_subnet_id(validator_id, attestation_committee_count); if let Err(e) = outbound_p2p_sender.send( OutboundP2pRequest::GossipAttestation(signed_attestation, subnet_id) ) { @@ -1282,7 +1326,7 @@ async fn main() -> Result<()> { ValidatorChainMessage::ProduceBlock { slot, proposer_index, sender } => { let prepare_result = { let mut w = store.write(); - prepare_block_production(&mut *w, slot, proposer_index) + prepare_block_production(&mut *w, slot, proposer_index, chain_log_inv_rate) }; match prepare_result { @@ -1387,44 +1431,9 @@ async fn main() -> Result<()> { } }; - let (atx, arx) = oneshot::channel(); - if validator_chain_sender - .send(ValidatorChainMessage::BuildAttestationData { - slot: Slot(current_slot), - sender: atx, - }) - .is_err() - { - warn!("Validator task: chain channel closed, stopping"); - break; - } - - let attestation_data = match arx.await { - Ok(Ok(data)) => data, - Ok(Err(e)) => { - warn!(slot = current_slot, error = %e, "Validator task: chain failed to build attestation data"); - last_proposal_slot = Some(current_slot); - continue; - } - Err(_) => { - warn!( - slot = current_slot, - "Validator task: no response to BuildAttestationData" - ); - last_proposal_slot = Some(current_slot); - continue; - } - }; - - match vs.sign_block_with_data( - block, - proposer_idx, - signatures, - attestation_data, - ) { + match vs.sign_block_with_data(block, proposer_idx, signatures) { Ok(signed_block) => { - let block_root = - signed_block.message.block.hash_tree_root(); + let block_root = signed_block.block.hash_tree_root(); info!( slot = current_slot, block_root = %format!("0x{:x}", block_root), @@ -1432,7 +1441,7 @@ async fn main() -> Result<()> { ); if chain_msg_sender_for_validator .send(ChainMessage::ProcessBlock { - signed_block_with_attestation: signed_block, + signed_block, is_trusted: true, should_gossip: true, }) @@ -1483,7 +1492,10 @@ async fn main() -> Result<()> { continue; } let validator_id = signed_att.validator_id; - let subnet_id = compute_subnet_id(validator_id); + let subnet_id = compute_subnet_id( + validator_id, + attestation_committee_count, + ); info!( slot = current_slot, validator = validator_id, diff --git a/lean_client/validator/Cargo.toml b/lean_client/validator/Cargo.toml index 6d39568f..747a15f6 100644 --- a/lean_client/validator/Cargo.toml +++ b/lean_client/validator/Cargo.toml @@ -9,6 +9,7 @@ env-config = { workspace = true } ethereum-types = { workspace = true } fork_choice = { workspace = true } metrics = { workspace = true } +serde = { workspace = true } serde_yaml = { workspace = true } ssz = { workspace = true } tracing = { workspace = true } diff --git a/lean_client/validator/src/keys.rs b/lean_client/validator/src/keys.rs index 87c5f581..e8cb95a3 100644 --- a/lean_client/validator/src/keys.rs +++ b/lean_client/validator/src/keys.rs @@ -7,10 +7,12 @@ use anyhow::anyhow; use anyhow::{Context, Result, ensure}; use xmss::{SecretKey, Signature}; -/// Manages XMSS secret keys for validators +/// Manages XMSS secret keys for validators (attestation + proposal keys per validator) pub struct KeyManager { - /// Map of validator index to secret key bytes - keys: HashMap, + /// Map of validator index to attestation secret key + attestation_keys: HashMap, + /// Map of validator index to proposal secret key + proposal_keys: HashMap, /// Path to keys directory keys_dir: PathBuf, } @@ -25,46 +27,108 @@ impl KeyManager { info!(path = ?keys_dir, "Initializing key manager"); Ok(KeyManager { - keys: HashMap::new(), + attestation_keys: HashMap::new(), + proposal_keys: HashMap::new(), keys_dir, }) } - /// Load a secret key for a specific validator index - pub fn load_key(&mut self, validator_index: u64) -> Result<()> { - let sk_path = self + /// Load both attestation and proposal keys for a specific validator index. + /// + /// Expects files named: + /// `validator_{idx}_attestation_sk.ssz` — attestation signing key + /// `validator_{idx}_proposal_sk.ssz` — block proposal signing key + pub fn load_keys(&mut self, validator_index: u64) -> Result<()> { + let attest_path = self .keys_dir - .join(format!("validator_{}_sk.ssz", validator_index)); + .join(format!("validator_{validator_index}_attestation_sk.ssz")); + let proposal_path = self + .keys_dir + .join(format!("validator_{validator_index}_proposal_sk.ssz")); // todo(security): this probably should be zeroized - let key_bytes = std::fs::read(&sk_path) - .context(format!("Failed to read secret key file: {sk_path:?}"))?; + let attest_bytes = std::fs::read(&attest_path) + .context(format!("Failed to read attestation key file: {attest_path:?}"))?; + let attest_key = SecretKey::try_from(attest_bytes.as_slice())?; - let key = SecretKey::try_from(key_bytes.as_slice())?; + let proposal_bytes = std::fs::read(&proposal_path) + .context(format!("Failed to read proposal key file: {proposal_path:?}"))?; + let proposal_key = SecretKey::try_from(proposal_bytes.as_slice())?; info!( validator = validator_index, - size = key_bytes.len(), - "Loaded secret key" + attest_size = attest_bytes.len(), + proposal_size = proposal_bytes.len(), + "Loaded attestation and proposal keys" ); - self.keys.insert(validator_index, key); + self.attestation_keys.insert(validator_index, attest_key); + self.proposal_keys.insert(validator_index, proposal_key); Ok(()) } - /// Sign a message with the validator's secret key - pub fn sign(&self, validator_index: u64, epoch: u32, message: H256) -> Result { + /// Load attestation and proposal keys from explicit file paths. + /// Used when annotated_validators.yaml provides the filenames directly. + pub fn load_keys_from_files( + &mut self, + validator_index: u64, + attest_path: &std::path::Path, + proposal_path: &std::path::Path, + ) -> Result<()> { + let attest_bytes = std::fs::read(attest_path) + .context(format!("Failed to read attestation key file: {attest_path:?}"))?; + let attest_key = SecretKey::try_from(attest_bytes.as_slice())?; + + let proposal_bytes = std::fs::read(proposal_path) + .context(format!("Failed to read proposal key file: {proposal_path:?}"))?; + let proposal_key = SecretKey::try_from(proposal_bytes.as_slice())?; + + info!( + validator = validator_index, + attest_size = attest_bytes.len(), + proposal_size = proposal_bytes.len(), + "Loaded attestation and proposal keys" + ); + + self.attestation_keys.insert(validator_index, attest_key); + self.proposal_keys.insert(validator_index, proposal_key); + Ok(()) + } + + /// Sign an attestation message with the validator's attestation secret key. + pub fn sign_attestation( + &self, + validator_index: u64, + epoch: u32, + message: H256, + ) -> Result { + let key = self + .attestation_keys + .get(&validator_index) + .ok_or_else(|| anyhow!("No attestation key loaded for validator {}", validator_index))?; + + key.sign(message, epoch) + } + + /// Sign a block with the validator's proposal secret key. + pub fn sign_proposal( + &self, + validator_index: u64, + epoch: u32, + message: H256, + ) -> Result { let key = self - .keys + .proposal_keys .get(&validator_index) - .ok_or_else(|| anyhow!("No key loaded for validator {}", validator_index))?; + .ok_or_else(|| anyhow!("No proposal key loaded for validator {}", validator_index))?; key.sign(message, epoch) } - /// Check if a key is loaded for a validator + /// Check if both attestation and proposal keys are loaded for a validator. pub fn has_key(&self, validator_index: u64) -> bool { - self.keys.contains_key(&validator_index) + self.attestation_keys.contains_key(&validator_index) + && self.proposal_keys.contains_key(&validator_index) } } diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 5bf61578..608d56e4 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -1,12 +1,13 @@ // Lean validator client with XMSS signing support -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::Path; +use serde::Deserialize; + use anyhow::{Context, Result, anyhow, bail}; use containers::{ - AggregatedSignatureProof, AggregationBits, Attestation, AttestationData, AttestationSignatures, - Block, BlockSignatures, BlockWithAttestation, SignedAggregatedAttestation, SignedAttestation, - SignedBlockWithAttestation, Slot, + AggregatedSignatureProof, AggregationBits, AttestationData, AttestationSignatures, Block, + BlockSignatures, SignedAggregatedAttestation, SignedAttestation, SignedBlock, Slot, }; use fork_choice::store::Store; use metrics::{METRICS, stop_and_discard, stop_and_record}; @@ -18,32 +19,73 @@ use try_from_iterator::TryFromIterator as _; pub mod keys; use keys::KeyManager; -use xmss::Signature; +use xmss::{PublicKey, Signature}; + +/// Entry in an annotated_validators.yaml file. +/// Each validator index has two entries: one for the attester key and one for the proposer key, +/// distinguished by the filename containing "attester" or "proposer". +/// Single-key format (e.g. validator_N_sk.ssz with neither keyword) is also accepted and +/// uses the same file for both attestation and proposal. +#[derive(Debug, Clone, Deserialize)] +pub struct AnnotatedValidatorEntry { + pub index: u64, + pub pubkey_hex: String, + pub privkey_file: String, +} + +pub type ValidatorRegistry = HashMap>; -pub type ValidatorRegistry = HashMap>; -// Node #[derive(Debug, Clone)] pub struct ValidatorConfig { pub node_id: String, pub validator_indices: Vec, + /// Maps validator index → (attestation_privkey_filename, proposal_privkey_filename). + /// Populated from annotated_validators.yaml; empty when constructed manually (tests). + pub key_files: HashMap, } impl ValidatorConfig { - // load validator index pub fn load_from_file(path: impl AsRef, node_id: &str) -> Result { let file = std::fs::File::open(path)?; let registry: ValidatorRegistry = serde_yaml::from_reader(file)?; - let indices = registry + let entries = registry .get(node_id) - .ok_or_else(|| anyhow!("Node `{node_id}` not found in validator registry"))? - .clone(); + .ok_or_else(|| anyhow!("Node `{node_id}` not found in validator registry"))?; + + // Group entries by validator index, mapping attester/proposer filenames by keyword. + let mut file_map: HashMap, Option)> = HashMap::new(); + for entry in entries { + let slot = file_map.entry(entry.index).or_default(); + if entry.privkey_file.contains("attester") { + slot.0 = Some(entry.privkey_file.clone()); + } else if entry.privkey_file.contains("proposer") { + slot.1 = Some(entry.privkey_file.clone()); + } else { + // Single-key format: same file for both keys. + slot.0 = Some(entry.privkey_file.clone()); + slot.1 = Some(entry.privkey_file.clone()); + } + } - info!(node_id = %node_id, indices = ?indices, "Validator config loaded..."); + let mut key_files: HashMap = HashMap::new(); + for (idx, (att, prop)) in file_map { + let att = + att.ok_or_else(|| anyhow!("No attester privkey_file for validator {idx}"))?; + let prop = + prop.ok_or_else(|| anyhow!("No proposer privkey_file for validator {idx}"))?; + key_files.insert(idx, (att, prop)); + } + + let mut validator_indices: Vec = key_files.keys().cloned().collect(); + validator_indices.sort_unstable(); + + info!(node_id = %node_id, indices = ?validator_indices, "Validator config loaded..."); Ok(ValidatorConfig { node_id: node_id.to_string(), - validator_indices: indices, + validator_indices, + key_files, }) } @@ -60,6 +102,51 @@ pub struct ValidatorService { is_aggregator: bool, } +/// Greedily extend `children` with proofs from `candidates` that maximally cover +/// validators not yet in `covered`. +/// +/// Mirrors the spec's `select_greedily`: +/// each iteration picks the candidate covering the most uncovered validators, +/// until no candidate adds new coverage. `covered` is updated in place so +/// callers can chain two passes (new payloads first, then known payloads). +fn extend_children_greedily<'a>( + candidates: &'a [AggregatedSignatureProof], + children: &mut Vec<&'a AggregatedSignatureProof>, + covered: &mut HashSet, +) { + // Track which candidates are still eligible (not yet selected). + let mut remaining: Vec<&'a AggregatedSignatureProof> = candidates.iter().collect(); + + loop { + // Find the candidate that covers the most uncovered validators. + let best = remaining + .iter() + .enumerate() + .map(|(pos, proof)| { + let new_cov = proof + .participants + .to_validator_indices() + .into_iter() + .filter(|vid| !covered.contains(vid)) + .count(); + (pos, new_cov) + }) + .max_by_key(|&(_, new_cov)| new_cov); + + let Some((pos, new_cov)) = best else { break }; + if new_cov == 0 { + // No remaining candidate adds coverage — done. + break; + } + + let proof = remaining.remove(pos); + for vid in proof.participants.to_validator_indices() { + covered.insert(vid); + } + children.push(proof); + } +} + impl ValidatorService { pub fn new(config: ValidatorConfig, num_validators: u64) -> Self { Self::new_with_aggregator(config, num_validators, false) @@ -106,11 +193,19 @@ impl ValidatorService { keys_dir: impl AsRef, is_aggregator: bool, ) -> Result> { - let mut key_manager = KeyManager::new(keys_dir)?; + let mut key_manager = KeyManager::new(&keys_dir)?; - // Load keys for all assigned validators + // Load keys for all assigned validators. + // When annotated_validators.yaml was used, key_files carries explicit filenames; + // otherwise fall back to the legacy convention-based paths. for &idx in &config.validator_indices { - key_manager.load_key(idx)?; + if let Some((attest_file, proposal_file)) = config.key_files.get(&idx) { + let attest_path = keys_dir.as_ref().join(attest_file); + let proposal_path = keys_dir.as_ref().join(proposal_file); + key_manager.load_keys_from_files(idx, &attest_path, &proposal_path)?; + } else { + key_manager.load_keys(idx)?; + } } info!( @@ -156,43 +251,60 @@ impl ValidatorService { self.is_aggregator && !self.config.validator_indices.is_empty() } - /// Perform aggregation duty if this node is an aggregator (devnet-3) - /// Collects signatures from gossip_signatures and creates aggregated attestations - /// Returns None if not an aggregator or no signatures to aggregate + /// Perform aggregation duty if this node is an aggregator. + /// + /// Implements the spec's three-phase `aggregate()` function: + /// 1. **Select** — greedily pick existing proofs (new payloads first, then known) + /// that maximise validator coverage (`select_greedily(new, known)`). + /// 2. **Fill** — collect raw gossip sigs for validators not covered by children. + /// 3. **Aggregate** — produce a single recursive XMSS proof. + /// + /// Returns `Some((attestations, consumed_data_roots))` where `consumed_data_roots` + /// is the set of data_roots whose gossip signatures were incorporated into a proof. + /// The caller must remove those keys from `store.gossip_signatures` to prevent + /// re-aggregation in future rounds (spec: "consumed gossip signatures are removed"). + /// + /// Returns `None` if this node has no aggregation duty or nothing to aggregate. pub fn maybe_aggregate( &self, store: &Store, slot: Slot, - ) -> Option> { + log_inv_rate: usize, + ) -> Option<(Vec, HashSet)> { if !self.is_aggregator_for_slot(slot) { return None; } - // Get the head state to access validator public keys + // Get the head state to access validator public keys. let head_state = store.states.get(&store.head)?; - // Group signatures by data_root - // SignatureKey contains (validator_id, data_root) - let mut groups: HashMap> = HashMap::new(); - + // Group fresh individual gossip signatures by data_root. + let mut gossip_groups: HashMap> = HashMap::new(); for (sig_key, signature) in &store.gossip_signatures { - groups + gossip_groups .entry(sig_key.data_root) .or_default() .push((sig_key.validator_id, signature.clone())); } - if groups.is_empty() { + // Spec: iterate over new.keys() | gossip_sigs.keys(). + // Known payloads alone cannot trigger aggregation — they only help extend coverage. + let mut all_data_roots: HashSet = gossip_groups.keys().copied().collect(); + for data_root in store.latest_new_aggregated_payloads.keys() { + all_data_roots.insert(*data_root); + } + + if all_data_roots.is_empty() { info!(slot = slot.0, "No signatures to aggregate"); return None; } - let mut aggregated_attestations = Vec::new(); + let mut aggregated_attestations: Vec = Vec::new(); + // data_roots whose raw gossip sigs were consumed into a proof; the caller will + // remove these from the store to match spec cleanup semantics. + let mut consumed_data_roots: HashSet = HashSet::new(); - for (data_root, validator_sigs) in groups { - // Look up attestation data by its hash (data_root) - // This ensures we get the exact attestation that was signed, - // matching ream's attestation_data_by_root_provider approach + for data_root in all_data_roots { let Some(attestation_data) = store.attestation_data_by_root.get(&data_root).cloned() else { warn!( @@ -202,81 +314,178 @@ impl ValidatorService { continue; }; - // Only aggregate attestations for the current slot + // Only aggregate attestations for the current slot. if attestation_data.slot != slot { continue; } - // Collect validator IDs, public keys, and signatures - // IMPORTANT: Must sort by validator_id to match ream/zeam behavior. - // The participants bitfield is iterated in ascending order during verification, - // so the proof must be created with public_keys/signatures in the same order. - let mut entries: Vec<(u64, Signature)> = validator_sigs + // ── Phase 1: Select ────────────────────────────────────────────────────── + // Two-pass greedy child selection matching spec `select_greedily(new, known)`. + // New payloads go first (uncommitted work); known payloads fill remaining gaps. + let mut children: Vec<&AggregatedSignatureProof> = Vec::new(); + let mut covered_by_children: HashSet = HashSet::new(); + + extend_children_greedily( + store + .latest_new_aggregated_payloads + .get(&data_root) + .map(|v| v.as_slice()) + .unwrap_or(&[]), + &mut children, + &mut covered_by_children, + ); + extend_children_greedily( + store + .latest_known_aggregated_payloads + .get(&data_root) + .map(|v| v.as_slice()) + .unwrap_or(&[]), + &mut children, + &mut covered_by_children, + ); + + // ── Phase 2: Fill ──────────────────────────────────────────────────────── + // Collect raw gossip sigs for validators not already covered by children, + // sorted ascending for deterministic bitfield construction. + let mut entries: Vec<(u64, Signature)> = gossip_groups + .get(&data_root) + .cloned() + .unwrap_or_default() .into_iter() - .filter(|(vid, _)| head_state.validators.get(*vid).is_ok()) + .filter(|(vid, _)| { + !covered_by_children.contains(vid) + && head_state.validators.get(*vid).is_ok() + }) .collect(); entries.sort_by_key(|(vid, _)| *vid); - let mut validator_ids = Vec::new(); - let mut public_keys = Vec::new(); - let mut signatures = Vec::new(); + let mut fresh_validator_ids: Vec = Vec::new(); + let mut fresh_public_keys: Vec = Vec::new(); + let mut fresh_signatures: Vec = Vec::new(); for (vid, sig) in entries { - // Get public key from state validators (already filtered above) let validator = head_state.validators.get(vid).unwrap(); - validator_ids.push(vid); - public_keys.push(validator.pubkey.clone()); - signatures.push(sig); + fresh_validator_ids.push(vid); + fresh_public_keys.push(validator.attestation_pubkey.clone()); + fresh_signatures.push(sig); } - if validator_ids.is_empty() { + if children.is_empty() && fresh_validator_ids.is_empty() { continue; } - // Create aggregation bits from validator IDs - let participants = AggregationBits::from_validator_indices(&validator_ids); + // ── Spec guard ─────────────────────────────────────────────────────────── + // A lone child proof with no fresh raw signatures is already a valid proof — + // skip re-aggregation. Matches spec rule: + // `if not raw_entries and len(child_proofs) < 2: continue` + if fresh_validator_ids.is_empty() && children.len() < 2 { + continue; + } - // Create the aggregated signature proof + // ── Phase 3: Aggregate ─────────────────────────────────────────────────── let timer = METRICS.get().map(|m| { m.lean_committee_signatures_aggregation_time_seconds .start_timer() }); - let proof = match AggregatedSignatureProof::aggregate( - participants, - public_keys, - signatures, - data_root, - attestation_data.slot.0 as u32, - ) { - Ok(p) => { - stop_and_record(timer); - p + + let proof = if children.is_empty() { + // No child proofs: simple fresh aggregation from raw sigs only. + let participants = AggregationBits::from_validator_indices(&fresh_validator_ids); + match AggregatedSignatureProof::aggregate( + participants, + fresh_public_keys, + fresh_signatures, + data_root, + attestation_data.slot.0 as u32, + log_inv_rate, + ) { + Ok(p) => { + stop_and_record(timer); + p + } + Err(e) => { + stop_and_discard(timer); + warn!(error = %e, "Failed to create aggregated signature proof"); + continue; + } } - Err(e) => { - stop_and_discard(timer); - warn!(error = %e, "Failed to create aggregated signature proof"); - continue; + } else { + // Have child proofs: recursive aggregation — children compress prior rounds, + // fresh raw sigs fill any remaining uncovered validators. + let child_pk_vecs: Vec> = children + .iter() + .map(|child| { + child + .participants + .to_validator_indices() + .into_iter() + .filter_map(|vid| { + head_state + .validators + .get(vid) + .ok() + .map(|v| v.attestation_pubkey.clone()) + }) + .collect() + }) + .collect(); + + let children_arg: Vec<(&[PublicKey], &AggregatedSignatureProof)> = child_pk_vecs + .iter() + .zip(children.iter()) + .map(|(pks, proof)| (pks.as_slice(), *proof)) + .collect(); + + let mut all_validator_ids: Vec = + covered_by_children.iter().copied().collect(); + all_validator_ids.extend_from_slice(&fresh_validator_ids); + all_validator_ids.sort(); + all_validator_ids.dedup(); + let all_participants = + AggregationBits::from_validator_indices(&all_validator_ids); + + match AggregatedSignatureProof::aggregate_with_children( + all_participants, + &children_arg, + fresh_public_keys, + fresh_signatures, + data_root, + attestation_data.slot.0 as u32, + log_inv_rate, + ) { + Ok(p) => { + stop_and_record(timer); + p + } + Err(e) => { + stop_and_discard(timer); + warn!(error = %e, "Failed to create recursive aggregated signature proof"); + continue; + } } }; info!( slot = slot.0, - validators = validator_ids.len(), + validators = fresh_validator_ids.len() + covered_by_children.len(), + children = children.len(), data_root = %format!("0x{:x}", data_root), "Created aggregated attestation" ); - // Create SignedAggregatedAttestation matching ream/zeam structure aggregated_attestations.push(SignedAggregatedAttestation { data: attestation_data, proof, }); + + // Mark as consumed so the caller can evict the raw sigs from the store. + consumed_data_roots.insert(data_root); } if aggregated_attestations.is_empty() { None } else { - Some(aggregated_attestations) + Some((aggregated_attestations, consumed_data_roots)) } } @@ -291,13 +500,7 @@ impl ValidatorService { block: Block, validator_index: u64, attestation_signatures: Vec, - proposer_attestation_data: AttestationData, - ) -> Result { - let proposer_attestation = Attestation { - validator_id: validator_index, - data: proposer_attestation_data, - }; - + ) -> Result { let Some(key_manager) = self.key_manager.as_ref() else { bail!("unable to sign block - keymanager not configured"); }; @@ -310,27 +513,22 @@ impl ValidatorService { }); key_manager - .sign( + .sign_proposal( validator_index, block.slot.0 as u32, - proposer_attestation.data.hash_tree_root(), + block.hash_tree_root(), ) .context("failed to sign block") .inspect_err(|_| stop_and_discard(sign_timer))? }; - let message = BlockWithAttestation { - block, - proposer_attestation, - }; - let signature = BlockSignatures { attestation_signatures: AttestationSignatures::try_from_iter(attestation_signatures) .context("invalid attestation signatures")?, proposer_signature, }; - Ok(SignedBlockWithAttestation { message, signature }) + Ok(SignedBlock { block, signature }) } /// Create and sign attestations for all validators given pre-fetched attestation data. @@ -367,7 +565,7 @@ impl ValidatorService { .start_timer() }); - match key_manager.sign(idx, epoch, message) { + match key_manager.sign_attestation(idx, epoch, message) { Ok(sig) => { METRICS.get().map(|metrics| { metrics.lean_pq_sig_attestation_signatures_total.inc(); diff --git a/lean_client/validator/tests/unit_tests.rs b/lean_client/validator/tests/unit_tests.rs index 2da822d0..9bad904c 100644 --- a/lean_client/validator/tests/unit_tests.rs +++ b/lean_client/validator/tests/unit_tests.rs @@ -7,6 +7,7 @@ fn test_proposer_selection() { let config = ValidatorConfig { node_id: "test_0".to_string(), validator_indices: vec![2], + key_files: Default::default(), }; let service = ValidatorService::new(config, 4); @@ -28,6 +29,7 @@ fn test_is_assigned() { let config = ValidatorConfig { node_id: "test_0".to_string(), validator_indices: vec![2, 5, 8], + key_files: Default::default(), }; assert!(config.is_assigned(2)); diff --git a/lean_client/xmss/Cargo.toml b/lean_client/xmss/Cargo.toml index 123365d2..a45f4cbf 100644 --- a/lean_client/xmss/Cargo.toml +++ b/lean_client/xmss/Cargo.toml @@ -11,8 +11,9 @@ derive_more = { workspace = true } eth_ssz = { workspace = true } ethereum-types = { workspace = true } hex = { workspace = true } -lean-multisig = { workspace = true } +rec_aggregation = { workspace = true } leansig = { workspace = true } +leansig_wrapper = { workspace = true } metrics = { workspace = true } rand = { workspace = true } ssz = { workspace = true } diff --git a/lean_client/xmss/src/aggregated_signature.rs b/lean_client/xmss/src/aggregated_signature.rs index 9fd08c1c..b83cadd1 100644 --- a/lean_client/xmss/src/aggregated_signature.rs +++ b/lean_client/xmss/src/aggregated_signature.rs @@ -3,15 +3,11 @@ use std::{str::FromStr, sync::Once}; use crate::{PublicKey, Signature}; use anyhow::{Context, Error, Result, anyhow, bail}; -use eth_ssz::{Decode, Encode}; use ethereum_types::H256; -use lean_multisig::{ - Devnet2XmssAggregateSignature, xmss_aggregate_signatures, xmss_aggregation_setup_prover, - xmss_aggregation_setup_verifier, xmss_verify_aggregated_signatures, -}; +use rec_aggregation::{AggregatedXMSS, init_aggregation_bytecode, xmss_aggregate, xmss_verify_aggregation}; use metrics::{METRICS, stop_and_discard}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; -use ssz::{ByteList, Ssz, SszRead}; +use ssz::{ByteList, ReadError, Size, SszHash, SszRead, SszSize, SszWrite, WriteError, U1}; use typenum::U1048576; /// Max size currently is 1MiB by spec. @@ -24,22 +20,44 @@ type AggregatedSignatureSizeLimit = U1048576; /// bit of nice encapsulation, so that xmss-related types don't leak /// abstraction into containers crate. /// -/// todo(xmss): deriving Ssz not particularly good there, as this won't validate -/// if it actually has valid proof structure, so `.as_lean()` method may panic. -#[derive(Debug, Clone, Ssz)] -#[ssz(transparent)] +/// SSZ traits are implemented manually (not derived) so that `SszRead` can validate +/// the inner bytes via `AggregatedXMSS::deserialize` at decode time, rejecting +/// malformed gossip messages at the boundary instead of panicking later in `as_lean`. +#[derive(Debug, Clone)] pub struct AggregatedSignature(ByteList); -fn setup_prover() { - static PROVER_SETUP: Once = Once::new(); +impl SszSize for AggregatedSignature { + const SIZE: Size = Size::Variable { minimum_size: 0 }; +} + +impl SszRead for AggregatedSignature { + fn from_ssz_unchecked(context: &C, bytes: &[u8]) -> Result { + let inner = + ByteList::::from_ssz_unchecked(context, bytes)?; + AggregatedXMSS::deserialize(inner.as_bytes()).ok_or(ReadError::Custom { + message: "invalid aggregated XMSS signature", + })?; + Ok(Self(inner)) + } +} - PROVER_SETUP.call_once(|| xmss_aggregation_setup_prover()); +impl SszWrite for AggregatedSignature { + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + self.0.write_variable(bytes) + } } -fn setup_verifier() { - static VERIFIER_SETUP: Once = Once::new(); +impl SszHash for AggregatedSignature { + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + self.0.hash_tree_root() + } +} - VERIFIER_SETUP.call_once(|| xmss_aggregation_setup_verifier()); +fn setup_aggregation() { + static SETUP: Once = Once::new(); + SETUP.call_once(init_aggregation_bytecode); } impl AggregatedSignature { @@ -47,9 +65,8 @@ impl AggregatedSignature { let bytes = ByteList::try_from(bytes.to_vec()) .context("signature too large - currently max 1MiB signatures allowed")?; - Devnet2XmssAggregateSignature::from_ssz_bytes(bytes.as_bytes()) - .map_err(|err| anyhow!("{err:?}")) - .context("invalid aggregated signature")?; + AggregatedXMSS::deserialize(bytes.as_bytes()) + .ok_or_else(|| anyhow!("invalid aggregated signature"))?; Ok(Self(bytes)) } @@ -58,9 +75,26 @@ impl AggregatedSignature { public_keys: impl IntoIterator, signatures: impl IntoIterator, message: H256, - epoch: u32, + slot: u32, + log_inv_rate: usize, + ) -> Result { + Self::aggregate_with_children(&[], public_keys, signatures, message, slot, log_inv_rate) + } + + /// Aggregate signatures with optional recursive child proofs. + /// + /// `children` is a list of `(public_keys_covered_by_child, child_proof)` pairs. + /// Each child proof was previously produced by aggregating the listed public keys, + /// allowing recursive / hierarchical proof compaction. + pub fn aggregate_with_children( + children: &[(&[PublicKey], &AggregatedSignature)], + public_keys: impl IntoIterator, + signatures: impl IntoIterator, + message: H256, + slot: u32, + log_inv_rate: usize, ) -> Result { - setup_prover(); + setup_aggregation(); let timer = METRICS.get().map(|metrics| { metrics @@ -68,14 +102,8 @@ impl AggregatedSignature { .start_timer() }); - let public_keys = public_keys - .into_iter() - .map(|k| k.as_lean()) - .collect::>(); - let signatures = signatures - .into_iter() - .map(|s| s.as_lean()) - .collect::>(); + let public_keys = public_keys.into_iter().collect::>(); + let signatures = signatures.into_iter().collect::>(); if public_keys.len() != signatures.len() { stop_and_discard(timer); @@ -86,26 +114,64 @@ impl AggregatedSignature { ); } - let aggregate = - xmss_aggregate_signatures(&public_keys, &signatures, message.as_fixed_bytes(), epoch) - .map_err(|err| anyhow!("{err:?}"))?; + if public_keys.is_empty() && children.is_empty() { + stop_and_discard(timer); + bail!("cannot aggregate: no raw signatures and no children"); + } + + let sig_count = public_keys.len(); + + let raw_xmss = public_keys + .into_iter() + .zip(signatures) + .map(|(pk, sig)| (pk.as_lean(), sig.as_lean())) + .collect::>(); + + // Convert children: store owned XmssPublicKey Vecs and owned AggregatedXMSS values, + // then build the reference slice that xmss_aggregate expects. + let lean_pks_vec: Vec> = children + .iter() + .map(|(pks, _)| pks.iter().map(|pk| pk.as_lean()).collect()) + .collect(); + let lean_agg_vec: Vec<_> = children + .iter() + .map(|(_, agg)| agg.as_lean()) + .collect::>>()?; + + let children_arg: Vec<(&[_], _)> = lean_pks_vec + .iter() + .zip(lean_agg_vec.into_iter()) + .map(|(pks, agg)| (pks.as_slice(), agg)) + .collect(); + + let (_pub_keys, agg) = xmss_aggregate( + &children_arg, + raw_xmss, + message.as_fixed_bytes(), + slot, + log_inv_rate, + ); METRICS.get().map(|metrics| { metrics .lean_pq_sig_aggregated_signatures_total - .inc_by(signatures.len() as u64) + .inc_by(sig_count as u64) }); - Ok(Self(aggregate.as_ssz_bytes().try_into()?)) + let bytes = agg.serialize(); + Ok(Self( + ByteList::try_from(bytes) + .context("aggregated proof too large - exceeds 1MiB limit")?, + )) } pub fn verify( &self, public_keys: impl IntoIterator, message: H256, - epoch: u32, + slot: u32, ) -> Result<()> { - setup_verifier(); + setup_aggregation(); let _timer = METRICS.get().map(|metrics| { metrics @@ -113,37 +179,33 @@ impl AggregatedSignature { .start_timer() }); - let public_keys = public_keys + let pub_keys = public_keys .into_iter() .map(|k| k.as_lean()) .collect::>(); - let aggregated_signature = self.as_lean(); + let agg = self.as_lean()?; - xmss_verify_aggregated_signatures( - &public_keys, - message.as_fixed_bytes(), - &aggregated_signature, - epoch, - ) - .map_err(|err| anyhow!("{err:?}")) - .inspect(|_| { - METRICS - .get() - .map(|metrics| metrics.lean_pq_sig_aggregated_signatures_valid_total.inc()); - }) - .inspect_err(|_| { - METRICS.get().map(|metrics| { - metrics - .lean_pq_sig_aggregated_signatures_invalid_total - .inc() - }); - }) + xmss_verify_aggregation(pub_keys, &agg, message.as_fixed_bytes(), slot) + .map(|_| ()) + .map_err(|err| anyhow!("{err:?}")) + .inspect(|_| { + METRICS + .get() + .map(|metrics| metrics.lean_pq_sig_aggregated_signatures_valid_total.inc()); + }) + .inspect_err(|_| { + METRICS.get().map(|metrics| { + metrics + .lean_pq_sig_aggregated_signatures_invalid_total + .inc() + }); + }) } - fn as_lean(&self) -> Devnet2XmssAggregateSignature { - Devnet2XmssAggregateSignature::from_ssz_bytes(self.0.as_bytes()) - .expect("AggregatedSignature was not constructed properly") + fn as_lean(&self) -> Result { + AggregatedXMSS::deserialize(self.0.as_bytes()) + .ok_or_else(|| anyhow!("invalid aggregated XMSS signature")) } // todo(xmss): this is a function used only for testing. ideally, it should not exist diff --git a/lean_client/xmss/src/public_key.rs b/lean_client/xmss/src/public_key.rs index d946e489..d0662f5d 100644 --- a/lean_client/xmss/src/public_key.rs +++ b/lean_client/xmss/src/public_key.rs @@ -5,15 +5,15 @@ use core::{ }; use anyhow::{Error, anyhow}; -use leansig::{serialization::Serializable, signature::SignatureScheme, signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8}; +use eth_ssz::DecodeError; +use leansig_wrapper::{XmssPublicKey, xmss_public_key_from_ssz, xmss_public_key_to_ssz}; use serde::{Deserialize, Serialize, de::{self, Visitor}}; use ssz::{BytesToDepth, MerkleTree, SszHash, SszRead, SszSize, SszWrite}; -use eth_ssz::DecodeError; use typenum::{U52, U1, Unsigned}; type PublicKeySize = U52; -type LeanSigPublicKey = ::PublicKey; +type LeanSigPublicKey = XmssPublicKey; #[derive(Clone, PartialEq, Eq)] pub struct PublicKey([u8; PublicKeySize::USIZE]); @@ -58,7 +58,8 @@ impl SszHash for PublicKey { impl PublicKey { pub fn new(bytes: &[u8]) -> Result { - LeanSigPublicKey::from_bytes(bytes)?; + xmss_public_key_from_ssz(bytes) + .map_err(|_| DecodeError::BytesInvalid("invalid xmss public key".to_string()))?; Ok(Self(bytes.try_into().expect( "slice of length != 52 shouldn't deserialize as valid leansig public key", @@ -66,8 +67,9 @@ impl PublicKey { } pub(crate) fn from_lean(key: LeanSigPublicKey) -> Self { + let bytes = xmss_public_key_to_ssz(&key); Self( - key.to_bytes() + bytes .as_slice() .try_into() .expect("slice of length != 52 shouldn't deserialize as valid leansig public key"), @@ -75,7 +77,7 @@ impl PublicKey { } pub(crate) fn as_lean(&self) -> LeanSigPublicKey { - LeanSigPublicKey::from_bytes(&self.0).expect("PublicKey was instantiated incorrectly") + xmss_public_key_from_ssz(&self.0).expect("PublicKey was instantiated incorrectly") } } diff --git a/lean_client/xmss/src/secret_key.rs b/lean_client/xmss/src/secret_key.rs index f8b2b8e3..16b08ec4 100644 --- a/lean_client/xmss/src/secret_key.rs +++ b/lean_client/xmss/src/secret_key.rs @@ -1,56 +1,38 @@ -use anyhow::{Context, Error, Result, anyhow}; +use anyhow::{Error, Result, anyhow}; use derive_more::Debug; -use leansig::{serialization::Serializable, signature::{ - SignatureScheme, generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8 -}}; -use rand::Rng; +use leansig::serialization::Serializable; +use leansig::signature::SignatureScheme; +use leansig::signature::generalized_xmss::instantiations_aborting::lifetime_2_to_the_32::{ + SchemeAbortingTargetSumLifetime32Dim46Base8 as XmssScheme, + SecretKeyAbortingTargetSumLifetime32Dim46Base8 as XmssSecretKey, +}; +use rand::CryptoRng; use ssz::H256; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{PublicKey, Signature}; -type LeanSigSecretKey = ::SecretKey; - #[derive(Clone, Zeroize, ZeroizeOnDrop, Debug)] #[debug("[REDACTED]")] pub struct SecretKey(Vec); impl SecretKey { pub fn sign(&self, message: H256, epoch: u32) -> Result { - let signature = ::sign( - &self.as_lean(), - epoch, - message.as_fixed_bytes(), - ) - .context("failed to sign message")?; - - Ok(Signature::from_lean(signature)) + let sk = XmssSecretKey::from_bytes(&self.0) + .map_err(|_| anyhow!("failed to deserialize secret key"))?; + let sig = XmssScheme::sign(&sk, epoch, message.as_fixed_bytes()) + .map_err(|_| anyhow!("failed to sign message"))?; + Ok(Signature::from_lean(sig)) } - pub fn generate_key_pair( + pub fn generate_key_pair( rng: &mut R, activation_epoch: u32, num_active_epochs: u32, ) -> (PublicKey, SecretKey) { - let (public_key, secret_key) = - ::key_gen::( - rng, - activation_epoch as usize, - num_active_epochs as usize, - ); - - ( - PublicKey::from_lean(public_key), - SecretKey::from_lean(secret_key), - ) - } - - fn from_lean(key: LeanSigSecretKey) -> Self { - Self(key.to_bytes()) - } - - fn as_lean(&self) -> LeanSigSecretKey { - LeanSigSecretKey::from_bytes(&self.0).expect("SecretKey was instantiated incorrectly") + let (pk, sk) = + XmssScheme::key_gen(rng, activation_epoch as usize, num_active_epochs as usize); + (PublicKey::from_lean(pk), SecretKey(sk.to_bytes())) } } @@ -58,9 +40,8 @@ impl TryFrom<&[u8]> for SecretKey { type Error = Error; fn try_from(value: &[u8]) -> Result { - LeanSigSecretKey::from_bytes(value) + XmssSecretKey::from_bytes(value) .map_err(|_| anyhow!("value is not valid secret key"))?; - Ok(Self(value.to_vec())) } } diff --git a/lean_client/xmss/src/signature.rs b/lean_client/xmss/src/signature.rs index 19b842e5..13009643 100644 --- a/lean_client/xmss/src/signature.rs +++ b/lean_client/xmss/src/signature.rs @@ -6,20 +6,18 @@ use core::{ use anyhow::{Error, anyhow, Result}; use eth_ssz::DecodeError; -use leansig::{serialization::Serializable, signature::SignatureScheme}; -use leansig::signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8; +use leansig_wrapper::{XmssSignature, xmss_signature_from_ssz, xmss_signature_to_ssz, xmss_verify}; use metrics::METRICS; use serde::de; use serde::{Deserialize, Serialize}; use ssz::{ByteVector, H256, Ssz}; use crate::public_key::PublicKey; -use typenum::{Diff, U984, U4096}; +use typenum::{Sum, U2048, U256, U128, U64, U32, U8}; -type U3112 = Diff; +// 2536 = 2048 + 256 + 128 + 64 + 32 + 8 +type SignatureSize = Sum, U128>, U64>, U32>, U8>; -type SignatureSize = U3112; - -type LeanSigSignature = ::Signature; +type LeanSigSignature = XmssSignature; // todo(xmss): default implementation doesn't make sense here, and is needed only for tests #[derive(Clone, Default, Ssz)] @@ -28,49 +26,46 @@ pub struct Signature(ByteVector); impl Signature { pub fn new(inner: &[u8]) -> Result { - LeanSigSignature::from_bytes(inner)?; + xmss_signature_from_ssz(inner) + .map_err(|_| DecodeError::BytesInvalid("invalid xmss signature".to_string()))?; Ok(Self(inner.try_into().expect( - "slice of length != 3112 shouldn't deserialize as valid leansig signature", + "slice of length != 2536 shouldn't deserialize as valid leansig signature", ))) } pub fn verify(&self, public_key: &PublicKey, epoch: u32, message: H256) -> Result<()> { - let is_valid = ::verify( - &public_key.as_lean(), - epoch, - message.as_fixed_bytes(), - &self.as_lean(), - ); - - if is_valid { - METRICS.get().map(|metrics| { - metrics.lean_pq_sig_attestation_signatures_valid_total.inc(); - }); - Ok(()) - } else { - METRICS.get().map(|metrics| { - metrics - .lean_pq_sig_attestation_signatures_invalid_total - .inc(); - }); - Err(anyhow!("invalid signature")) + match xmss_verify(&public_key.as_lean(), epoch, message.as_fixed_bytes(), &self.as_lean()) { + Ok(()) => { + METRICS.get().map(|metrics| { + metrics.lean_pq_sig_attestation_signatures_valid_total.inc(); + }); + Ok(()) + } + Err(()) => { + METRICS.get().map(|metrics| { + metrics + .lean_pq_sig_attestation_signatures_invalid_total + .inc(); + }); + Err(anyhow!("invalid signature")) + } } } pub(crate) fn from_lean(signature: LeanSigSignature) -> Self { - let bytes = signature.to_bytes(); + let bytes = xmss_signature_to_ssz(&signature); Self( bytes .as_slice() .try_into() - .expect("slice of length != 3112 shouldn't deserialize as valid leansig signature"), + .expect("slice of length != 2536 shouldn't deserialize as valid leansig signature"), ) } pub(crate) fn as_lean(&self) -> LeanSigSignature { - LeanSigSignature::from_bytes(self.0.as_bytes()) + xmss_signature_from_ssz(self.0.as_bytes()) .expect("signature internal representation must be valid leansig signature") } } @@ -127,7 +122,7 @@ impl<'de> Deserialize<'de> for Signature { } #[derive(Deserialize)] - struct XmssSignature { + struct XmssSignatureJson { path: XmssPath, rho: DataWrapper>, hashes: DataWrapper>>>, @@ -138,7 +133,7 @@ impl<'de> Deserialize<'de> for Signature { siblings: DataWrapper>>>, } - let xmss_sig = XmssSignature::deserialize(deserializer)?; + let xmss_sig = XmssSignatureJson::deserialize(deserializer)?; let mut rho_bytes = Vec::new(); for val in &xmss_sig.rho.data { rho_bytes.extend_from_slice(&val.to_le_bytes()); @@ -197,8 +192,6 @@ impl<'de> Deserialize<'de> for Signature { // 5. Write Hashes Data (Variable) ssz_bytes.extend_from_slice(&hashes_bytes); - println!("Total SSZ Bytes Length: {}", ssz_bytes.len()); - Signature::try_from(ssz_bytes.as_slice()) .map_err(|err| de::Error::custom(format!("invalid signature: {err:?}"))) } @@ -211,6 +204,6 @@ mod test { #[test] fn valid_signature_size() { - assert_eq!(SignatureSize::U64, 3112); + assert_eq!(SignatureSize::U64, 2536); } } diff --git a/lean_client/xmss/tests/aggregate.rs b/lean_client/xmss/tests/aggregate.rs index 457cef6c..ce380f5d 100644 --- a/lean_client/xmss/tests/aggregate.rs +++ b/lean_client/xmss/tests/aggregate.rs @@ -19,6 +19,7 @@ fn aggregate_two() { vec![sig1, sig2], message.clone(), 0, + 1, ) .unwrap();