From 9f67e2f41b8f08a8f5c0b53bae3f870bd70bc3c5 Mon Sep 17 00:00:00 2001 From: bomanaps Date: Thu, 11 Jun 2026 04:42:58 +0100 Subject: [PATCH 1/7] devnet 5 changes plus debugging --- lean_client/Cargo.lock | 633 +++++------------- lean_client/Cargo.toml | 18 +- lean_client/Makefile | 3 +- lean_client/containers/src/attestation.rs | 2 - lean_client/containers/src/block.rs | 160 ++--- lean_client/containers/src/lib.rs | 7 +- lean_client/containers/src/state.rs | 50 +- .../containers/tests/unit_tests/common.rs | 37 +- .../tests/unit_tests/state_process.rs | 6 +- .../tests/unit_tests/state_transition.rs | 100 +-- lean_client/fork_choice/Cargo.toml | 1 + lean_client/fork_choice/src/handlers.rs | 156 ++++- lean_client/fork_choice/src/lib.rs | 1 + lean_client/fork_choice/src/reaggregate.rs | 262 ++++++++ lean_client/fork_choice/src/store.rs | 21 +- .../tests/fork_choice_test_vectors.rs | 11 +- .../fork_choice/tests/unit_tests/common.rs | 4 +- .../fork_choice/tests/unit_tests/validator.rs | 18 +- lean_client/http_api/src/test_driver.rs | 12 +- lean_client/metrics/src/metrics.rs | 56 ++ lean_client/networking/Cargo.toml | 1 - .../networking/src/network/behaviour.rs | 1 + lean_client/networking/src/network/service.rs | 264 +++++++- lean_client/networking/src/req_resp.rs | 253 +++++-- lean_client/networking/src/types.rs | 20 +- lean_client/spec_test_fixtures/src/common.rs | 53 +- lean_client/spec_test_fixtures/src/lib.rs | 6 +- .../src/verify_signatures.rs | 2 + lean_client/src/aggregation.rs | 103 ++- lean_client/src/main.rs | 548 ++++++++++----- lean_client/validator/src/lib.rs | 544 ++++++++------- lean_client/validator/tests/unit_tests.rs | 3 +- lean_client/xmss/src/aggregated_signature.rs | 99 +-- lean_client/xmss/src/lib.rs | 2 + lean_client/xmss/src/multi_message.rs | 171 +++++ lean_client/xmss/src/secret_key.rs | 20 +- 36 files changed, 2276 insertions(+), 1372 deletions(-) create mode 100644 lean_client/fork_choice/src/reaggregate.rs create mode 100644 lean_client/xmss/src/multi_message.rs diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index 4e1bdad2..aa5cab34 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -37,18 +37,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -404,7 +392,7 @@ dependencies = [ "proc-macro2 1.0.106", "quote 1.0.45", "syn 2.0.117", - "synstructure 0.13.2", + "synstructure", ] [[package]] @@ -465,19 +453,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "asynchronous-codec" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" -dependencies = [ - "bytes", - "futures-sink", - "futures-util", - "memchr", - "pin-project-lite", -] - [[package]] name = "asynchronous-codec" version = "0.7.0" @@ -602,7 +577,7 @@ dependencies = [ [[package]] name = "backend" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "mt-air", "mt-fiat-shamir", @@ -613,8 +588,9 @@ dependencies = [ "mt-symetric", "mt-utils", "mt-whir", - "rayon", + "parallel", "tracing", + "zk-alloc", ] [[package]] @@ -786,12 +762,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bs58" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" - [[package]] name = "bs58" version = "0.5.1" @@ -1474,7 +1444,7 @@ dependencies = [ "enr", "fnv", "futures", - "hashlink 0.11.0", + "hashlink", "hex", "hkdf", "lazy_static", @@ -1725,7 +1695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1908,6 +1878,7 @@ dependencies = [ "env-config", "indexmap 2.14.0", "metrics", + "parking_lot", "rand 0.10.1", "rand_chacha 0.10.0", "serde", @@ -1951,12 +1922,12 @@ dependencies = [ [[package]] name = "futures-bounded" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e" +checksum = "b604752cefc5aa3ab98992a107a8bd99465d2825c1584e0b60cb6957b21e19d7" dependencies = [ - "futures-timer", "futures-util", + "tokio", ] [[package]] @@ -2198,9 +2169,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -2239,24 +2207,6 @@ dependencies = [ "sha2 0.10.9 (git+https://github.com/grandinetech/universal-precompiles.git?tag=sha2-v0.10.9-up.2)", ] -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "hashlink" version = "0.11.0" @@ -2847,15 +2797,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "ipconfig" version = "0.3.4" @@ -3001,11 +2942,12 @@ dependencies = [ "git-version", "hex", "http_api", - "libp2p-identity 0.2.13", + "libp2p-identity", "metrics", "networking", "num_cpus", "parking_lot", + "rayon", "reqwest", "ssz", "tikv-jemallocator", @@ -3019,7 +2961,7 @@ dependencies = [ [[package]] name = "lean_compiler" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "backend", "include_dir", @@ -3035,7 +2977,7 @@ dependencies = [ [[package]] name = "lean_prover" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "backend", "itertools 0.14.0", @@ -3053,7 +2995,7 @@ dependencies = [ [[package]] name = "lean_vm" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "backend", "itertools 0.14.0", @@ -3069,7 +3011,7 @@ dependencies = [ [[package]] name = "leansig" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanSig#c08a3bae74b0d85379cab72dcbefa4091546ecbb" +source = "git+https://github.com/leanEthereum/leanSig?branch=devnet4#15cbdd43ec8525aa43fea2f42cafc5ed366084ae" dependencies = [ "dashmap", "ethereum_ssz", @@ -3089,7 +3031,7 @@ dependencies = [ [[package]] name = "leansig_fast_keygen" version = "0.1.0" -source = "git+https://github.com/TomWambsgans/leanSig?branch=devnet4-fast-keygen#5b86867a4d3c1d4a8add840f70fa047ea1506188" +source = "git+https://github.com/TomWambsgans/leanSig?branch=devnet4-fast-keygen#0fa9e19b8946ef50a34f3d50d82918b98bcfa4a5" dependencies = [ "dashmap", "ethereum_ssz", @@ -3109,7 +3051,7 @@ dependencies = [ [[package]] name = "leansig_wrapper" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "backend", "ethereum_ssz", @@ -3144,9 +3086,8 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libp2p" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce71348bf5838e46449ae240631117b487073d5f347c06d434caddcb91dceb5a" +version = "0.57.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "bytes", "either", @@ -3155,11 +3096,11 @@ dependencies = [ "getrandom 0.2.17", "libp2p-allow-block-list", "libp2p-connection-limits", - "libp2p-core 0.43.2", + "libp2p-core", "libp2p-dns", "libp2p-gossipsub", "libp2p-identify", - "libp2p-identity 0.2.13", + "libp2p-identity", "libp2p-mdns", "libp2p-metrics", "libp2p-noise", @@ -3169,98 +3110,65 @@ dependencies = [ "libp2p-tcp", "libp2p-upnp", "libp2p-yamux", - "multiaddr 0.18.2", + "multiaddr", "pin-project", - "rw-stream-sink 0.4.0", + "rw-stream-sink", "thiserror 2.0.18", ] [[package]] name = "libp2p-allow-block-list" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" +version = "0.7.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", ] [[package]] name = "libp2p-connection-limits" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" +version = "0.7.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", ] [[package]] name = "libp2p-core" -version = "0.39.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c1df63c0b582aa434fb09b2d86897fa2b419ffeccf934b36f87fcedc8e835c2" -dependencies = [ - "either", - "fnv", - "futures", - "futures-timer", - "instant", - "libp2p-identity 0.1.3", - "log", - "multiaddr 0.17.1", - "multihash 0.17.0", - "multistream-select 0.12.1", - "once_cell", - "parking_lot", - "pin-project", - "quick-protobuf", - "rand 0.8.5", - "rw-stream-sink 0.3.0", - "smallvec", - "thiserror 1.0.69", - "unsigned-varint 0.7.2", - "void", -] - -[[package]] -name = "libp2p-core" -version = "0.43.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "249128cd37a2199aff30a7675dffa51caf073b51aa612d2f544b19932b9aebca" +version = "0.44.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "either", "fnv", "futures", "futures-timer", - "libp2p-identity 0.2.13", - "multiaddr 0.18.2", - "multihash 0.19.3", - "multistream-select 0.13.0", + "libp2p-identity", + "multiaddr", + "multihash", + "multistream-select", "parking_lot", "pin-project", "quick-protobuf", "rand 0.8.5", - "rw-stream-sink 0.4.0", + "rw-stream-sink", "thiserror 2.0.18", "tracing", - "unsigned-varint 0.8.0", + "unsigned-varint", "web-time", ] [[package]] name = "libp2p-dns" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" +version = "0.45.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ - "async-trait", "futures", "hickory-resolver", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "parking_lot", "smallvec", "tracing", @@ -3268,12 +3176,11 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" -version = "0.49.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a538e571cd38f504f761c61b8f79127489ea7a7d6f05c41ca15d31ffb5726326" +version = "0.50.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "async-channel", - "asynchronous-codec 0.7.0", + "asynchronous-codec", "base64", "byteorder", "bytes", @@ -3282,10 +3189,10 @@ dependencies = [ "futures", "futures-timer", "getrandom 0.2.17", - "hashlink 0.9.1", + "hashlink", "hex_fmt", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", @@ -3298,17 +3205,16 @@ dependencies = [ [[package]] name = "libp2p-identify" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" +version = "0.48.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ - "asynchronous-codec 0.7.0", + "asynchronous-codec", "either", "futures", "futures-bounded", "futures-timer", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", @@ -3317,24 +3223,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "libp2p-identity" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276bb57e7af15d8f100d3c11cbdd32c6752b7eef4ba7a18ecf464972c07abcce" -dependencies = [ - "bs58 0.4.0", - "ed25519-dalek", - "log", - "multiaddr 0.17.1", - "multihash 0.17.0", - "quick-protobuf", - "rand 0.8.5", - "sha2 0.10.9 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.69", - "zeroize", -] - [[package]] name = "libp2p-identity" version = "0.2.13" @@ -3342,11 +3230,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" dependencies = [ "asn1_der", - "bs58 0.5.1", + "bs58", "ed25519-dalek", "hkdf", "k256", - "multihash 0.19.3", + "multihash", "quick-protobuf", "rand 0.8.5", "sha2 0.10.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3357,71 +3245,50 @@ dependencies = [ [[package]] name = "libp2p-mdns" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" +version = "0.49.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "futures", "hickory-proto", "if-watch", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", "rand 0.8.5", "smallvec", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tracing", ] [[package]] name = "libp2p-metrics" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" +version = "0.18.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "futures", - "libp2p-core 0.43.2", + "libp2p-core", "libp2p-gossipsub", "libp2p-identify", - "libp2p-identity 0.2.13", + "libp2p-identity", "libp2p-swarm", "pin-project", "prometheus-client", "web-time", ] -[[package]] -name = "libp2p-mplex" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d34780b514b159e6f3fd70ba3e72664ec89da28dca2d1e7856ee55e2c7031ba" -dependencies = [ - "asynchronous-codec 0.6.2", - "bytes", - "futures", - "libp2p-core 0.39.2", - "log", - "nohash-hasher", - "parking_lot", - "rand 0.8.5", - "smallvec", - "unsigned-varint 0.7.2", -] - [[package]] name = "libp2p-noise" -version = "0.46.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" +version = "0.47.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ - "asynchronous-codec 0.7.0", + "asynchronous-codec", "bytes", "futures", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", - "multiaddr 0.18.2", - "multihash 0.19.3", + "libp2p-core", + "libp2p-identity", + "multiaddr", + "multihash", "quick-protobuf", "rand 0.8.5", "snow", @@ -3434,21 +3301,20 @@ dependencies = [ [[package]] name = "libp2p-quic" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" +version = "0.14.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "futures", "futures-timer", "if-watch", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "libp2p-tls", "quinn", "rand 0.8.5", "ring", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -3456,15 +3322,13 @@ dependencies = [ [[package]] name = "libp2p-request-response" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f1cca83488b90102abac7b67d5c36fc65bc02ed47620228af7ed002e6a1478" +version = "0.30.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ - "async-trait", "futures", "futures-bounded", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", "rand 0.8.5", "smallvec", @@ -3473,19 +3337,18 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce88c6c4bf746c8482480345ea3edfd08301f49e026889d1cbccfa1808a9ed9e" +version = "0.48.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "either", "fnv", "futures", "futures-timer", - "hashlink 0.10.0", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "hashlink", + "libp2p-core", + "libp2p-identity", "libp2p-swarm-derive", - "multistream-select 0.13.0", + "multistream-select", "rand 0.8.5", "smallvec", "tokio", @@ -3495,9 +3358,8 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" -version = "0.35.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" +version = "0.36.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "heck", "quote 1.0.45", @@ -3506,15 +3368,14 @@ dependencies = [ [[package]] name = "libp2p-tcp" -version = "0.44.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6585b9309699f58704ec9ab0bb102eca7a3777170fa91a8678d73ca9cafa93" +version = "0.45.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "futures", "futures-timer", "if-watch", "libc", - "libp2p-core 0.43.2", + "libp2p-core", "socket2 0.6.3", "tokio", "tracing", @@ -3522,14 +3383,13 @@ dependencies = [ [[package]] name = "libp2p-tls" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" +version = "0.7.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "futures", "futures-rustls", - "libp2p-core 0.43.2", - "libp2p-identity 0.2.13", + "libp2p-core", + "libp2p-identity", "rcgen", "ring", "rustls", @@ -3541,14 +3401,13 @@ dependencies = [ [[package]] name = "libp2p-upnp" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" +version = "0.7.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "futures", "futures-timer", "igd-next", - "libp2p-core 0.43.2", + "libp2p-core", "libp2p-swarm", "tokio", "tracing", @@ -3556,13 +3415,12 @@ dependencies = [ [[package]] name = "libp2p-yamux" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" +version = "0.48.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "either", "futures", - "libp2p-core 0.43.2", + "libp2p-core", "thiserror 2.0.18", "tracing", "yamux 0.12.1", @@ -3725,7 +3583,7 @@ checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" [[package]] name = "mt-air" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "mt-field", "mt-poly", @@ -3734,13 +3592,13 @@ dependencies = [ [[package]] name = "mt-fiat-shamir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "mt-field", "mt-koala-bear", "mt-symetric", "mt-utils", - "rayon", + "parallel", "serde", "tracing", ] @@ -3748,14 +3606,14 @@ dependencies = [ [[package]] name = "mt-field" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "itertools 0.14.0", "mt-utils", "num-bigint", + "parallel", "paste", "rand 0.10.1", - "rayon", "serde", "tracing", ] @@ -3763,7 +3621,7 @@ dependencies = [ [[package]] name = "mt-koala-bear" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "itertools 0.14.0", "mt-field", @@ -3771,7 +3629,6 @@ dependencies = [ "num-bigint", "paste", "rand 0.10.1", - "rayon", "serde", "tracing", ] @@ -3779,44 +3636,47 @@ dependencies = [ [[package]] name = "mt-poly" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "itertools 0.14.0", "mt-field", "mt-utils", + "parallel", "rand 0.10.1", - "rayon", "serde", "system-info", + "zk-alloc", ] [[package]] name = "mt-sumcheck" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "mt-air", "mt-fiat-shamir", "mt-field", "mt-poly", - "rayon", + "parallel", "tracing", + "zk-alloc", ] [[package]] name = "mt-symetric" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "mt-field", "mt-koala-bear", - "rayon", + "parallel", + "zk-alloc", ] [[package]] name = "mt-utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "serde", ] @@ -3824,7 +3684,7 @@ dependencies = [ [[package]] name = "mt-whir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "itertools 0.14.0", "mt-fiat-shamir", @@ -3834,29 +3694,11 @@ dependencies = [ "mt-sumcheck", "mt-symetric", "mt-utils", + "parallel", "rand 0.10.1", - "rayon", "system-info", "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", + "zk-alloc", ] [[package]] @@ -3868,13 +3710,13 @@ dependencies = [ "arrayref", "byteorder", "data-encoding", - "libp2p-identity 0.2.13", + "libp2p-identity", "multibase", - "multihash 0.19.3", + "multihash", "percent-encoding", "serde", "static_assertions", - "unsigned-varint 0.8.0", + "unsigned-varint", "url", ] @@ -3890,17 +3732,6 @@ dependencies = [ "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", -] - [[package]] name = "multihash" version = "0.19.3" @@ -3908,49 +3739,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" dependencies = [ "core2", - "unsigned-varint 0.8.0", -] - -[[package]] -name = "multihash-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6d4752e6230d8ef7adf7bd5d8c4b1f6561c1014c5ba9a37445ccefe18aa1db" -dependencies = [ - "proc-macro-crate 1.1.3", - "proc-macro-error", - "proc-macro2 1.0.106", - "quote 1.0.45", - "syn 1.0.109", - "synstructure 0.12.6", -] - -[[package]] -name = "multistream-select" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8552ab875c1313b97b8d20cb857b9fd63e2d1d6a0a1b53ce9821e575405f27a" -dependencies = [ - "bytes", - "futures", - "log", - "pin-project", - "smallvec", - "unsigned-varint 0.7.2", + "unsigned-varint", ] [[package]] name = "multistream-select" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" +version = "0.14.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "bytes", "futures", - "log", "pin-project", "smallvec", - "unsigned-varint 0.7.2", + "tracing", + "unsigned-varint", ] [[package]] @@ -4017,8 +3819,7 @@ dependencies = [ "hex", "k256", "libp2p", - "libp2p-identity 0.2.13", - "libp2p-mplex", + "libp2p-identity", "metrics", "num-bigint", "num-traits", @@ -4184,7 +3985,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p3-baby-bear" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "p3-challenger", "p3-field", @@ -4199,7 +4000,7 @@ dependencies = [ [[package]] name = "p3-challenger" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "p3-field", "p3-maybe-rayon", @@ -4212,21 +4013,21 @@ dependencies = [ [[package]] name = "p3-dft" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "itertools 0.14.0", "p3-field", "p3-matrix", "p3-maybe-rayon", "p3-util", - "spin 0.11.0", + "spin 0.12.0", "tracing", ] [[package]] name = "p3-field" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "itertools 0.14.0", "num-bigint", @@ -4241,7 +4042,7 @@ dependencies = [ [[package]] name = "p3-koala-bear" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "p3-challenger", "p3-field", @@ -4256,7 +4057,7 @@ dependencies = [ [[package]] name = "p3-matrix" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "itertools 0.14.0", "p3-field", @@ -4270,12 +4071,12 @@ dependencies = [ [[package]] name = "p3-maybe-rayon" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" [[package]] name = "p3-mds" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "p3-dft", "p3-field", @@ -4287,7 +4088,7 @@ dependencies = [ [[package]] name = "p3-monty-31" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "itertools 0.14.0", "num-bigint", @@ -4303,14 +4104,14 @@ dependencies = [ "paste", "rand 0.10.1", "serde", - "spin 0.11.0", + "spin 0.12.0", "tracing", ] [[package]] name = "p3-poseidon1" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "p3-field", "p3-mds", @@ -4321,7 +4122,7 @@ dependencies = [ [[package]] name = "p3-poseidon2" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "p3-field", "p3-mds", @@ -4333,7 +4134,7 @@ dependencies = [ [[package]] name = "p3-symmetric" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "itertools 0.14.0", "p3-field", @@ -4344,12 +4145,20 @@ dependencies = [ [[package]] name = "p3-util" version = "0.5.1" -source = "git+https://github.com/Plonky3/Plonky3.git#e0bc50c6f29bca1be2eee92d215e1eb4431c6d50" +source = "git+https://github.com/Plonky3/Plonky3.git#4e27646d9c7f650d7f2657f01f5e634463b09105" dependencies = [ "serde", "transpose", ] +[[package]] +name = "parallel" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" +dependencies = [ + "system-info", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -4372,7 +4181,7 @@ version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate", "proc-macro2 1.0.106", "quote 1.0.45", "syn 2.0.117", @@ -4646,16 +4455,6 @@ dependencies = [ "uint 0.9.5", ] -[[package]] -name = "proc-macro-crate" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" -dependencies = [ - "thiserror 1.0.69", - "toml", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -4665,30 +4464,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2 1.0.106", - "quote 1.0.45", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2 1.0.106", - "quote 1.0.45", - "version_check", -] - [[package]] name = "proc-macro2" version = "0.4.30" @@ -4760,9 +4535,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +checksum = "cca3d75b4566b9a29fe1ed623587fb058e826eb329a0be4b7c4da1ebb2d7a6ca" dependencies = [ "dtoa", "itoa", @@ -4772,9 +4547,9 @@ dependencies = [ [[package]] name = "prometheus-client-derive-encode" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" dependencies = [ "proc-macro2 1.0.106", "quote 1.0.45", @@ -4854,15 +4629,14 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +version = "0.4.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ - "asynchronous-codec 0.7.0", + "asynchronous-codec", "bytes", "quick-protobuf", - "thiserror 1.0.69", - "unsigned-varint 0.8.0", + "thiserror 2.0.18", + "unsigned-varint", ] [[package]] @@ -5108,7 +4882,7 @@ dependencies = [ [[package]] name = "rec_aggregation" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "backend", "include_dir", @@ -5420,7 +5194,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5478,20 +5252,8 @@ dependencies = [ [[package]] name = "rw-stream-sink" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26338f5e09bb721b85b135ea05af7767c90b52f6de4f087d4f4a3a9d64e7dc04" -dependencies = [ - "futures", - "pin-project", - "static_assertions", -] - -[[package]] -name = "rw-stream-sink" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +version = "0.5.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" dependencies = [ "futures", "pin-project", @@ -5877,9 +5639,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783f3f6f6b01e295a669edfc402133a5f2553d1f0e81284b3ba4594e80bdd4a2" +checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865" dependencies = [ "lock_api", ] @@ -5934,7 +5696,7 @@ dependencies = [ "darling 0.21.3", "easy-ext", "itertools 0.14.0", - "proc-macro-crate 3.5.0", + "proc-macro-crate", "proc-macro2 1.0.106", "quote 1.0.45", "syn 2.0.117", @@ -6020,7 +5782,7 @@ dependencies = [ [[package]] name = "sub_protocols" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "backend", "lean_vm", @@ -6076,18 +5838,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2 1.0.106", - "quote 1.0.45", - "syn 1.0.109", - "unicode-xid 0.2.6", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -6123,10 +5873,9 @@ dependencies = [ [[package]] name = "system-info" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "libc", - "rayon", ] [[package]] @@ -6151,7 +5900,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6373,15 +6122,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -6690,16 +6430,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "unsigned-varint" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" -dependencies = [ - "asynchronous-codec 0.6.2", - "bytes", -] - [[package]] name = "unsigned-varint" version = "0.8.0" @@ -6739,7 +6469,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "backend", "tracing", @@ -6804,12 +6534,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "wait-timeout" version = "0.2.1" @@ -7445,7 +7169,7 @@ dependencies = [ "proc-macro2 1.0.106", "quote 1.0.45", "syn 2.0.117", - "synstructure 0.13.2", + "synstructure", ] [[package]] @@ -7486,7 +7210,7 @@ dependencies = [ "proc-macro2 1.0.106", "quote 1.0.45", "syn 2.0.117", - "synstructure 0.13.2", + "synstructure", ] [[package]] @@ -7562,9 +7286,10 @@ dependencies = [ [[package]] name = "zk-alloc" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=f66d4a974eced803574eb0ea43d812e523c8d7ad#f66d4a974eced803574eb0ea43d812e523c8d7ad" +source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df4e30fdddbbf8ae26a333116c68cec7026#e2592df4e30fdddbbf8ae26a333116c68cec7026" dependencies = [ "libc", + "parallel", "system-info", ] diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index b88e745d..85d2c5bd 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -251,10 +251,10 @@ indexmap = "2" http-body-util = "0.1" http_api_utils = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" } k256 = "0.13" -rec_aggregation = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "f66d4a974eced803574eb0ea43d812e523c8d7ad" } -leansig = { git = "https://github.com/leanEthereum/leanSig" } -leansig_wrapper = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "f66d4a974eced803574eb0ea43d812e523c8d7ad" } -libp2p = { version = "0.56.0", default-features = false, features = [ +rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df4e30fdddbbf8ae26a333116c68cec7026" } +leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" } +leansig_wrapper = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df4e30fdddbbf8ae26a333116c68cec7026" } +libp2p = { git = "https://github.com/lambdaclass/rust-libp2p.git", rev = "2f14d0ec9665a01cfb6a02326c90628c4bba521c", default-features = false, features = [ 'dns', 'gossipsub', 'identify', @@ -266,8 +266,7 @@ libp2p = { version = "0.56.0", default-features = false, features = [ 'tokio', 'yamux' ] } -libp2p-identity = { version = "0.2", features = ["secp256k1"] } -libp2p-mplex = "0.39" +libp2p-identity = { version = "0.2.13", features = ["secp256k1"] } dedicated_executor = { path = "dedicated_executor" } num-bigint = "0.4" num-traits = "0.2" @@ -329,6 +328,7 @@ metrics = { workspace = true } networking = { workspace = true } num_cpus = { workspace = true } parking_lot = { workspace = true } +rayon = { workspace = true } ssz = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } @@ -337,3 +337,9 @@ validator = { workspace = true } xmss = { workspace = true } reqwest = { workspace = true } tikv-jemallocator = { workspace = true } + +[profile.release] +debug = true +split-debuginfo = "packed" +lto = "thin" +codegen-units = 1 diff --git a/lean_client/Makefile b/lean_client/Makefile index 32ee6856..e1dcb34f 100644 --- a/lean_client/Makefile +++ b/lean_client/Makefile @@ -16,7 +16,7 @@ GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) # Build date. Used for docker image metadata. BUILD_DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') # Currently supported devnet. Used to publish devnet tag, along with `latest`. -CURRENT_DEVNET := devnet-4 +CURRENT_DEVNET := devnet-5 # Repo root that cross bind-mounts into its build container (see # `lean_client/Cross.toml`). Auto-derived locally; CI overrides via env. LEAN_REPO_ROOT ?= $(shell git rev-parse --show-toplevel) @@ -86,6 +86,7 @@ clean: x86_64-unknown-linux-gnu: ./target/x86_64-unknown-linux-gnu/release/lean_client ./target/x86_64-unknown-linux-gnu/release/lean_client: + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C target-cpu=x86-64-v3" \ LEAN_REPO_ROOT=$(LEAN_REPO_ROOT) cross build --bin lean_client --target x86_64-unknown-linux-gnu --profile release .PHONY: aarch64-unknown-linux-gnu diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 930496ba..cadeced6 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -12,8 +12,6 @@ pub type Attestations = PersistentList; pub type AggregatedAttestations = PersistentList; -pub type AttestationSignatures = PersistentList; - /// Aggregated signature proof with participant tracking. /// /// Combines the participant bitfield with the proof bytes. diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 1342f4c0..f8a6b63c 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -1,16 +1,14 @@ +use std::collections::HashSet; + use crate::{Slot, State}; use anyhow::{Context, Result, ensure}; use metrics::METRICS; -use rayon::prelude::*; use ssz::{H256, Ssz, SszHash}; -use xmss::Signature; +use xmss::MultiMessageAggregate; -use crate::attestation::{AggregatedAttestations, AttestationSignatures}; +use crate::attestation::AggregatedAttestations; +use crate::state::MAX_ATTESTATIONS_DATA; -/// The body of a block, containing payload data. -/// -/// Attestations are stored WITHOUT signatures. Signatures are aggregated -/// separately in BlockSignatures to match the spec architecture. // todo(containers): default implementation doesn't make sense here. #[derive(Clone, Debug, Ssz, Default)] pub struct BlockBody { @@ -35,121 +33,93 @@ pub struct Block { pub body: BlockBody, } -// todo(containers): default implementation doesn't make sense here -#[derive(Debug, Clone, Ssz, Default)] -pub struct BlockSignatures { - pub attestation_signatures: AttestationSignatures, - pub proposer_signature: Signature, -} - -/// Signed block for devnet4: block body + aggregated signatures. -/// Proposer attestation is no longer embedded in the block message. #[derive(Clone, Debug, Ssz)] pub struct SignedBlock { - /// The proposed block. pub block: Block, - /// Aggregated signature payload (attestation proofs + proposer signature). - pub signature: BlockSignatures, + pub proof: MultiMessageAggregate, } impl SignedBlock { - /// Verify all XMSS signatures in this signed block. - /// - /// 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<()> { let block = &self.block; - let signature = &self.signature; let aggregated_attestations = &block.body.attestations; - let attestation_signatures = &signature.attestation_signatures; - // Verify signature count matches aggregated attestation count ensure!( - aggregated_attestations.len_u64() == attestation_signatures.len_u64(), - "attestation signature count mismatch: {} attestations vs {} signatures", + (aggregated_attestations.len_u64() as usize) <= MAX_ATTESTATIONS_DATA, + "block has {} attestations; max is {MAX_ATTESTATIONS_DATA}", aggregated_attestations.len_u64(), - attestation_signatures.len_u64() ); let validators = &parent_state.validators; let num_validators = validators.len_u64(); - // Phase 1: collect all verification inputs (serial - reads from State) - let verification_tasks = aggregated_attestations - .into_iter() - .zip(attestation_signatures.into_iter()) - .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 _timer = METRICS.get().map(|metrics| { + metrics + .lean_pq_sig_attestation_verification_time_seconds + .start_timer() + }); + + let attestations_len = aggregated_attestations.len_u64() as usize; + let mut pubkeys_owned: Vec> = Vec::with_capacity(attestations_len + 1); + let mut messages: Vec<(H256, u32)> = Vec::with_capacity(attestations_len + 1); + let mut seen_data_roots: HashSet = HashSet::with_capacity(attestations_len); + + for aggregated_attestation in aggregated_attestations { + let validator_ids = aggregated_attestation + .aggregation_bits + .to_validator_indices(); + ensure!( + !validator_ids.is_empty(), + "attestation has empty aggregation_bits" + ); + for validator_id in &validator_ids { + ensure!( + *validator_id < num_validators, + "validator index {validator_id} out of range (max {num_validators})" + ); + } + let pubkeys = validator_ids + .into_iter() + .map(|id| { + validators + .get(id) + .map(|validator| validator.attestation_pubkey.clone()) + .map_err(Into::into) + }) + .collect::>>()?; + pubkeys_owned.push(pubkeys); + + let data_root = aggregated_attestation.data.hash_tree_root(); + ensure!( + seen_data_roots.insert(data_root), + "duplicate AttestationData in block body" + ); + messages.push((data_root, aggregated_attestation.data.slot.0 as u32)); + } + let proposer_index = block.proposer_index; ensure!( proposer_index < num_validators, "proposer index {proposer_index} out of range (max {num_validators})" ); - let proposer = validators .get(proposer_index) .context(format!("proposer {proposer_index} not found in state"))?; + pubkeys_owned.push(vec![proposer.proposal_pubkey.clone()]); - let _timer = METRICS.get().map(|metrics| { - metrics - .lean_pq_sig_attestation_verification_time_seconds - .start_timer() - }); + let block_root = block.hash_tree_root(); + ensure!( + !seen_data_roots.contains(&block_root), + "block root collides with attestation data root" + ); + messages.push((block_root, block.slot.0 as u32)); + + let pubkeys_per_message: Vec<&[xmss::PublicKey]> = + pubkeys_owned.iter().map(|v| v.as_slice()).collect(); - signature - .proposer_signature - .verify( - &proposer.proposal_pubkey, - block.slot.0 as u32, - block.hash_tree_root(), - ) - .context("proposer signature verification failed")?; + self.proof + .verify(&pubkeys_per_message, &messages) + .context("block proof verification failed")?; Ok(()) } diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index 4ed68c45..38a2fa6f 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -9,10 +9,10 @@ mod validator; pub use attestation::{ AggregatedAttestation, AggregatedSignatureProof, AggregatedSignatures, AggregationBits, - Attestation, AttestationData, AttestationSignatures, Attestations, SignatureKey, - SignedAggregatedAttestation, SignedAttestation, + Attestation, AttestationData, Attestations, SignatureKey, SignedAggregatedAttestation, + SignedAttestation, }; -pub use block::{Block, BlockBody, BlockHeader, BlockSignatures, SignedBlock}; +pub use block::{Block, BlockBody, BlockHeader, SignedBlock}; pub use checkpoint::Checkpoint; pub use config::{Config, GenesisConfig, GenesisValidatorEntry}; pub use slot::Slot; @@ -21,3 +21,4 @@ pub use state::{ }; pub use status::Status; pub use validator::{Validator, Validators}; +pub use xmss::MultiMessageAggregate; diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 0f491a58..609f14e8 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -13,12 +13,19 @@ use crate::{ attestation::{ AggregatedAttestation, AggregatedAttestations, AggregationBits, AttestationData, }, - block::{Block, BlockBody, BlockHeader, SignedBlock}, + block::{Block, BlockBody, BlockHeader}, validator::{Validator, ValidatorRegistryLimit, Validators}, }; /// Maximum number of distinct AttestationData entries per block (spec: chain/config.py:36). -const MAX_ATTESTATIONS_DATA: usize = 16; +/// Used by the import-side validation so we accept any spec-valid block from other clients. +pub(crate) const MAX_ATTESTATIONS_DATA: usize = 8; + +/// Producer-side cap on distinct AttestationData entries when *we* build a block. +/// Lower than the spec ceiling so the per-block Type-2 MultiMessageAggregate prove +/// fits inside the slot. Spec-permissible because MAX_ATTESTATIONS_DATA is a ceiling, +/// not a floor (confirmed on 2026-06-10 spec call). +pub(crate) const PRODUCER_MAX_ATTESTATIONS_DATA: usize = 4; type HistoricalRootsLimit = U262144; // 2^18 @@ -295,17 +302,10 @@ impl State { self } - pub fn state_transition( - &self, - signed_block: SignedBlock, - valid_signatures: bool, - ) -> Result { - ensure!(valid_signatures, "invalid block signatures"); - + pub fn state_transition(&self, block: &Block) -> Result { let _timer = METRICS .get() .map(|metrics| metrics.lean_state_transition_time_seconds.start_timer()); - let block = &signed_block.block; let mut state = self.process_slots(block.slot)?; state = state.process_block(block)?; @@ -500,6 +500,7 @@ impl State { let source = attestation.data.source.clone(); let target = attestation.data.target.clone(); + let head = attestation.data.head.clone(); if !justified_slots.is_slot_justified(finalized_slot, source.slot)? { info!("skipping attestation, source slot is not justified"); @@ -511,17 +512,16 @@ impl State { continue; } - if source.root.is_zero() || target.root.is_zero() { - info!("skipping attestation, source or target slots are zero"); + if source.root.is_zero() || target.root.is_zero() || head.root.is_zero() { + info!("skipping attestation, source/target/head root is zero"); continue; } if &source.root != self.historical_block_hashes.get(source.slot.0)? || &target.root != self.historical_block_hashes.get(target.slot.0)? + || &head.root != self.historical_block_hashes.get(head.slot.0)? { - info!( - "skipping attestation, source or target roots not found in historical block hashes" - ); + info!("skipping attestation, source/target/head roots not on canonical chain"); continue; } @@ -698,7 +698,12 @@ impl State { &H256, &(AttestationData, Vec), )> = aggregated_payloads.iter().collect(); - sorted_entries.sort_by_key(|(_, (data, _))| data.target.slot); + sorted_entries.sort_by_key(|(_, (data, _))| { + ( + std::cmp::Reverse(data.target.slot), + std::cmp::Reverse(data.slot), + ) + }); let mut processed_data_roots: HashSet = HashSet::new(); @@ -714,9 +719,12 @@ impl State { if processed_data_roots.contains(data_root) { continue; } - if processed_data_roots.len() >= MAX_ATTESTATIONS_DATA { + if processed_data_roots.len() >= PRODUCER_MAX_ATTESTATIONS_DATA { break; } + if att_data.target.slot <= att_data.source.slot { + continue; + } if !known_block_roots.contains(&att_data.head.root) { continue; } @@ -732,12 +740,8 @@ impl State { continue; } - let is_genesis_self_vote = - att_data.source.slot == Slot(0) && att_data.target.slot == Slot(0); - - if !is_genesis_self_vote - && current_justified_slots - .is_slot_justified(current_finalized_slot, att_data.target.slot)? + if current_justified_slots + .is_slot_justified(current_finalized_slot, att_data.target.slot)? { continue; } diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 828db39d..fd153af8 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -4,15 +4,14 @@ //! using the devnet2 data structures. use containers::{ - AggregatedAttestation, Attestation, Attestations, Block, BlockBody, BlockHeader, - BlockSignatures, Checkpoint, Config, SignedBlock, Slot, State, Validators, + AggregatedAttestation, Attestation, Attestations, Block, BlockBody, BlockHeader, Checkpoint, + Config, Slot, State, Validators, }; use containers::{ HistoricalBlockHashes, JustificationRoots, JustificationValidators, JustifiedSlots, Validator, }; use ssz::{H256, PersistentList, SszHash}; use typenum::U4096; -use xmss::Signature; pub const DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const TEST_VALIDATOR_COUNT: usize = 4; // Actual validator count used in tests @@ -25,50 +24,36 @@ pub fn create_block( slot: u64, parent_header: &mut BlockHeader, attestations: Option, -) -> SignedBlock { +) -> Block { let body = BlockBody { attestations: { - let attestations_vec = attestations.unwrap_or_default(); - - // Convert PersistentList into a Vec - let attestations_vec: Vec = - attestations_vec.into_iter().cloned().collect(); + let attestations_vec: Vec = attestations + .unwrap_or_default() + .into_iter() + .cloned() + .collect(); let aggregated: Vec = AggregatedAttestation::aggregate_by_data(&attestations_vec); - // Create a new empty PersistentList let mut persistent_list: PersistentList = PersistentList::default(); - - // Push each aggregated attestation for agg in aggregated { persistent_list .push(agg) .expect("PersistentList capacity exceeded"); } - persistent_list }, }; - let block_message = Block { + Block { slot: Slot(slot), proposer_index: slot % 10, parent_root: parent_header.hash_tree_root(), state_root: H256::zero(), - body: body, - }; - - let return_value = SignedBlock { - block: block_message, - signature: BlockSignatures { - attestation_signatures: PersistentList::default(), - proposer_signature: Signature::default(), - }, - }; - - return_value + body, + } } pub fn create_attestations(indices: &[usize]) -> Vec { diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index 32562451..43baada8 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -43,10 +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 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(); + let block = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let new_state = state_at_slot_1.process_block_header(&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 3b1d9595..622d620c 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -3,16 +3,14 @@ //! Tests for full state transitions including signature validation //! and state root verification. -// tests/state_transition.rs -use containers::{Block, BlockSignatures, SignedBlock, Slot, State}; +use containers::{Block, Slot, State}; use pretty_assertions::assert_eq; use rstest::fixture; -use ssz::{H256, PersistentList, SszHash}; +use ssz::{H256, SszHash}; #[path = "common.rs"] mod common; use common::{create_block, sample_config}; -use xmss::Signature; #[fixture] fn genesis_state() -> State { @@ -23,14 +21,11 @@ fn genesis_state() -> State { #[test] fn test_state_transition_full() { let state = genesis_state(); - let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); + let state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - let signed_block = create_block(1, &mut state_at_slot_1.latest_block_header, None); - let block = signed_block.block.clone(); + let block = create_block(1, &mut state_at_slot_1.latest_block_header.clone(), None); - // 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(); - let expected_state = state_after_header .process_attestations(&block.body.attestations) .unwrap(); @@ -40,12 +35,7 @@ fn test_state_transition_full() { ..block }; - let final_signed_block = SignedBlock { - block: block_with_correct_root, - signature: signed_block.signature, - }; - - let final_state = state.state_transition(final_signed_block, true).unwrap(); + let final_state = state.state_transition(&block_with_correct_root).unwrap(); assert_eq!( final_state.hash_tree_root(), @@ -53,90 +43,14 @@ fn test_state_transition_full() { ); } -#[test] -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 = 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(); - - let expected_state = state_after_header - .process_attestations(&block.body.attestations) - .unwrap(); - - let block_with_correct_root = Block { - state_root: expected_state.hash_tree_root(), - ..block - }; - - let final_signed_block = SignedBlock { - block: block_with_correct_root, - signature: signed_block.signature, - }; - - let result = state.state_transition(final_signed_block, false); - assert!(result.is_err()); -} - -// Test with bad state root using devnet2 BlockSignatures structure #[test] 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 = create_block(1, &mut state_at_slot_1.latest_block_header, None); - let mut block = signed_block.block.clone(); - + let mut block = create_block(1, &mut state_at_slot_1.latest_block_header, None); block.state_root = H256::zero(); - let final_signed_block = SignedBlock { - block, - signature: BlockSignatures { - attestation_signatures: PersistentList::default(), - proposer_signature: Signature::default(), - }, - }; - - let result = state.state_transition(final_signed_block, true); + let result = state.state_transition(&block); assert!(result.is_err()); } - -#[test] -fn test_state_transition_devnet2() { - let state = genesis_state(); - let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - - // Create a block with attestations for devnet2 - 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(); - - let expected_state = state_after_header - .process_attestations(&block.body.attestations) - .unwrap(); - - // Ensure the state root matches the expected state - let block_with_correct_root = Block { - state_root: expected_state.hash_tree_root(), - ..block - }; - - 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, true).unwrap(); - - assert_eq!( - final_state.hash_tree_root(), - expected_state.hash_tree_root() - ); -} diff --git a/lean_client/fork_choice/Cargo.toml b/lean_client/fork_choice/Cargo.toml index d5589772..e80e2f4c 100644 --- a/lean_client/fork_choice/Cargo.toml +++ b/lean_client/fork_choice/Cargo.toml @@ -9,6 +9,7 @@ containers = { workspace = true } env-config = { workspace = true } indexmap = { workspace = true } metrics = { workspace = true } +parking_lot = { workspace = true } ssz = { workspace = true } tracing = { workspace = true } xmss = { workspace = true } diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 579c18ee..ebfd17b7 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::sync::Arc; use anyhow::{Context, Result, anyhow, bail, ensure}; use containers::{ @@ -6,8 +7,10 @@ use containers::{ State, }; use metrics::METRICS; +use parking_lot::RwLock; use ssz::{H256, SszHash}; use tracing::warn; +use xmss::PublicKey; use crate::block_cache::BlockCache; use crate::store::{ @@ -407,6 +410,19 @@ pub fn on_aggregated_attestation( .verify(public_keys, data_root, attestation_data.slot.0 as u32) .context("aggregated attestation proof verification failed")?; + let attestation_slot = attestation_data.slot; + for vid in &validator_ids { + if store + .latest_new_attestations + .get(vid) + .map_or(true, |existing| existing.slot < attestation_slot) + { + store + .latest_new_attestations + .insert(*vid, attestation_data.clone()); + } + } + // Store the verified proof in latest_new_aggregated_payloads, keyed by data_root store .latest_new_aggregated_payloads @@ -428,6 +444,113 @@ pub fn on_aggregated_attestation( Ok(()) } +/// Three-phase variant of `on_aggregated_attestation` that releases the store +/// lock across the XMSS verify SNARK. Phase 1 takes a brief read borrow to +/// validate metadata and copy participant pubkeys; Phase 2 runs `proof.verify` +/// with no lock held; Phase 3 takes a brief write borrow to insert results. +pub fn on_aggregated_attestation_async( + store: Arc>, + signed_aggregated_attestation: SignedAggregatedAttestation, +) -> Result<()> { + enum Phase1 { + Pending(H256), + Verify(Vec), + } + + let attestation_data = signed_aggregated_attestation.data.clone(); + let proof = signed_aggregated_attestation.proof.clone(); + let data_root = attestation_data.hash_tree_root(); + + let phase1 = { + let s = store.read(); + + if let Some(missing_root) = find_unknown_attestation_block(&s, &attestation_data) { + Phase1::Pending(missing_root) + } else { + validate_attestation_data(&s, &attestation_data)?; + + let key_state = s.states.get(&attestation_data.target.root).ok_or_else(|| { + anyhow!("no state for target block {}", attestation_data.target.root) + })?; + + ensure!( + proof.participants.0.iter().any(|b| *b), + "aggregated attestation has empty participants bitfield" + ); + + let validator_ids = proof.participants.to_validator_indices(); + let public_keys = validator_ids + .iter() + .map(|&id| { + key_state + .validators + .get(id) + .map(|v| v.attestation_pubkey.clone()) + .map_err(Into::into) + }) + .collect::>>()?; + + Phase1::Verify(public_keys) + } + }; + + match phase1 { + Phase1::Pending(missing_root) => { + let mut s = store.write(); + s.pending_aggregated_attestations + .entry(missing_root) + .or_default() + .push(signed_aggregated_attestation); + s.pending_fetch_roots.insert(missing_root); + METRICS.get().map(|m| { + m.grandine_pending_fetch_roots + .set(s.pending_fetch_roots.len() as i64) + }); + } + Phase1::Verify(public_keys) => { + proof + .verify(public_keys, data_root, attestation_data.slot.0 as u32) + .context("aggregated attestation proof verification failed")?; + + let mut s = store.write(); + s.attestation_data_by_root + .insert(data_root, attestation_data.clone()); + METRICS.get().map(|m| { + m.grandine_attestation_data_by_root + .set(s.attestation_data_by_root.len() as i64) + }); + + let validator_ids = proof.participants.to_validator_indices(); + let attestation_slot = attestation_data.slot; + for vid in &validator_ids { + if s.latest_new_attestations + .get(vid) + .map_or(true, |existing| existing.slot < attestation_slot) + { + s.latest_new_attestations + .insert(*vid, attestation_data.clone()); + } + } + + s.latest_new_aggregated_payloads + .entry(data_root) + .or_default() + .push(proof); + METRICS.get().map(|metrics| { + metrics + .lean_attestations_valid_total + .with_label_values(&["aggregation"]) + .inc(); + metrics + .lean_latest_new_aggregated_payloads + .set(s.latest_new_aggregated_payloads.len() as i64); + }); + } + } + + Ok(()) +} + /// Internal attestation processing - stores AttestationData fn on_attestation_internal( store: &mut Store, @@ -538,7 +661,7 @@ pub fn verify_and_transition( if verify_signatures { signed_block.verify_signatures(parent_state.clone())?; } - parent_state.state_transition(signed_block, true) + parent_state.state_transition(&signed_block.block) } /// Store-mutating portion of block processing: must run on the chain task. Inserts the @@ -680,28 +803,17 @@ pub fn apply_verified_block( ); } - // Process block body attestations as on-chain (is_from_block=true) - let signatures = &signed_block.signature; let aggregated_attestations = &block.body.attestations; - // Store aggregated proofs indexed by (validator_id, data_root) for future block building - for (att_idx, aggregated_attestation) in aggregated_attestations.into_iter().enumerate() { + for aggregated_attestation in aggregated_attestations.into_iter() { let data_root = aggregated_attestation.data.hash_tree_root(); - - // Store attestation data for safe target extraction - // This is critical: without this, block attestations are invisible to update_safe_target() store .attestation_data_by_root .insert(data_root, aggregated_attestation.data.clone()); - - // 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 - .latest_known_aggregated_payloads - .entry(data_root) - .or_default() - .push(proof_data.clone()); - } + store + .latest_known_aggregated_payloads + .entry(data_root) + .or_default(); } // Update gauge for known aggregated payloads count @@ -824,6 +936,16 @@ fn prune_with_retention_bounds(store: &mut Store) { .set(store.latest_new_aggregated_payloads.len() as i64); m.grandine_attestation_data_by_root .set(store.attestation_data_by_root.len() as i64); + let pending_atts: usize = store.pending_attestations.values().map(|v| v.len()).sum(); + let pending_agg_atts: usize = store + .pending_aggregated_attestations + .values() + .map(|v| v.len()) + .sum(); + m.grandine_pending_attestations_size + .set(pending_atts as i64); + m.grandine_pending_aggregated_attestations_size + .set(pending_agg_atts as i64); }); } diff --git a/lean_client/fork_choice/src/lib.rs b/lean_client/fork_choice/src/lib.rs index 0def96c0..7432afe9 100644 --- a/lean_client/fork_choice/src/lib.rs +++ b/lean_client/fork_choice/src/lib.rs @@ -1,5 +1,6 @@ pub mod block_cache; pub mod handlers; +pub mod reaggregate; pub mod store; pub mod sync_state; diff --git a/lean_client/fork_choice/src/reaggregate.rs b/lean_client/fork_choice/src/reaggregate.rs new file mode 100644 index 00000000..571ce1a9 --- /dev/null +++ b/lean_client/fork_choice/src/reaggregate.rs @@ -0,0 +1,262 @@ +use std::collections::HashSet; +use std::sync::Arc; + +use containers::{AggregatedSignatureProof, AggregationBits, SignedBlock, Validators}; +use metrics::METRICS; +use parking_lot::RwLock; +use ssz::{H256, SszHash}; +use tracing::{debug, info, warn}; +use xmss::PublicKey; + +use crate::store::Store; + +pub const MAX_REAGGREGATIONS_PER_BLOCK: usize = 4; + +pub struct ReaggregateContext { + pubkeys_per_component: Vec>, + candidates: Vec<(H256, AggregationBits)>, +} + +pub fn select_candidates( + store: &Store, + signed_block: &SignedBlock, + validators: &Validators, +) -> Option { + let block = &signed_block.block; + let attestations = &block.body.attestations; + if attestations.len_u64() == 0 { + return None; + } + + let num_validators = validators.len_u64(); + + let proposer_index = block.proposer_index; + if proposer_index >= num_validators { + warn!( + proposer_index, + "reaggregate skipped: proposer index out of range" + ); + return None; + } + + let mut pubkeys_per_component: Vec> = + Vec::with_capacity(attestations.len_u64() as usize + 1); + + for att in attestations.into_iter() { + let validator_ids = att.aggregation_bits.to_validator_indices(); + for vid in &validator_ids { + if *vid >= num_validators { + warn!(vid, "reaggregate skipped: participant out of range"); + return None; + } + } + let mut pks = Vec::with_capacity(validator_ids.len()); + for vid in validator_ids { + let v = match validators.get(vid) { + Ok(v) => v, + Err(err) => { + warn!(vid, ?err, "reaggregate skipped: validator lookup failed"); + return None; + } + }; + pks.push(v.attestation_pubkey.clone()); + } + pubkeys_per_component.push(pks); + } + + let proposer = match validators.get(proposer_index) { + Ok(v) => v, + Err(err) => { + warn!( + proposer_index, + ?err, + "reaggregate skipped: proposer lookup failed" + ); + return None; + } + }; + pubkeys_per_component.push(vec![proposer.proposal_pubkey.clone()]); + + let latest_justified_slot = store.latest_justified.slot; + let mut candidates: Vec<(H256, AggregationBits)> = Vec::new(); + + for att in attestations.into_iter() { + if candidates.len() >= MAX_REAGGREGATIONS_PER_BLOCK { + break; + } + + if att.data.target.slot <= latest_justified_slot { + continue; + } + + let data_root = att.data.hash_tree_root(); + + let local_union: HashSet = store + .latest_new_aggregated_payloads + .get(&data_root) + .map(|proofs| { + proofs + .iter() + .flat_map(|p| p.participants.to_validator_indices()) + .collect() + }) + .unwrap_or_default(); + + let block_participants: HashSet = att + .aggregation_bits + .to_validator_indices() + .into_iter() + .collect(); + + if block_participants.is_subset(&local_union) { + continue; + } + + candidates.push((data_root, att.aggregation_bits.clone())); + } + + let body_attestations = attestations.len_u64() as usize; + let selected = candidates.len(); + let skipped_target = body_attestations.saturating_sub(selected); + METRICS.get().map(|m| { + m.grandine_reaggregate_candidates_selected_total + .with_label_values(&["selected"]) + .inc_by(selected as u64); + if skipped_target > 0 { + m.grandine_reaggregate_candidates_selected_total + .with_label_values(&["skipped"]) + .inc_by(skipped_target as u64); + } + }); + + if candidates.is_empty() { + info!( + body_attestations, + justified_slot = latest_justified_slot.0, + "reaggregate: no candidates after filter" + ); + return None; + } + + info!( + body_attestations, + selected, + justified_slot = latest_justified_slot.0, + "reaggregate: candidates ready for split" + ); + + Some(ReaggregateContext { + pubkeys_per_component, + candidates, + }) +} + +pub fn compute_recoveries( + signed_block: &SignedBlock, + context: ReaggregateContext, + log_inv_rate: usize, +) -> Vec<(H256, AggregatedSignatureProof)> { + let pubkeys_per_component_view: Vec<&[PublicKey]> = context + .pubkeys_per_component + .iter() + .map(|v| v.as_slice()) + .collect(); + + let mut recoveries = Vec::with_capacity(context.candidates.len()); + for (data_root, participants) in context.candidates { + match signed_block.proof.split_by_message( + &pubkeys_per_component_view, + data_root, + log_inv_rate, + ) { + Ok(recovered) => { + METRICS.get().map(|m| { + m.grandine_reaggregate_split_outcomes_total + .with_label_values(&["success"]) + .inc() + }); + recoveries.push(( + data_root, + AggregatedSignatureProof { + participants, + proof_data: recovered, + }, + )); + } + Err(err) => { + METRICS.get().map(|m| { + m.grandine_reaggregate_split_outcomes_total + .with_label_values(&["failure"]) + .inc() + }); + warn!(?err, %data_root, "reaggregate split failed for attestation"); + } + } + } + recoveries +} + +pub fn apply_recoveries(store: &mut Store, recoveries: Vec<(H256, AggregatedSignatureProof)>) { + for (data_root, proof) in recoveries { + store + .latest_new_aggregated_payloads + .entry(data_root) + .or_default() + .push(proof); + } +} + +pub fn run_sync(store: &mut Store, signed_block: &SignedBlock, log_inv_rate: usize) { + let parent_validators = match store.states.get(&signed_block.block.parent_root) { + Some(state) => state.validators.clone(), + None => { + debug!( + parent_root = %signed_block.block.parent_root, + "reaggregate skipped: parent state missing" + ); + return; + } + }; + let context = select_candidates(store, signed_block, &parent_validators); + + let context = match context { + Some(c) => c, + None => return, + }; + + let recoveries = compute_recoveries(signed_block, context, log_inv_rate); + if recoveries.is_empty() { + return; + } + + apply_recoveries(store, recoveries); +} + +pub fn run_in_executor(store: Arc>, signed_block: SignedBlock, log_inv_rate: usize) { + let context = { + let s = store.read(); + let parent_validators = match s.states.get(&signed_block.block.parent_root) { + Some(state) => state.validators.clone(), + None => { + debug!( + parent_root = %signed_block.block.parent_root, + "reaggregate skipped: parent state missing" + ); + return; + } + }; + select_candidates(&s, &signed_block, &parent_validators) + }; + + let context = match context { + Some(c) => c, + None => return, + }; + + let recoveries = compute_recoveries(&signed_block, context, log_inv_rate); + if recoveries.is_empty() { + return; + } + + apply_recoveries(&mut store.write(), recoveries); +} diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 59eed12a..05293fa9 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -103,6 +103,8 @@ pub struct Store { /// Block roots that were referenced by attestations but not found in the store. /// Drained by the caller (main.rs) to trigger blocks-by-root RPC fetches. pub pending_fetch_roots: HashSet, + + pub log_inv_rate: usize, } const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3; @@ -205,6 +207,7 @@ pub fn get_forkchoice_store( anchor_block: SignedBlock, config: Config, is_aggregator: bool, + log_inv_rate: usize, ) -> Store { // Extract the plain Block from the signed block let block = anchor_block.block.clone(); @@ -274,6 +277,7 @@ pub fn get_forkchoice_store( pending_attestations: HashMap::new(), pending_aggregated_attestations: HashMap::new(), pending_fetch_roots: HashSet::new(), + log_inv_rate, } } @@ -647,7 +651,7 @@ pub fn prepare_block_production( pub fn execute_block_production( inputs: BlockProductionInputs, -) -> Result<(H256, Block, Vec)> { +) -> Result<(H256, Block, State, Vec)> { let BlockProductionInputs { slot, validator_index, @@ -695,19 +699,20 @@ pub fn execute_block_production( ); if final_post_state.latest_justified.slot < store_latest_justified.slot { - warn!( - produced = final_post_state.latest_justified.slot.0, - store = store_latest_justified.slot.0, - "Produced block justified < store justified. Fixed-point did not converge." - ); METRICS .get() .map(|m| m.lean_build_block_fixed_point_no_converge_total.inc()); + return Err(anyhow!( + "Produced block justified slot {} is behind store justified slot {}; \ + fixed-point attestation loop did not converge", + final_post_state.latest_justified.slot.0, + store_latest_justified.slot.0, + )); } let block_root = final_block.hash_tree_root(); - Ok((block_root, final_block, signatures)) + Ok((block_root, final_block, final_post_state, signatures)) } pub fn produce_block_with_signatures( @@ -717,5 +722,5 @@ pub fn produce_block_with_signatures( log_inv_rate: usize, ) -> Result<(H256, Block, Vec)> { let inputs = prepare_block_production(store, slot, validator_index, log_inv_rate)?; - execute_block_production(inputs) + execute_block_production(inputs).map(|(root, block, _post_state, sigs)| (root, block, sigs)) } 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 dd98731d..514e3e2b 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -1,7 +1,8 @@ use containers::{ AggregatedAttestation, AggregationBits, Attestation, AttestationData, Block, BlockBody, - BlockHeader, BlockSignatures, Checkpoint, Config, HistoricalBlockHashes, JustificationRoots, - JustificationValidators, JustifiedSlots, SignedBlock, Slot, State, Validator, Validators, + BlockHeader, Checkpoint, Config, HistoricalBlockHashes, JustificationRoots, + JustificationValidators, JustifiedSlots, MultiMessageAggregate, SignedBlock, Slot, State, + Validator, Validators, }; use fork_choice::{ block_cache::BlockCache, @@ -221,7 +222,7 @@ impl Into for TestAnchorBlock { SignedBlock { block, - signature: BlockSignatures::default(), + proof: MultiMessageAggregate::default(), } } } @@ -468,7 +469,7 @@ fn forkchoice(spec_file: &str) { body_root, }; - let mut store = get_forkchoice_store(anchor_state, anchor_block, config, false); + let mut store = get_forkchoice_store(anchor_state, anchor_block, config, false, 1); let mut cache = BlockCache::new(); let mut block_labels: HashMap = HashMap::new(); @@ -483,7 +484,7 @@ fn forkchoice(spec_file: &str) { let block: Block = test_block.into(); let signed_block = SignedBlock { block, - signature: BlockSignatures::default(), + proof: MultiMessageAggregate::default(), }; let block_root = signed_block.block.hash_tree_root(); diff --git a/lean_client/fork_choice/tests/unit_tests/common.rs b/lean_client/fork_choice/tests/unit_tests/common.rs index 11b6a92d..c9c71da2 100644 --- a/lean_client/fork_choice/tests/unit_tests/common.rs +++ b/lean_client/fork_choice/tests/unit_tests/common.rs @@ -19,8 +19,8 @@ pub fn create_test_store() -> Store { let signed_block = SignedBlock { block, - signature: Default::default(), + proof: Default::default(), }; - get_forkchoice_store(state, signed_block, config, true) + get_forkchoice_store(state, signed_block, config, true, 1) } diff --git a/lean_client/fork_choice/tests/unit_tests/validator.rs b/lean_client/fork_choice/tests/unit_tests/validator.rs index fedc67d9..7a964e34 100644 --- a/lean_client/fork_choice/tests/unit_tests/validator.rs +++ b/lean_client/fork_choice/tests/unit_tests/validator.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use crate::unit_tests::common::create_test_store; use containers::{ AggregatedSignatureProof, AggregationBits, Attestation, AttestationData, Block, BlockBody, - BlockSignatures, Checkpoint, Config, SignedBlock, Slot, State, Validator, + Checkpoint, Config, MultiMessageAggregate, SignedBlock, Slot, State, Validator, }; use fork_choice::block_cache::BlockCache; use fork_choice::handlers::on_block; @@ -21,7 +21,7 @@ use xmss::SecretKey; fn apply_block(store: &mut Store, block: &Block) { let signed_block = SignedBlock { block: block.clone(), - signature: BlockSignatures::default(), + proof: MultiMessageAggregate::default(), }; let mut cache = BlockCache::new(); on_block(store, &mut cache, signed_block, false).expect("on_block should succeed"); @@ -118,11 +118,11 @@ fn create_test_store_with_signers() -> (Store, HashMap) { let signed_block = SignedBlock { block, - signature: Default::default(), + proof: Default::default(), }; ( - get_forkchoice_store(state, signed_block, config, true), + get_forkchoice_store(state, signed_block, config, true, 1), keys, ) } @@ -511,10 +511,10 @@ fn test_validator_operations_empty_store() { let signed_block = SignedBlock { block: genesis, - signature: Default::default(), + proof: Default::default(), }; - let mut store = get_forkchoice_store(state, signed_block, config, true); + let mut store = get_forkchoice_store(state, signed_block, config, true, 1); // Should be able to produce block and attestation let (_root, block, _sig) = @@ -626,7 +626,7 @@ fn produce_and_apply( produce_block_with_signatures(store, slot, proposer, 1).expect("block production failed"); let signed = SignedBlock { block, - signature: BlockSignatures::default(), + proof: MultiMessageAggregate::default(), }; let block_root = signed.block.hash_tree_root(); on_block(store, cache, signed, false).expect("on_block failed"); @@ -736,7 +736,7 @@ fn test_produce_block_closes_justification_gap() { .expect("build_block for sibling block_6 failed"); let signed_block_6 = SignedBlock { block: block_6, - signature: BlockSignatures::default(), + proof: MultiMessageAggregate::default(), }; let block_6_root = signed_block_6.block.hash_tree_root(); on_block(&mut store, &mut cache, signed_block_6, false).expect("on_block for block_6 failed"); @@ -777,7 +777,7 @@ fn test_produce_block_closes_justification_gap() { let signed_block_7 = SignedBlock { block: block_7, - signature: BlockSignatures::default(), + proof: MultiMessageAggregate::default(), }; on_block(&mut store, &mut cache, signed_block_7, false).expect("on_block for block_7 failed"); assert_eq!(store.latest_justified, block_2_ckpt); diff --git a/lean_client/http_api/src/test_driver.rs b/lean_client/http_api/src/test_driver.rs index 485cd08a..bc1a8308 100644 --- a/lean_client/http_api/src/test_driver.rs +++ b/lean_client/http_api/src/test_driver.rs @@ -17,7 +17,7 @@ use axum::{ Json, Router, body::Bytes, extract::State as AxumState, http::StatusCode, routing::post, }; use containers::{ - AggregatedSignatureProof, BlockSignatures, Config, SignedAggregatedAttestation, + AggregatedSignatureProof, Config, MultiMessageAggregate, SignedAggregatedAttestation, SignedAttestation, SignedBlock, State, }; use fork_choice::{ @@ -179,7 +179,7 @@ async fn init_fork_choice( genesis_time: anchor_state.config.genesis_time, }; - let new_store = get_forkchoice_store(anchor_state, anchor_block, config, false); + let new_store = get_forkchoice_store(anchor_state, anchor_block, config, false, 1); *state.store.write() = new_store; *state.cache.write() = BlockCache::new(); @@ -254,11 +254,7 @@ async fn run_state_transition(body: Bytes) -> Json { let mut last_err: Option = None; for block in blocks { - let signed = SignedBlock { - block, - signature: BlockSignatures::default(), - }; - match state.state_transition(signed, true) { + match state.state_transition(&block) { Ok(next) => state = next, Err(err) => { last_err = Some(err.to_string()); @@ -372,7 +368,7 @@ fn apply_step( let block: containers::Block = block.into(); let signed = SignedBlock { block, - signature: BlockSignatures::default(), + proof: MultiMessageAggregate::default(), }; // Advance store time to the block's slot before applying it. diff --git a/lean_client/metrics/src/metrics.rs b/lean_client/metrics/src/metrics.rs index 688b0de8..39cf7f92 100644 --- a/lean_client/metrics/src/metrics.rs +++ b/lean_client/metrics/src/metrics.rs @@ -164,6 +164,15 @@ pub struct Metrics { /// Number of orphan blocks held in the backfill BlockCache (hard-capped at 1024) pub grandine_block_cache_size: IntGauge, + /// Number of attestations queued waiting for a missing block to arrive + pub grandine_pending_attestations_size: IntGauge, + + /// Number of aggregated attestations queued waiting for a missing block to arrive + pub grandine_pending_aggregated_attestations_size: IntGauge, + + /// Signed blocks in the networking-layer cache (hard-capped at 1024) + pub grandine_signed_block_provider_size: IntGauge, + /// Gap between the network's current slot and the node's head slot (backfill depth) pub grandine_slots_behind: IntGauge, @@ -180,6 +189,9 @@ pub struct Metrics { pub grandine_validator_chain_message_channel_depth: IntGauge, pub grandine_verify_result_channel_depth: IntGauge, pub grandine_cpu_normal_executor_tasks_in_flight: IntGauge, + pub grandine_cpu_low_executor_tasks_in_flight: IntGauge, + pub grandine_reaggregate_candidates_selected_total: IntCounterVec, + pub grandine_reaggregate_split_outcomes_total: IntCounterVec, /// Wall-clock time of the aggregation snapshot deep-clone. After V2, the /// clone runs on the aggregation worker thread (not the chain task), so this @@ -506,6 +518,18 @@ impl Metrics { "grandine_block_cache_size", "Orphan blocks in backfill BlockCache (hard cap 1024)", )?, + grandine_pending_attestations_size: IntGauge::new( + "grandine_pending_attestations_size", + "Attestations queued waiting for a missing block to arrive", + )?, + grandine_pending_aggregated_attestations_size: IntGauge::new( + "grandine_pending_aggregated_attestations_size", + "Aggregated attestations queued waiting for a missing block to arrive", + )?, + grandine_signed_block_provider_size: IntGauge::new( + "grandine_signed_block_provider_size", + "Signed blocks in the networking-layer cache (hard cap 1024)", + )?, grandine_slots_behind: IntGauge::new( "grandine_slots_behind", "Current slot minus head slot — backfill depth and primary OOM risk indicator", @@ -538,6 +562,24 @@ impl Metrics { "grandine_cpu_normal_executor_tasks_in_flight", "Active tasks on cpu_normal DedicatedExecutor (XMSS block verify + attestation signing combined)", )?, + grandine_cpu_low_executor_tasks_in_flight: IntGauge::new( + "grandine_cpu_low_executor_tasks_in_flight", + "Active tasks on cpu_low DedicatedExecutor (reaggregate split_type_2 work)", + )?, + grandine_reaggregate_candidates_selected_total: IntCounterVec::new( + opts!( + "grandine_reaggregate_candidates_selected_total", + "Reaggregate candidate-selection outcomes per block", + ), + &["outcome"], + )?, + grandine_reaggregate_split_outcomes_total: IntCounterVec::new( + opts!( + "grandine_reaggregate_split_outcomes_total", + "Reaggregate Type-2 split_by_message outcomes", + ), + &["outcome"], + )?, lean_aggregation_snapshot_clone_seconds: Histogram::with_opts(histogram_opts!( "lean_aggregation_snapshot_clone_seconds", "Wall-clock time of the aggregation snapshot deep-clone (runs on aggregation worker thread)", @@ -812,6 +854,11 @@ impl Metrics { default_registry.register(Box::new(self.grandine_attestation_data_by_root.clone()))?; default_registry.register(Box::new(self.grandine_pending_fetch_roots.clone()))?; default_registry.register(Box::new(self.grandine_block_cache_size.clone()))?; + default_registry.register(Box::new(self.grandine_pending_attestations_size.clone()))?; + default_registry.register(Box::new( + self.grandine_pending_aggregated_attestations_size.clone(), + ))?; + default_registry.register(Box::new(self.grandine_signed_block_provider_size.clone()))?; default_registry.register(Box::new(self.grandine_slots_behind.clone()))?; default_registry.register(Box::new( self.grandine_fork_choice_known_attestations.clone(), @@ -826,6 +873,15 @@ impl Metrics { default_registry.register(Box::new( self.grandine_cpu_normal_executor_tasks_in_flight.clone(), ))?; + default_registry.register(Box::new( + self.grandine_cpu_low_executor_tasks_in_flight.clone(), + ))?; + default_registry.register(Box::new( + self.grandine_reaggregate_candidates_selected_total.clone(), + ))?; + default_registry.register(Box::new( + self.grandine_reaggregate_split_outcomes_total.clone(), + ))?; default_registry.register(Box::new( self.lean_aggregation_snapshot_clone_seconds.clone(), ))?; diff --git a/lean_client/networking/Cargo.toml b/lean_client/networking/Cargo.toml index 2cbe592f..563b2fdb 100644 --- a/lean_client/networking/Cargo.toml +++ b/lean_client/networking/Cargo.toml @@ -16,7 +16,6 @@ hex = { workspace = true } k256 = { workspace = true } libp2p = { workspace = true } libp2p-identity = { workspace = true } -libp2p-mplex = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } rand = { workspace = true } diff --git a/lean_client/networking/src/network/behaviour.rs b/lean_client/networking/src/network/behaviour.rs index 1ea17040..61d60098 100644 --- a/lean_client/networking/src/network/behaviour.rs +++ b/lean_client/networking/src/network/behaviour.rs @@ -8,6 +8,7 @@ pub struct LeanNetworkBehaviour { pub identify: identify::Behaviour, pub status_req_resp: ReqResp, pub blocks_by_root_req_resp: ReqResp, + pub blocks_by_range_req_resp: ReqResp, pub gossipsub: GossipsubBehaviour, pub connection_limits: connection_limits::Behaviour, } diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index b3484b16..75d66d58 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -44,9 +44,9 @@ use crate::{ network::behaviour::{LeanNetworkBehaviour, LeanNetworkBehaviourEvent}, req_resp::{self, LeanRequest, ReqRespMessage}, types::{ - ChainMessage, ChainMessageSink, ConnectionState, MAX_BLOCK_CACHE_SIZE, - NetworkFinalizedSlot, OutboundP2pRequest, P2pRequestSource, SignedBlockProvider, - StatusProvider, + CanonicalBlocksProvider, ChainMessage, ChainMessageSink, ConnectionState, + MAX_BLOCK_CACHE_SIZE, NetworkFinalizedSlot, OutboundP2pRequest, P2pRequestSource, + SignedBlockProvider, StatusProvider, }, }; @@ -193,6 +193,8 @@ where signed_block_provider: SignedBlockProvider, /// Shared status provider for Status req/resp protocol status_provider: StatusProvider, + /// Canonical-blocks-by-range provider for serving BlocksByRange requests + blocks_by_range_provider: CanonicalBlocksProvider, /// Pending BlocksByRoot requests for retry on empty response pending_blocks_by_root: HashMap, /// Depth tracking per block root for limiting backward chain walking @@ -201,6 +203,9 @@ where in_flight_roots: HashSet, network_finalized_slot: NetworkFinalizedSlot, peer_finalized_slots: HashMap, + /// Peer head slots reported via Status; used by the caller to decide + /// when a backfill should switch from per-root BlocksByRoot to batched BlocksByRange. + peer_head_slots: HashMap, status_notify: Arc, } @@ -215,6 +220,7 @@ where chain_message_sink: S, signed_block_provider: SignedBlockProvider, status_provider: StatusProvider, + blocks_by_range_provider: CanonicalBlocksProvider, network_finalized_slot: NetworkFinalizedSlot, status_notify: Arc, ) -> Result { @@ -225,6 +231,7 @@ where Arc::new(AtomicU64::new(0)), signed_block_provider, status_provider, + blocks_by_range_provider, network_finalized_slot, status_notify, ) @@ -238,6 +245,7 @@ where peer_count: Arc, signed_block_provider: SignedBlockProvider, status_provider: StatusProvider, + blocks_by_range_provider: CanonicalBlocksProvider, network_finalized_slot: NetworkFinalizedSlot, status_notify: Arc, ) -> Result { @@ -250,6 +258,7 @@ where local_key, signed_block_provider, status_provider, + blocks_by_range_provider, network_finalized_slot, status_notify, ) @@ -264,6 +273,7 @@ where local_key: Keypair, signed_block_provider: SignedBlockProvider, status_provider: StatusProvider, + blocks_by_range_provider: CanonicalBlocksProvider, network_finalized_slot: NetworkFinalizedSlot, status_notify: Arc, ) -> Result { @@ -272,7 +282,8 @@ where let config = Config::with_tokio_executor() .with_notify_handler_buffer_size(NonZeroUsize::new(7).unwrap()) .with_per_connection_event_buffer_size(4) - .with_dial_concurrency_factor(NonZeroU8::new(1).unwrap()); + .with_dial_concurrency_factor(NonZeroU8::new(1).unwrap()) + .with_idle_connection_timeout(Duration::from_secs(u64::MAX)); let multiaddr = Self::multiaddr(&network_config)?; let swarm = SwarmBuilder::with_existing_identity(local_key.clone()) @@ -318,11 +329,13 @@ where chain_message_sink, signed_block_provider, status_provider, + blocks_by_range_provider, pending_blocks_by_root: HashMap::new(), pending_block_depths: HashMap::new(), in_flight_roots: HashSet::new(), network_finalized_slot, peer_finalized_slots: HashMap::new(), + peer_head_slots: HashMap::new(), status_notify, }; @@ -437,6 +450,9 @@ where LeanNetworkBehaviourEvent::BlocksByRootReqResp(event) => { self.handle_blocks_by_root_req_resp_event(event) } + LeanNetworkBehaviourEvent::BlocksByRangeReqResp(event) => { + self.handle_blocks_by_range_req_resp_event(event) + } LeanNetworkBehaviourEvent::Identify(event) => self.handle_identify_event(event), LeanNetworkBehaviourEvent::ConnectionLimits(_) => { // ConnectionLimits behaviour has no events @@ -494,6 +510,7 @@ where self.peer_count.store(connected, Ordering::Relaxed); self.peer_finalized_slots.remove(&peer_id); + self.peer_head_slots.remove(&peer_id); self.recompute_network_finalized_slot(); info!(peer = %peer_id, ?cause, "Disconnected from peer (total: {})", connected); @@ -608,6 +625,7 @@ where signed_block, is_trusted: false, should_gossip: true, + cached_post_state: None, }) .await { @@ -854,6 +872,10 @@ where provider.remove(&root); } } + METRICS.get().map(|m| { + m.grandine_signed_block_provider_size + .set(provider.len() as i64) + }); } // Step 2: Collect unique parent roots that are not yet received. @@ -891,24 +913,57 @@ where }; if !unknown_parents.is_empty() { - info!( - num_parents = unknown_parents.len(), - depth = next_depth, - "Pipelining parent fetch before chain processing" - ); - for &root in &unknown_parents { - self.pending_block_depths.insert(root, next_depth); - } - // Chunk and send; each chunk goes to a random peer. - for chunk in unknown_parents.chunks(MAX_BLOCKS_PER_REQUEST) { - if let Some(peer_id) = self.get_random_connected_peer() { - self.send_blocks_by_root_request_internal( + const RANGE_CASCADE_TRIGGER: usize = 3; + const RANGE_CASCADE_COUNT: u64 = 256; + + if unknown_parents.len() >= RANGE_CASCADE_TRIGGER { + let min_received_slot = blocks + .iter() + .map(|b| b.block.slot.0) + .min() + .unwrap_or(0); + let start_slot = + min_received_slot.saturating_sub(RANGE_CASCADE_COUNT); + let chosen = self + .peer_head_slots + .iter() + .find(|(_, head)| **head >= min_received_slot) + .map(|(p, _)| *p) + .or_else(|| self.get_random_connected_peer()); + if let Some(peer_id) = chosen { + info!( + num_parents = unknown_parents.len(), + start_slot, + count = RANGE_CASCADE_COUNT, + "Pipelining backfill via BlocksByRange" + ); + self.send_blocks_by_range_request( peer_id, - chunk.to_vec(), - 0, - next_depth, + start_slot, + RANGE_CASCADE_COUNT, ); } + } else { + info!( + num_parents = unknown_parents.len(), + depth = next_depth, + "Pipelining parent fetch before chain processing" + ); + for &root in &unknown_parents { + self.pending_block_depths.insert(root, next_depth); + } + for chunk in unknown_parents.chunks(MAX_BLOCKS_PER_REQUEST) + { + if let Some(peer_id) = self.get_random_connected_peer() + { + self.send_blocks_by_root_request_internal( + peer_id, + chunk.to_vec(), + 0, + next_depth, + ); + } + } } } } @@ -924,6 +979,7 @@ where signed_block: block, is_trusted: false, should_gossip: false, + cached_post_state: None, }) .await { @@ -1202,6 +1258,7 @@ where our_head_slot: u64, ) { self.peer_finalized_slots.insert(peer, peer_finalized_slot); + self.peer_head_slots.insert(peer, peer_head_slot); self.recompute_network_finalized_slot(); if (peer_finalized_slot > our_finalized_slot || peer_head_slot > our_head_slot) @@ -1303,6 +1360,22 @@ where } } } + OutboundP2pRequest::RequestBlocksByRange { start_slot, count } => { + let mut chosen: Option = None; + let target = start_slot.saturating_add(count.saturating_sub(1)); + for (peer, head) in &self.peer_head_slots { + if *head >= target { + chosen = Some(*peer); + break; + } + } + let peer_id = chosen.or_else(|| self.get_random_connected_peer()); + if let Some(peer_id) = peer_id { + self.send_blocks_by_range_request(peer_id, start_slot, count); + } else { + debug!("BlocksByRange: no connected peer to dispatch to"); + } + } OutboundP2pRequest::RequestBlocksByRoot(roots) => { // Look up and validate depth for each root // Depth is set when we receive a block and track its parent @@ -1411,6 +1484,33 @@ where self.send_blocks_by_root_request_with_depth(peer_id, roots, 0); } + pub fn send_blocks_by_range_request(&mut self, peer_id: PeerId, start_slot: u64, count: u64) { + if count == 0 { + return; + } + if count > req_resp::MAX_REQUEST_BLOCKS as u64 { + warn!( + peer = %peer_id, + count, + max = req_resp::MAX_REQUEST_BLOCKS, + "BlocksByRange request exceeds MAX_REQUEST_BLOCKS" + ); + return; + } + info!( + peer = %peer_id, + start_slot, + count, + "Sending BlocksByRange request" + ); + let request = LeanRequest::BlocksByRange { start_slot, count }; + let _request_id = self + .swarm + .behaviour_mut() + .blocks_by_range_req_resp + .send_request(&peer_id, request); + } + pub fn send_blocks_by_root_request_with_depth( &mut self, peer_id: PeerId, @@ -1470,6 +1570,130 @@ where }); } + fn handle_blocks_by_range_req_resp_event( + &mut self, + event: ReqRespMessage, + ) -> Option { + use crate::req_resp::{LeanRequest, LeanResponse}; + use libp2p::request_response::{Event, Message}; + + match event { + Event::Message { peer, message, .. } => match message { + Message::Request { + request, channel, .. + } => { + let (start_slot, count) = match request { + LeanRequest::BlocksByRange { start_slot, count } => (start_slot, count), + _ => { + warn!(peer = %peer, "Unexpected request type on BlocksByRange protocol"); + return None; + } + }; + + if count == 0 || count > req_resp::MAX_REQUEST_BLOCKS as u64 { + info!(peer = %peer, start_slot, count, "Rejecting BlocksByRange: invalid count"); + // Send an empty response — peer will treat as no data. + let response = LeanResponse::BlocksByRange(Vec::new()); + if let Err(e) = self + .swarm + .behaviour_mut() + .blocks_by_range_req_resp + .send_response(channel, response) + { + warn!(peer = %peer, ?e, "Failed to send BlocksByRange error response"); + } + return None; + } + + info!(peer = %peer, start_slot, count, "Received BlocksByRange request"); + + let blocks = (self.blocks_by_range_provider)(start_slot, count); + + info!( + peer = %peer, + start_slot, + count, + found = blocks.len(), + "Serving BlocksByRange response" + ); + + let response = LeanResponse::BlocksByRange(blocks); + if let Err(e) = self + .swarm + .behaviour_mut() + .blocks_by_range_req_resp + .send_response(channel, response) + { + warn!(peer = %peer, ?e, "Failed to send BlocksByRange response"); + } + } + Message::Response { response, .. } => match response { + LeanResponse::BlocksByRange(blocks) => { + info!( + peer = %peer, + num_blocks = blocks.len(), + "Received BlocksByRange response" + ); + + { + let mut provider = self.signed_block_provider.write(); + for block in &blocks { + let root = block.block.hash_tree_root(); + provider.insert(root, block.clone()); + } + 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.block.slot.0)) + .collect(); + slots.sort_by_key(|(_, slot)| *slot); + for (root, _) in slots.into_iter().take(to_remove) { + provider.remove(&root); + } + } + METRICS.get().map(|m| { + m.grandine_signed_block_provider_size + .set(provider.len() as i64) + }); + } + + let chain_sink = self.chain_message_sink.clone(); + tokio::spawn(async move { + for block in blocks { + let slot = block.block.slot.0; + if let Err(e) = chain_sink + .send(ChainMessage::ProcessBlock { + signed_block: block, + is_trusted: false, + should_gossip: false, + cached_post_state: None, + }) + .await + { + warn!(slot, ?e, "Failed to forward range block to chain"); + } + } + }); + } + _ => { + warn!(peer = %peer, "Unexpected response type on BlocksByRange protocol"); + } + }, + }, + Event::OutboundFailure { peer, error, .. } => { + warn!(peer = %peer, ?error, "BlocksByRange outbound request failed"); + } + Event::InboundFailure { peer, error, .. } => { + warn!(peer = %peer, ?error, "BlocksByRange inbound request failed"); + } + Event::ResponseSent { peer, .. } => { + trace!(peer = %peer, "BlocksByRange response sent"); + } + } + None + } + fn build_behaviour( local_key: &Keypair, cfg: &NetworkServiceConfig, @@ -1484,6 +1708,7 @@ where let status_req_resp = req_resp::build_status(); let blocks_by_root_req_resp = req_resp::build_blocks_by_root(); + let blocks_by_range_req_resp = req_resp::build_blocks_by_range(); let connection_limits = connection_limits::Behaviour::new( ConnectionLimits::default() @@ -1496,6 +1721,7 @@ where identify, status_req_resp, blocks_by_root_req_resp, + blocks_by_range_req_resp, gossipsub, connection_limits, }) diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index 933347d4..9e90be2c 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -15,10 +15,12 @@ use tracing::warn; use typenum::U1024; pub const MAX_REQUEST_BLOCKS: usize = 1024; +pub const MIN_SLOTS_FOR_BLOCK_REQUESTS: u64 = 3600; pub const MAX_PAYLOAD_SIZE: usize = 10 * 1024 * 1024; // 10 MiB pub const STATUS_PROTOCOL_V1: &str = "/leanconsensus/req/status/1/ssz_snappy"; pub const BLOCKS_BY_ROOT_PROTOCOL_V1: &str = "/leanconsensus/req/blocks_by_root/1/ssz_snappy"; +pub const BLOCKS_BY_RANGE_PROTOCOL_V1: &str = "/leanconsensus/req/blocks_by_range/1/ssz_snappy"; /// Response codes for req/resp protocol messages. pub const RESPONSE_SUCCESS: u8 = 0; @@ -33,6 +35,12 @@ pub struct BlocksByRootRequest { pub roots: RequestedBlockRoots, } +#[derive(Clone, Debug, PartialEq, Eq, Ssz)] +pub struct BlocksByRangeRequest { + pub start_slot: u64, + pub count: u64, +} + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct LeanProtocol(pub String); @@ -46,12 +54,14 @@ impl AsRef for LeanProtocol { pub enum LeanRequest { Status(Status), BlocksByRoot(Vec), + BlocksByRange { start_slot: u64, count: u64 }, } #[derive(Debug, Clone)] pub enum LeanResponse { Status(Status), BlocksByRoot(Vec), + BlocksByRange(Vec), Empty, } @@ -132,6 +142,15 @@ impl LeanCodec { io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) })? } + LeanRequest::BlocksByRange { start_slot, count } => { + let request = BlocksByRangeRequest { + start_slot: *start_slot, + count: *count, + }; + request.to_ssz().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) + })? + } }; if ssz_bytes.len() > MAX_PAYLOAD_SIZE { @@ -215,6 +234,17 @@ impl LeanCodec { )); } Ok(LeanRequest::BlocksByRoot(roots)) + } else if protocol.contains("blocks_by_range") { + let request = BlocksByRangeRequest::from_ssz_default(&ssz_bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("SSZ decode BlocksByRangeRequest failed: {e:?}"), + ) + })?; + Ok(LeanRequest::BlocksByRange { + start_slot: request.start_slot, + count: request.count, + }) } else { Err(io::Error::new( io::ErrorKind::Other, @@ -256,22 +286,34 @@ impl LeanCodec { // Empty response: no chunks written (stream just ends) Ok(result) } + LeanResponse::BlocksByRange(blocks) => { + let mut result = Vec::new(); + for block in blocks { + let ssz_bytes = block.to_ssz().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) + })?; + let chunk = Self::encode_response_chunk(RESPONSE_SUCCESS, &ssz_bytes)?; + result.extend(chunk); + } + Ok(result) + } LeanResponse::Empty => Ok(Vec::new()), } } - /// Returns the byte length of one snappy framing stream starting at data[0]. + /// Returns the byte length consumed by one ssz-snappy framed payload starting at data[0]. /// - /// Snappy framing format (https://github.com/google/snappy/blob/main/framing_format.txt): - /// Stream identifier chunk: [0xFF][0x06][0x00][0x00][s][N][a][P][p][Y] - /// Data chunk: [type][len_lo][len_mid][len_hi][data...] + /// Driven by `expected_uncompressed_size` from the response's varint prefix: we walk + /// chunk-by-chunk, accumulating each chunk's uncompressed contribution, and stop when + /// we've collected the declared message size. /// - /// We scan through chunk headers to advance without decompressing. The - /// stream identifier byte 0xFF cannot legally appear as a chunk type inside - /// a stream, so hitting 0xFF after the first chunk signals the start of the - /// next framing stream and therefore the end of this one. - fn snappy_frame_size(data: &[u8]) -> io::Result { - // Stream identifier is 10 bytes: 4-byte header + 6-byte "sNaPpY" + /// Snappy framing format (https://github.com/google/snappy/blob/main/framing_format.txt): + /// 0xFF (stream identifier): fixed 10 bytes: [0xFF][0x06 0x00 0x00][s][N][a][P][p][Y] + /// 0x00 (compressed data): [type][len:3LE][crc:4][snappy_block] + /// 0x01 (uncompressed data): [type][len:3LE][crc:4][raw_bytes] + /// 0x80..=0xFF (reserved skippable): skip whole chunk, contributes 0 to uncompressed + /// 0x02..=0x7F (reserved unskippable): MUST reject per spec + fn snappy_frame_size(data: &[u8], expected_uncompressed_size: usize) -> io::Result { const STREAM_ID: &[u8] = b"\xff\x06\x00\x00sNaPpY"; if data.len() < STREAM_ID.len() || &data[..STREAM_ID.len()] != STREAM_ID { return Err(io::Error::new( @@ -281,31 +323,78 @@ impl LeanCodec { } let mut pos = STREAM_ID.len(); - while pos < data.len() { - // 0xFF marks the start of a new snappy stream — this stream ends here. - if data[pos] == 0xFF { - break; - } + let mut uncompressed_len = 0_usize; + + while uncompressed_len < expected_uncompressed_size { if pos + 4 > data.len() { return Err(io::Error::new( io::ErrorKind::UnexpectedEof, "Truncated snappy chunk header", )); } - // 3-byte LE length field (bytes 1..=3 of header) + let chunk_type = data[pos]; let chunk_len = u32::from_le_bytes([data[pos + 1], data[pos + 2], data[pos + 3], 0]) as usize; + if pos + 4 + chunk_len > data.len() { return Err(io::Error::new( - io::ErrorKind::InvalidData, + io::ErrorKind::UnexpectedEof, format!( - "snappy chunk_len {} at pos {} exceeds buffer len {}", + "Truncated snappy chunk: type=0x{:02x} chunk_len={} at pos={} buffer={}", + chunk_type, chunk_len, pos, data.len() ), )); } + let chunk_payload = &data[pos + 4..pos + 4 + chunk_len]; + + let chunk_uncompressed_len = match chunk_type { + // Compressed: [4-byte CRC][snappy block]. snap::raw::decompress_len reads the + // uncompressed length from the block's own varint header without decompressing. + 0x00 => { + let block = chunk_payload.get(4..).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "Compressed chunk missing CRC") + })?; + snap::raw::decompress_len(block).map_err(|err| { + io::Error::new(io::ErrorKind::InvalidData, format!("snappy len: {err}")) + })? + } + // Uncompressed: [4-byte CRC][raw bytes] → contributes chunk_len - 4 bytes. + 0x01 => chunk_len.checked_sub(4).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "Uncompressed chunk missing CRC") + })?, + // Reserved skippable (includes mid-stream 0xFF stream identifier). + 0x80..=0xFF => 0, + // Reserved unskippable: MUST reject per snappy framing spec. + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Reserved unskippable snappy chunk type 0x{:02x}", + chunk_type + ), + )); + } + }; + + uncompressed_len = uncompressed_len + .checked_add(chunk_uncompressed_len) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "Uncompressed size overflow") + })?; + + if uncompressed_len > expected_uncompressed_size { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Snappy uncompressed_len {} exceeds varint-declared {}", + uncompressed_len, expected_uncompressed_size + ), + )); + } + pos += 4 + chunk_len; } @@ -343,7 +432,7 @@ impl LeanCodec { // Determine the byte length of this snappy framing stream so we know // exactly where the next chunk begins (required for multi-block responses). - let frame_size = Self::snappy_frame_size(&data[payload_start..])?; + let frame_size = Self::snappy_frame_size(&data[payload_start..], declared_len as usize)?; let payload_end = payload_start + frame_size; let ssz_bytes = Self::decompress(&data[payload_start..payload_end])?; @@ -415,6 +504,33 @@ impl LeanCodec { blocks.push(block); } Ok(LeanResponse::BlocksByRoot(blocks)) + } else if protocol.contains("blocks_by_range") { + let mut blocks = Vec::new(); + let mut offset = 0; + while offset < data.len() { + let (code, ssz_bytes, consumed) = Self::decode_response_chunk(&data[offset..])?; + offset += consumed; + + if code != RESPONSE_SUCCESS { + warn!( + response_code = code, + "BlocksByRange non-success response chunk" + ); + continue; + } + if ssz_bytes.is_empty() { + continue; + } + + let block = SignedBlock::from_ssz_default(&ssz_bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("SSZ decode Block failed: {e:?}"), + ) + })?; + blocks.push(block); + } + Ok(LeanResponse::BlocksByRange(blocks)) } else { Err(io::Error::new( io::ErrorKind::Other, @@ -424,64 +540,71 @@ impl LeanCodec { } } -#[async_trait] impl Codec for LeanCodec { type Protocol = LeanProtocol; type Request = LeanRequest; type Response = LeanResponse; - async fn read_request( + fn read_request( &mut self, protocol: &Self::Protocol, io: &mut T, - ) -> io::Result + ) -> impl core::future::Future> + Send where T: AsyncRead + Unpin + Send, { - let mut data = Vec::new(); - io.read_to_end(&mut data).await?; - Self::decode_request(&protocol.0, &data) + async move { + let mut data = Vec::new(); + io.read_to_end(&mut data).await?; + Self::decode_request(&protocol.0, &data) + } } - async fn read_response( + fn read_response( &mut self, protocol: &Self::Protocol, io: &mut T, - ) -> io::Result + ) -> impl core::future::Future> + Send where T: AsyncRead + Unpin + Send, { - let mut data = Vec::new(); - io.read_to_end(&mut data).await?; - Self::decode_response(&protocol.0, &data) + async move { + let mut data = Vec::new(); + io.read_to_end(&mut data).await?; + Self::decode_response(&protocol.0, &data) + } } - async fn write_request( + fn write_request( &mut self, _protocol: &Self::Protocol, io: &mut T, request: Self::Request, - ) -> io::Result<()> + ) -> impl core::future::Future> + Send where T: AsyncWrite + Unpin + Send, { - let data = Self::encode_request(&request)?; - io.write_all(&data).await?; - io.close().await + async move { + let data = Self::encode_request(&request)?; + io.write_all(&data).await?; + io.close().await + } } - async fn write_response( + fn write_response( &mut self, _protocol: &Self::Protocol, io: &mut T, response: Self::Response, - ) -> io::Result<()> + ) -> impl core::future::Future> + Send where T: AsyncWrite + Unpin + Send, { - let data = Self::encode_response(&response)?; - io.write_all(&data).await?; - io.close().await + async move { + let data = Self::encode_response(&response)?; + io.write_all(&data).await?; + io.close().await + } } } @@ -503,62 +626,69 @@ pub struct GenericRequest(pub Vec); #[derive(Debug, Clone, PartialEq, Eq)] pub struct GenericResponse(pub Vec); -#[async_trait] impl Codec for GenericCodec { type Protocol = GenericProtocol; type Request = GenericRequest; type Response = GenericResponse; - async fn read_request( + fn read_request( &mut self, _protocol: &Self::Protocol, io: &mut T, - ) -> io::Result + ) -> impl core::future::Future> + Send where T: AsyncRead + Unpin + Send, { - let mut data = Vec::new(); - io.read_to_end(&mut data).await?; - Ok(GenericRequest(data)) + async move { + let mut data = Vec::new(); + io.read_to_end(&mut data).await?; + Ok(GenericRequest(data)) + } } - async fn read_response( + fn read_response( &mut self, _protocol: &Self::Protocol, io: &mut T, - ) -> io::Result + ) -> impl core::future::Future> + Send where T: AsyncRead + Unpin + Send, { - let mut data = Vec::new(); - io.read_to_end(&mut data).await?; - Ok(GenericResponse(data)) + async move { + let mut data = Vec::new(); + io.read_to_end(&mut data).await?; + Ok(GenericResponse(data)) + } } - async fn write_request( + fn write_request( &mut self, _protocol: &Self::Protocol, io: &mut T, GenericRequest(data): Self::Request, - ) -> io::Result<()> + ) -> impl core::future::Future> + Send where T: AsyncWrite + Unpin + Send, { - io.write_all(&data).await?; - io.close().await + async move { + io.write_all(&data).await?; + io.close().await + } } - async fn write_response( + fn write_response( &mut self, _protocol: &Self::Protocol, io: &mut T, GenericResponse(data): Self::Response, - ) -> io::Result<()> + ) -> impl core::future::Future> + Send where T: AsyncWrite + Unpin + Send, { - io.write_all(&data).await?; - io.close().await + async move { + io.write_all(&data).await?; + io.close().await + } } } @@ -589,3 +719,8 @@ pub fn build_status() -> ReqResp { pub fn build_blocks_by_root() -> ReqResp { build(vec![BLOCKS_BY_ROOT_PROTOCOL_V1.to_string()]) } + +/// Build a RequestResponse behavior for BlocksByRange protocol only +pub fn build_blocks_by_range() -> ReqResp { + build(vec![BLOCKS_BY_RANGE_PROTOCOL_V1.to_string()]) +} diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index 9881ed51..f1f3dab6 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, SignedBlock, Slot, Status, + SignedAttestation, SignedBlock, Slot, State, Status, Validators, }; use metrics::METRICS; use parking_lot::{Mutex, RwLock}; @@ -23,6 +23,12 @@ pub const MAX_BLOCK_CACHE_SIZE: usize = 1024; /// Allows NetworkService to look up signed blocks for checkpoint sync backfill. pub type SignedBlockProvider = Arc>>; +/// Provider for serving BlocksByRange requests. +/// Returns canonical blocks in slot-ascending order within +/// `[start_slot, start_slot + count - 1]`. Empty slots are skipped, so the +/// resulting Vec may be shorter than `count`. +pub type CanonicalBlocksProvider = Arc Vec + Send + Sync>; + /// Shared status provider for Status req/resp protocol. /// Allows NetworkService to send accurate finalized/head checkpoints to peers. pub type StatusProvider = Arc>; @@ -148,6 +154,11 @@ pub enum ChainMessage { signed_block: SignedBlock, is_trusted: bool, should_gossip: bool, + /// Cached post-state from this node's own `execute_block_production`. + /// When `is_trusted = true` and this is `Some`, the chain task skips the + /// redundant `verify_and_transition` and applies the cached state directly. + /// Spec-aligned: leanSpec `build_block` returns the post-state for this reason. + cached_post_state: Option, }, ProcessAttestation { signed_attestation: SignedAttestation, @@ -168,6 +179,7 @@ impl ChainMessage { signed_block, is_trusted: false, should_gossip: true, + cached_post_state: None, } } @@ -222,7 +234,7 @@ pub enum ValidatorChainMessage { ProduceBlock { slot: Slot, proposer_index: u64, - sender: oneshot::Sender)>>, + sender: oneshot::Sender, Validators, State)>>, }, /// Request attestation data for the given slot. /// Chain reads the current head/justified/target state and returns it via sender. @@ -242,6 +254,10 @@ pub enum OutboundP2pRequest { /// Devnet-3: Gossip aggregated attestation to aggregation topic GossipAggregation(SignedAggregatedAttestation), RequestBlocksByRoot(Vec), + RequestBlocksByRange { + start_slot: u64, + count: u64, + }, } #[async_trait] diff --git a/lean_client/spec_test_fixtures/src/common.rs b/lean_client/spec_test_fixtures/src/common.rs index 9fe70775..fa61a8fb 100644 --- a/lean_client/spec_test_fixtures/src/common.rs +++ b/lean_client/spec_test_fixtures/src/common.rs @@ -7,13 +7,13 @@ use containers::{ AggregatedAttestation, AggregatedSignatureProof, AggregationBits, Attestation, AttestationData, - AttestationSignatures, Block, BlockBody, BlockHeader, BlockSignatures, Checkpoint, Config, - HistoricalBlockHashes, JustificationRoots, JustificationValidators, JustifiedSlots, - SignedBlock, Slot, State, Validator, Validators, + Block, BlockBody, BlockHeader, Checkpoint, Config, HistoricalBlockHashes, JustificationRoots, + JustificationValidators, JustifiedSlots, MultiMessageAggregate, SignedBlock, Slot, State, + Validator, Validators, }; use serde::Deserialize; use ssz::{BitList, H256, PersistentList}; -use xmss::{AggregatedSignature, PublicKey, Signature}; +use xmss::{AggregatedSignature, PublicKey}; // === Primitive wrappers ==================================================== @@ -377,7 +377,7 @@ impl From for SignedBlock { Self { block, - signature: BlockSignatures::default(), + proof: MultiMessageAggregate::default(), } } } @@ -392,25 +392,19 @@ pub struct HexBytesJSON { pub data: String, } -/// Fixture-shape signed block. Mirrors the JSON the simulator POSTs. +/// Fixture-shape signed block (devnet5). /// -/// `containers::SignedBlock` deserializes its `signature` field via -/// `xmss::Signature`, whose JSON form is the structured XMSS object -/// (`{path, rho, hashes}`). leanSpec fixtures, however, ship signatures as -/// plain hex strings (`"0x24…"`) and aggregated proofs as -/// `{participants:{data:[bool]}, proofData:{data:"0x…"}}`. This type matches -/// the wire format and provides a `TryFrom` conversion to the consensus type. +/// Mirrors leanSpec's `SignedBlock { block, proof: MultiMessageAggregate }` +/// where `MultiMessageAggregate` is the container `{ proof: ByteList512KiB }`. #[derive(Debug, Deserialize)] pub struct TestSignedBlock { pub block: TestBlock, - pub signature: TestBlockSignaturesFixture, + pub proof: TestMultiMessageAggregateFixture, } #[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TestBlockSignaturesFixture { - pub attestation_signatures: TestDataWrapper, - pub proposer_signature: String, +pub struct TestMultiMessageAggregateFixture { + pub proof: HexBytesJSON, } /// One entry inside `signature.attestationSignatures.data`. @@ -426,27 +420,10 @@ impl TryFrom for SignedBlock { fn try_from(value: TestSignedBlock) -> Result { let block = value.block.into(); - - let proposer_signature: Signature = value - .signature - .proposer_signature - .parse() - .map_err(|err| format!("invalid hex in proposer_signature: {err}"))?; - - let mut attestation_signatures = AttestationSignatures::default(); - for entry in value.signature.attestation_signatures.data { - let proof: AggregatedSignatureProof = entry.try_into()?; - PersistentList::push(&mut attestation_signatures, proof) - .map_err(|err| format!("attestation_signatures push: {err:?}"))?; - } - - Ok(Self { - block, - signature: BlockSignatures { - attestation_signatures, - proposer_signature, - }, - }) + let bytes = decode_hex(&value.proof.proof.data)?; + let proof = MultiMessageAggregate::new(&bytes) + .map_err(|err| format!("multi-message aggregate decode: {err}"))?; + Ok(Self { block, proof }) } } diff --git a/lean_client/spec_test_fixtures/src/lib.rs b/lean_client/spec_test_fixtures/src/lib.rs index 8df25d91..6a3a05d6 100644 --- a/lean_client/spec_test_fixtures/src/lib.rs +++ b/lean_client/spec_test_fixtures/src/lib.rs @@ -19,9 +19,9 @@ pub mod verify_signatures; pub use common::{ HexBytesJSON, TestAggregatedAttestation, TestAggregatedSignatureProofFixture, TestAggregationBits, TestAnchorBlock, TestAnchorState, TestAttestation, TestAttestationData, - TestBlock, TestBlockBody, TestBlockHeader, TestBlockSignaturesFixture, - TestBlockWithAttestation, TestCheckpoint, TestConfig, TestDataWrapper, TestSignedBlock, - TestValidator, parse_root, + TestBlock, TestBlockBody, TestBlockHeader, TestBlockWithAttestation, TestCheckpoint, + TestConfig, TestDataWrapper, TestMultiMessageAggregateFixture, TestSignedBlock, TestValidator, + parse_root, }; pub use fork_choice::{ AttestationCheck, ForkChoiceStep, ForkChoiceTest, GossipAggregatedAttestationStep, diff --git a/lean_client/spec_test_fixtures/src/verify_signatures.rs b/lean_client/spec_test_fixtures/src/verify_signatures.rs index dd6c362d..7fb1fb26 100644 --- a/lean_client/spec_test_fixtures/src/verify_signatures.rs +++ b/lean_client/spec_test_fixtures/src/verify_signatures.rs @@ -32,6 +32,8 @@ pub struct VerifySignaturesTestCase { pub signed_block: TestSignedBlock, #[serde(default)] pub expect_exception: Option, + #[serde(default)] + pub rejection_reason: Option, /// `_info` is a metadata blob the test harness emits for traceability. /// Not all fixtures populate every sub-field (and we do not consume any /// of them), so it is fully optional. diff --git a/lean_client/src/aggregation.rs b/lean_client/src/aggregation.rs index 4a861717..67123843 100644 --- a/lean_client/src/aggregation.rs +++ b/lean_client/src/aggregation.rs @@ -1,15 +1,18 @@ use std::collections::HashSet; use std::sync::Arc; -use std::time::Instant; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; use containers::{SignedAggregatedAttestation, Slot}; +use dedicated_executor::DedicatedExecutor; use fork_choice::store::Store; use metrics::METRICS; use parking_lot::RwLock; use ssz::H256; use tokio::sync::{mpsc, watch}; -use tokio::task; -use validator::ValidatorService; +use validator::{ValidatorService, snapshot_aggregation_inputs}; + +const AGGREGATION_DEADLINE: Duration = Duration::from_millis(750); /// Aggregation service that decouples XMSS aggregation from the chain task. /// @@ -37,7 +40,12 @@ impl AggregationService { /// The worker holds an `Arc>` reference so it can read + /// clone the snapshot on its own thread instead of forcing the chain task /// to do the deep-clone. - pub fn new(vs: Arc, store: Arc>, log_rate: usize) -> Self { + pub fn new( + vs: Arc, + store: Arc>, + log_rate: usize, + cpu_snark_executor: Arc, + ) -> Self { let (agg_tx, mut agg_rx) = watch::channel::>(None); let (res_tx, res_rx) = mpsc::channel::<( u64, @@ -46,7 +54,7 @@ impl AggregationService { let vs_for_worker = vs.clone(); - task::spawn(async move { + tokio::spawn(async move { loop { if agg_rx.changed().await.is_err() { break; // sender dropped — chain task shut down @@ -59,38 +67,58 @@ impl AggregationService { METRICS .get() .map(|m| m.lean_aggregation_in_flight_snapshots.inc()); - let result = task::spawn_blocking(move || { - // Clone on this worker thread: brief read lock held only for - // the duration of the clone, then released before XMSS work. - let clone_start = Instant::now(); - let snapshot = { - let guard = store.read(); - guard.clone() - }; - let clone_elapsed = clone_start.elapsed(); - - METRICS.get().map(|m| { - m.lean_aggregation_snapshot_clone_seconds - .observe(clone_elapsed.as_secs_f64()); - let entries = snapshot.blocks.len() - + snapshot.states.len() - + snapshot.gossip_signatures.len() - + snapshot.latest_known_aggregated_payloads.len() - + snapshot.latest_new_aggregated_payloads.len() - + snapshot.attestation_data_by_root.len(); - m.lean_aggregation_snapshot_size_entries - .observe(entries as f64); - }); - - vs.maybe_aggregate(&snapshot, Slot(slot), log_rate) - }) - .await - .unwrap_or(None); + + let snapshot_start = Instant::now(); + let snapshot = { + let guard = store.read(); + snapshot_aggregation_inputs(&guard) + }; + let snapshot_elapsed = snapshot_start.elapsed(); + + let Some(snapshot) = snapshot else { + METRICS + .get() + .map(|m| m.lean_aggregation_in_flight_snapshots.dec()); + if res_tx.send((slot, None)).await.is_err() { + break; + } + continue; + }; + + METRICS.get().map(|m| { + m.lean_aggregation_snapshot_clone_seconds + .observe(snapshot_elapsed.as_secs_f64()); + let entries: usize = snapshot + .jobs + .iter() + .map(|j| j.children.len() + j.raw_ids.len() + j.accepted_child_ids.len()) + .sum::() + + snapshot.jobs.len(); + m.lean_aggregation_snapshot_size_entries + .observe(entries as f64); + }); + + let cancel = Arc::new(AtomicBool::new(false)); + let cancel_for_timer = cancel.clone(); + let deadline_handle = tokio::spawn(async move { + tokio::time::sleep(AGGREGATION_DEADLINE).await; + cancel_for_timer.store(true, Ordering::Relaxed); + }); + + let cancel_for_worker = cancel.clone(); + let snark_exec = cpu_snark_executor.clone(); + let job = snark_exec.spawn(async move { + vs.maybe_aggregate(&snapshot, Slot(slot), log_rate, &cancel_for_worker) + }); + let result = job.await.unwrap_or(None); + + deadline_handle.abort(); + METRICS .get() .map(|m| m.lean_aggregation_in_flight_snapshots.dec()); if res_tx.send((slot, result)).await.is_err() { - break; // chain task dropped — shut down + break; } } }); @@ -115,13 +143,16 @@ impl AggregationService { let _ = self.agg_tx.send(Some(slot)); } - /// Returns the next completed aggregation result, or `None` if none is ready. - pub fn poll( + /// Awaits the next completed aggregation result. Returns `None` only when + /// the worker has shut down. Used as a `tokio::select!` arm on the chain + /// task so results land in `latest_new_aggregated_payloads` the moment the + /// SNARK finishes — not on the next 800 ms tick. + pub async fn recv( &mut self, ) -> Option<( u64, Option<(Vec, HashSet)>, )> { - self.res_rx.try_recv().ok() + self.res_rx.recv().await } } diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index bffcf2d2..71bfb9ae 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -5,8 +5,8 @@ static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; use anyhow::{Context as _, Result}; use clap::Parser; use containers::{ - Block, BlockBody, BlockHeader, BlockSignatures, Checkpoint, Config, SignedBlock, Slot, State, - Status, Validator, + Block, BlockBody, BlockHeader, Checkpoint, Config, MultiMessageAggregate, SignedBlock, Slot, + State, Status, Validator, }; use dedicated_executor::DedicatedExecutor; use ethereum_types::H256; @@ -14,8 +14,8 @@ use features::Feature; use fork_choice::{ block_cache::BlockCache, handlers::{ - apply_verified_block, on_aggregated_attestation, on_attestation, on_gossip_attestation, - on_tick, verify_and_transition, + apply_verified_block, on_aggregated_attestation_async, on_attestation, + on_gossip_attestation, on_tick, verify_and_transition, }, store::{ INTERVALS_PER_SLOT, MILLIS_PER_INTERVAL, Store, execute_block_production, @@ -30,8 +30,8 @@ use networking::gossipsub::config::GossipsubConfig; use networking::gossipsub::topic::{compute_subnet_id, get_subscription_topics}; use networking::network::{NetworkService, NetworkServiceConfig}; use networking::types::{ - ChainMessage, MAX_BLOCK_CACHE_SIZE, NetworkFinalizedSlot, OutboundP2pRequest, - SignedBlockProvider, StatusProvider, ValidatorChainMessage, + CanonicalBlocksProvider, ChainMessage, MAX_BLOCK_CACHE_SIZE, NetworkFinalizedSlot, + OutboundP2pRequest, SignedBlockProvider, StatusProvider, ValidatorChainMessage, }; use parking_lot::{Mutex, RwLock}; use ssz::{PersistentList, SszHash, SszReadDefault as _}; @@ -43,7 +43,7 @@ use std::{io::IsTerminal, net::IpAddr}; use tokio::{ sync::{Notify, mpsc, oneshot, watch}, task, - time::{Duration, Instant, interval, interval_at}, + time::{Duration, Instant, MissedTickBehavior, interval, interval_at}, }; use tracing::level_filters::LevelFilter; use tracing::{debug, error, info, warn}; @@ -52,6 +52,30 @@ use xmss::{PublicKey, Signature}; mod aggregation; +const BACKFILL_ORPHAN_TRIGGER: usize = 5; +const BACKFILL_RANGE_COUNT: u64 = 256; + +fn dispatch_backfill( + missing: Vec, + local_head_slot: u64, + orphan_count: usize, + outbound_p2p_sender: &mpsc::UnboundedSender, +) { + if orphan_count >= BACKFILL_ORPHAN_TRIGGER { + let request = OutboundP2pRequest::RequestBlocksByRange { + start_slot: local_head_slot.saturating_add(1), + count: BACKFILL_RANGE_COUNT, + }; + if let Err(e) = outbound_p2p_sender.send(request) { + warn!("Failed to dispatch BlocksByRange backfill: {}", e); + } + } else if !missing.is_empty() { + if let Err(e) = outbound_p2p_sender.send(OutboundP2pRequest::RequestBlocksByRoot(missing)) { + warn!("Failed to dispatch BlocksByRoot backfill: {}", e); + } + } +} + fn load_node_key(path: &str) -> Result> { let hex_str = std::fs::read_to_string(path)?.trim().to_string(); let bytes = hex::decode(&hex_str)?; @@ -398,6 +422,22 @@ struct Args { #[tokio::main] async fn main() -> Result<()> { + let available = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(2); + let rayon_threads = available.saturating_sub(2).max(1); + match rayon::ThreadPoolBuilder::new() + .num_threads(rayon_threads) + .build_global() + { + Ok(()) => eprintln!( + "configured global rayon pool: available={available} rayon_threads={rayon_threads}" + ), + Err(e) => eprintln!( + "global rayon pool already initialized; rayon_threads cap NOT applied: available={available} err={e}" + ), + } + xmss::setup_aggregation(); tracing_subscriber::fmt() @@ -463,12 +503,8 @@ async fn main() -> Result<()> { None, None, )); - let cpu_low_executor = Arc::new(DedicatedExecutor::new( - "lean-cpu-low", - (num_cpus / 4).max(1), - Some(19), - None, - )); + let cpu_snark_executor = Arc::new(DedicatedExecutor::new("lean-snark", 1, None, None)); + let cpu_verify_executor = Arc::new(DedicatedExecutor::new("lean-verify", 1, None, None)); // Verified blocks travel back from the executor to the chain task on this channel. // Tuple: (block_root, signed_block, should_gossip, verify outcome). @@ -553,10 +589,7 @@ async fn main() -> Result<()> { let genesis_signed_block = SignedBlock { block: genesis_block, - signature: BlockSignatures { - attestation_signatures: PersistentList::default(), - proposer_signature: Signature::default(), - }, + proof: MultiMessageAggregate::default(), }; let config = Config { genesis_time }; @@ -637,6 +670,8 @@ async fn main() -> Result<()> { num_validators, keys_path, cpu_normal_executor.clone(), + cpu_verify_executor.clone(), + cpu_snark_executor.clone(), args.is_aggregator, ) { Ok(service) => { @@ -658,6 +693,8 @@ async fn main() -> Result<()> { config, num_validators, cpu_normal_executor.clone(), + cpu_verify_executor.clone(), + cpu_snark_executor.clone(), args.is_aggregator, )) } @@ -671,6 +708,8 @@ async fn main() -> Result<()> { config, num_validators, cpu_normal_executor.clone(), + cpu_verify_executor.clone(), + cpu_snark_executor.clone(), args.is_aggregator, )) } @@ -685,6 +724,8 @@ async fn main() -> Result<()> { config, num_validators, cpu_normal_executor.clone(), + cpu_verify_executor.clone(), + cpu_snark_executor.clone(), args.is_aggregator, )) } @@ -704,6 +745,7 @@ async fn main() -> Result<()> { let validator_service: Option> = validator_service.map(Arc::new); // Chain task clone: used only for tick-2 aggregation (maybe_aggregate takes &self) let vs_for_chain = validator_service.clone(); + let vs_for_chain_proposer = validator_service.clone(); // Validator task: takes ownership for proposal and attestation duties let vs_for_validator = validator_service.clone(); // Admin API controller: toggles is_aggregator at runtime on both store and validator service @@ -785,6 +827,39 @@ async fn main() -> Result<()> { let network_finalized_slot: NetworkFinalizedSlot = Arc::new(Mutex::new(None)); let network_finalized_slot_for_network = network_finalized_slot.clone(); + let canonical_blocks_provider: CanonicalBlocksProvider = { + let block_provider = signed_block_provider.clone(); + let status = status_provider.clone(); + Arc::new(move |start_slot: u64, count: u64| -> Vec { + if count == 0 { + return Vec::new(); + } + let end_slot = start_slot.saturating_add(count.saturating_sub(1)); + let head_root = status.read().head.root; + let mut by_slot: HashMap = HashMap::new(); + { + let blocks = block_provider.read(); + let mut current_root = head_root; + while !current_root.is_zero() { + let Some(block) = blocks.get(¤t_root) else { + break; + }; + let slot = block.block.slot.0; + if slot < start_slot { + break; + } + if slot <= end_slot { + by_slot.insert(slot, block.clone()); + } + current_root = block.block.parent_root; + } + } + let mut result: Vec<(u64, SignedBlock)> = by_slot.into_iter().collect(); + result.sort_by_key(|(s, _)| *s); + result.into_iter().map(|(_, b)| b).collect() + }) + }; + let status_notify = Arc::new(Notify::new()); // LOAD NODE KEY @@ -801,6 +876,7 @@ async fn main() -> Result<()> { keypair, signed_block_provider_for_network, status_provider_for_network, + canonical_blocks_provider.clone(), network_finalized_slot_for_network, status_notify.clone(), ) @@ -816,6 +892,7 @@ async fn main() -> Result<()> { peer_count, signed_block_provider_for_network, status_provider_for_network, + canonical_blocks_provider.clone(), network_finalized_slot_for_network, status_notify.clone(), ) @@ -831,6 +908,7 @@ async fn main() -> Result<()> { peer_count, signed_block_provider_for_network, status_provider_for_network, + canonical_blocks_provider.clone(), network_finalized_slot_for_network, status_notify.clone(), ) @@ -952,6 +1030,7 @@ async fn main() -> Result<()> { anchor_block, config.clone(), args.is_aggregator, + genesis_log_inv_rate as usize, ))); // Seed the block provider so we can serve the anchor block to peers via BlocksByRoot. @@ -1047,8 +1126,14 @@ async fn main() -> Result<()> { // watch channel = lossless latest-value semantics: send() always overwrites so // the aggregation task always sees the most recent trigger even if XMSS was // still running when a newer slot arrived. No trigger is ever silently dropped. - let mut aggregator = vs_for_chain - .map(|vs| aggregation::AggregationService::new(vs, store.clone(), chain_log_inv_rate)); + let mut aggregator = vs_for_chain.map(|vs| { + aggregation::AggregationService::new( + vs, + store.clone(), + chain_log_inv_rate, + cpu_snark_executor.clone(), + ) + }); // Whether this node has any validator service at all. Used for sync-state // coloring; NOT a per-slot aggregator gate. The per-slot aggregator check // lives at the snapshot trigger site below and goes through @@ -1063,6 +1148,7 @@ async fn main() -> Result<()> { Instant::now() + genesis_tick_delay, Duration::from_millis(MILLIS_PER_INTERVAL), ); + tick_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); let mut last_logged_slot = 0u64; // Track the last slot for which aggregation was triggered so the catch-up // guard never fires twice for the same slot. @@ -1091,8 +1177,9 @@ async fn main() -> Result<()> { // processing. The first tick after startup has nothing to // measure against and is skipped. let tick_instant = Instant::now(); - if let Some(prev) = last_tick_instant { - let elapsed = tick_instant.duration_since(prev).as_secs_f64(); + let tick_elapsed = last_tick_instant + .map(|prev| tick_instant.duration_since(prev).as_secs_f64()); + if let Some(elapsed) = tick_elapsed { METRICS.get().map(|m| { m.lean_tick_interval_duration_seconds.observe(elapsed) }); @@ -1104,14 +1191,23 @@ async fn main() -> Result<()> { .unwrap() .as_millis() as u64; let target_interval = now_millis.saturating_sub(genesis_millis) / MILLIS_PER_INTERVAL; - let has_proposal = target_interval % INTERVALS_PER_SLOT == 0; + let target_slot = target_interval / INTERVALS_PER_SLOT; + let next_is_interval_0 = target_interval % INTERVALS_PER_SLOT == 0; + let has_proposal = next_is_interval_0 + && target_slot > 0 + && vs_for_chain_proposer + .as_ref() + .and_then(|vs| vs.get_proposer_for_slot(Slot(target_slot))) + .is_some(); on_tick(&mut *store.write(), now_millis, has_proposal); // Sample the cpu_normal executor's queue depth once per tick. // Counts both XMSS block verify and attestation signing tasks. METRICS.get().map(|m| { m.grandine_cpu_normal_executor_tasks_in_flight - .set(cpu_normal_executor.tasks() as i64) + .set(cpu_normal_executor.tasks() as i64); + m.grandine_cpu_low_executor_tasks_in_flight + .set(cpu_snark_executor.tasks() as i64) }); let (current_slot, current_interval) = { @@ -1119,6 +1215,15 @@ async fn main() -> Result<()> { (s.time / INTERVALS_PER_SLOT, s.time % INTERVALS_PER_SLOT) }; + if let Some(elapsed) = tick_elapsed { + info!( + slot = current_slot, + interval = current_interval, + duration_s = elapsed, + "tick" + ); + } + if last_status_slot != Some(current_slot) { let peers = peer_count.load(Ordering::Relaxed); print_chain_status(&*store.read(), peers); @@ -1129,29 +1234,11 @@ async fn main() -> Result<()> { evaluate_sync_state(&mut sync_state, peers, head_slot, nf); } - // ── Drain completed aggregation results (non-blocking) ──────────── - // Results arrive here from the dedicated aggregation task. - // Draining every tick (800 ms) is fast enough to stay within - // the gossip broadcast window. - while let Some((agg_slot, maybe_agg)) = aggregator.as_mut().and_then(|a| a.poll()) { - if let Some((aggregations, consumed_data_roots)) = maybe_agg { - for aggregation in aggregations { - if let Err(e) = chain_outbound_sender.send( - OutboundP2pRequest::GossipAggregation(aggregation) - ) { - 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 = agg_slot, "Aggregation phase - broadcast aggregated attestations"); - } else { - info!("Aggregation phase - no aggregation duty or no attestations"); - } - } + // Aggregation results no longer drained here; a dedicated + // `select!` arm below wakes the chain task the moment the + // worker finishes, removing the up-to-800 ms poll lag that + // caused interval-3 `update_safe_target` to read an empty + // `latest_new_aggregated_payloads`. // ── Aggregation catch-up / interval-2 guard ─────────────────────── // Trigger aggregation whenever we reach OR pass interval 2 for a @@ -1260,6 +1347,25 @@ async fn main() -> Result<()> { info!("Block processed successfully"); let _ = block_slot_tx.send(block_slot.0); + { + let exec = cpu_verify_executor.clone(); + let store_for_reagg = store.clone(); + let signed_block_for_reagg = signed_block.clone(); + let log_inv_rate = chain_log_inv_rate; + tokio::spawn(async move { + let job = exec.spawn(async move { + fork_choice::reaggregate::run_in_executor( + store_for_reagg, + signed_block_for_reagg, + log_inv_rate, + ); + }); + if let Err(e) = job.await { + warn!("reaggregate executor failed: {e}"); + } + }); + } + { let s = store.read(); let mut status = status_provider.write(); @@ -1305,7 +1411,7 @@ async fn main() -> Result<()> { store.read().states.get(&block_root).cloned() { for (child_root, child_block) in cascade_children { - let exec = cpu_normal_executor.clone(); + let exec = cpu_verify_executor.clone(); let result_tx = verify_result_tx.clone(); let parent_state_for_child = cascade_parent_state.clone(); @@ -1350,17 +1456,18 @@ async fn main() -> Result<()> { .get() .map(|m| m.grandine_block_cache_size.set(block_cache.len() as i64)); - // Drain block roots queued by retried attestations inside - // apply_verified_block. let missing: Vec = store.write().pending_fetch_roots.drain().collect(); METRICS.get().map(|m| m.grandine_pending_fetch_roots.set(0)); - if !missing.is_empty() { - if let Err(e) = outbound_p2p_sender.send( - OutboundP2pRequest::RequestBlocksByRoot(missing) - ) { - warn!("Failed to request blocks missing from retried attestations: {}", e); - } - } + let local_head_slot = { + let s = store.read(); + s.blocks.get(&s.head).map(|b| b.slot.0).unwrap_or(0) + }; + dispatch_backfill( + missing, + local_head_slot, + block_cache.orphan_count(), + &outbound_p2p_sender, + ); } Err(e) => warn!( block_root = %format!("0x{:x}", block_root), @@ -1374,6 +1481,41 @@ async fn main() -> Result<()> { ), } } + Some((agg_slot, maybe_agg)) = async { + aggregator.as_mut().unwrap().recv().await + }, if aggregator.is_some() => { + if let Some((aggregations, consumed_data_roots)) = maybe_agg { + let mut to_publish = Vec::with_capacity(aggregations.len()); + { + let mut s = store.write(); + for aggregation in aggregations { + let data_root = aggregation.data.hash_tree_root(); + s.latest_new_aggregated_payloads + .entry(data_root) + .or_default() + .push(aggregation.proof.clone()); + to_publish.push(aggregation); + } + s.gossip_signatures.retain(|key, _| { + !consumed_data_roots.contains(&key.data_root) + }); + METRICS.get().map(|m| { + m.lean_latest_new_aggregated_payloads + .set(s.latest_new_aggregated_payloads.len() as i64); + }); + } + for aggregation in to_publish { + if let Err(e) = chain_outbound_sender.send( + OutboundP2pRequest::GossipAggregation(aggregation) + ) { + warn!("Failed to gossip aggregation: {}", e); + } + } + info!(slot = agg_slot, "Aggregation phase - broadcast aggregated attestations"); + } else { + info!("Aggregation phase - no aggregation duty or no attestations"); + } + } message = chain_message_receiver.recv() => { let Some(message) = message else { break }; METRICS.get().map(|m| m.grandine_chain_message_channel_depth.dec()); @@ -1387,6 +1529,7 @@ async fn main() -> Result<()> { signed_block, is_trusted, should_gossip, + cached_post_state, } => { if should_gossip && !is_trusted && !sync_state.accepts_gossip() { debug!( @@ -1473,15 +1616,14 @@ async fn main() -> Result<()> { let missing: Vec = store.write().pending_fetch_roots.drain().collect(); METRICS.get().map(|m| m.grandine_pending_fetch_roots.set(0)); - if !missing.is_empty() { - if let Err(req_err) = outbound_p2p_sender.send( - OutboundP2pRequest::RequestBlocksByRoot(missing) - ) { - warn!("Failed to request missing parent block: {}", req_err); - } - } - let head_slot = { let s = store.read(); s.blocks.get(&s.head).map(|b| b.slot.0).unwrap_or(0) }; + dispatch_backfill( + missing, + head_slot, + block_cache.orphan_count(), + &outbound_p2p_sender, + ); + let nf = *network_finalized_slot.lock(); check_sync_trigger(&mut sync_state, head_slot, nf); check_sync_complete(&mut sync_state, head_slot, block_cache.orphan_count(), nf); @@ -1494,22 +1636,29 @@ async fn main() -> Result<()> { // the verified outcome later via verify_result_rx (Phase 3). // No store mutation happens here; gossip rebroadcast and post-apply // bookkeeping are deferred to Phase 3 once we know the block applied. - let exec = cpu_normal_executor.clone(); + // Self-built short-circuit: when this is our own freshly-produced block + // (`is_trusted = true` + cached post-state), skip the redundant STF — + // leanSpec's `build_block` already returned the post-state, no need to + // recompute it. Mirrors zeam's planned fix per their devnet5 chat. + let exec = cpu_verify_executor.clone(); let result_tx = verify_result_tx.clone(); let signed_block_for_verify = signed_block.clone(); + let verify_signatures = !is_trusted; METRICS .get() .map(|m| m.grandine_verify_result_channel_depth.inc()); tokio::spawn(async move { let signed_block_for_send = signed_block_for_verify.clone(); - let job = exec.spawn(async move { - verify_and_transition(parent_state, signed_block_for_verify, true) - }); - let outcome = job - .await - .unwrap_or_else(|e| { - Err(anyhow::anyhow!("executor: {e}")) + let outcome = if is_trusted && cached_post_state.is_some() { + Ok(cached_post_state.unwrap()) + } else { + let job = exec.spawn(async move { + verify_and_transition(parent_state, signed_block_for_verify, verify_signatures) }); + job.await.unwrap_or_else(|e| { + Err(anyhow::anyhow!("executor: {e}")) + }) + }; if result_tx .send((block_root, signed_block_for_send, should_gossip, outcome)) .await @@ -1573,13 +1722,16 @@ async fn main() -> Result<()> { let missing: Vec = store.write().pending_fetch_roots.drain().collect(); METRICS.get().map(|m| m.grandine_pending_fetch_roots.set(0)); - if !missing.is_empty() { - if let Err(e) = outbound_p2p_sender.send( - OutboundP2pRequest::RequestBlocksByRoot(missing) - ) { - warn!("Failed to request blocks missing from attestation: {}", e); - } - } + let local_head_slot = { + let s = store.read(); + s.blocks.get(&s.head).map(|b| b.slot.0).unwrap_or(0) + }; + dispatch_backfill( + missing, + local_head_slot, + block_cache.orphan_count(), + &outbound_p2p_sender, + ); } ChainMessage::ProcessAggregation { signed_aggregated_attestation, @@ -1604,32 +1756,41 @@ async fn main() -> Result<()> { .filter(|b| **b) .count(); - match on_aggregated_attestation(&mut *store.write(), signed_aggregated_attestation.clone()) { - Ok(_) => { - info!( - slot = agg_slot, - validators = validator_count, - "Processed aggregated attestation for safe target" - ); - } - Err(e) => { - warn!( - slot = agg_slot, - error = %e, - "Failed to process aggregated attestation" - ); - } + { + let exec = cpu_verify_executor.clone(); + let store_for_verify = store.clone(); + let signed_for_verify = signed_aggregated_attestation.clone(); + tokio::spawn(async move { + let job = exec.spawn(async move { + on_aggregated_attestation_async(store_for_verify, signed_for_verify) + }); + match job.await.unwrap_or_else(|e| Err(anyhow::anyhow!("executor: {e}"))) { + Ok(_) => info!( + slot = agg_slot, + validators = validator_count, + "Processed aggregated attestation for safe target" + ), + Err(e) => warn!( + slot = agg_slot, + error = %e, + "Failed to process aggregated attestation" + ), + } + }); } let missing: Vec = store.write().pending_fetch_roots.drain().collect(); METRICS.get().map(|m| m.grandine_pending_fetch_roots.set(0)); - if !missing.is_empty() { - if let Err(e) = outbound_p2p_sender.send( - OutboundP2pRequest::RequestBlocksByRoot(missing) - ) { - warn!("Failed to request blocks missing from aggregated attestation: {}", e); - } - } + let local_head_slot = { + let s = store.read(); + s.blocks.get(&s.head).map(|b| b.slot.0).unwrap_or(0) + }; + dispatch_backfill( + missing, + local_head_slot, + block_cache.orphan_count(), + &outbound_p2p_sender, + ); // Gossip the aggregation if needed if should_gossip { @@ -1662,33 +1823,39 @@ async fn main() -> Result<()> { } Ok(inputs) => { let agg_payload_count = inputs.aggregated_payloads.len(); + let validators = inputs.head_state.validators.clone(); + let exec = cpu_snark_executor.clone(); let exec_start = Instant::now(); - let result = task::spawn_blocking(move || { - execute_block_production(inputs) - .map(|(_, block, sigs)| (block, sigs)) - }) - .await - .unwrap_or_else(|e| Err(anyhow::anyhow!("block production task panicked: {e}"))); - - let exec_elapsed = exec_start.elapsed(); - let total_elapsed = block_build_start.elapsed(); - match &result { - Ok(_) => { - METRICS.get().map(|m| { - m.lean_block_building_success_total.inc(); - m.lean_block_building_time_seconds - .observe(total_elapsed.as_secs_f64()); - m.lean_block_building_payload_aggregation_time_seconds - .observe(exec_elapsed.as_secs_f64()); - m.lean_block_aggregated_payloads - .observe(agg_payload_count as f64); - }); - } - Err(_) => { - METRICS.get().map(|m| m.lean_block_building_failures_total.inc()); + tokio::spawn(async move { + let job = exec.spawn(async move { + execute_block_production(inputs) + .map(|(_, block, post_state, sigs)| (block, post_state, sigs)) + }); + let result = job + .await + .unwrap_or_else(|e| Err(anyhow::anyhow!("executor: {e}"))) + .map(|(block, post_state, sigs)| (block, sigs, validators, post_state)); + + let exec_elapsed = exec_start.elapsed(); + let total_elapsed = block_build_start.elapsed(); + match &result { + Ok(_) => { + METRICS.get().map(|m| { + m.lean_block_building_success_total.inc(); + m.lean_block_building_time_seconds + .observe(total_elapsed.as_secs_f64()); + m.lean_block_building_payload_aggregation_time_seconds + .observe(exec_elapsed.as_secs_f64()); + m.lean_block_aggregated_payloads + .observe(agg_payload_count as f64); + }); + } + Err(_) => { + METRICS.get().map(|m| m.lean_block_building_failures_total.inc()); + } } - } - let _ = sender.send(result); + let _ = sender.send(result); + }); } } } @@ -1724,8 +1891,10 @@ async fn main() -> Result<()> { Instant::now() + genesis_tick_delay, Duration::from_millis(MILLIS_PER_INTERVAL), ); + v_tick_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); let mut last_proposal_slot: Option = None; let mut last_attestation_slot: Option = None; + let mut propose_handle: Option> = None; loop { v_tick_interval.tick().await; @@ -1745,6 +1914,16 @@ async fn main() -> Result<()> { if current_slot > 0 { if let Some(proposer_idx) = vs.get_proposer_for_slot(Slot(current_slot)) { + if let Some(prev) = propose_handle.take() { + if !prev.is_finished() { + prev.abort(); + info!( + slot = current_slot, + "Validator task: aborting prior in-flight propose" + ); + } + } + info!( slot = current_slot, proposer = proposer_idx, @@ -1769,54 +1948,89 @@ async fn main() -> Result<()> { break; } - let (block, signatures) = match rx.await { - Ok(Ok(pair)) => pair, - Ok(Err(e)) => { - warn!(slot = current_slot, error = %e, "Validator task: chain failed to produce block"); - last_proposal_slot = Some(current_slot); - continue; - } - Err(_) => { + let vs_for_spawn = vs.clone(); + let chain_sender_for_spawn = chain_msg_sender_for_validator.clone(); + let log_inv_rate = chain_log_inv_rate; + let propose_slot = current_slot; + let genesis_millis_for_spawn = genesis_millis; + + propose_handle = Some(tokio::spawn(async move { + let (block, signatures, validators, cached_post_state) = + match rx.await { + Ok(Ok(tuple)) => tuple, + Ok(Err(e)) => { + warn!(slot = propose_slot, error = %e, "Validator task: chain failed to produce block"); + return; + } + Err(_) => { + warn!( + slot = propose_slot, + "Validator task: no response to ProduceBlock" + ); + return; + } + }; + + let now_millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() + as u64; + let wall_slot = now_millis + .saturating_sub(genesis_millis_for_spawn) + / MILLIS_PER_INTERVAL + / INTERVALS_PER_SLOT; + if wall_slot > propose_slot { warn!( - slot = current_slot, - "Validator task: no response to ProduceBlock" + slot = propose_slot, + wall_slot, + lag_slots = wall_slot - propose_slot, + "Late propose — publishing anyway", ); - last_proposal_slot = Some(current_slot); - continue; } - }; - match vs.sign_block_with_data(block, proposer_idx, signatures) { - Ok(signed_block) => { - let block_root = signed_block.block.hash_tree_root(); - info!( - slot = current_slot, - block_root = %format!("0x{:x}", block_root), - "Validator task: block signed, sending to chain" - ); - let send_result = chain_msg_sender_for_validator - .send(ChainMessage::ProcessBlock { - signed_block, - is_trusted: true, - should_gossip: true, - }) - .await; - if send_result.is_ok() { - METRICS.get().map(|m| { - m.grandine_chain_message_channel_depth.inc() - }); - } - if send_result.is_err() { - warn!( - "Validator task: chain message channel closed, stopping" + match vs_for_spawn + .sign_block_with_data( + block, + proposer_idx, + signatures, + validators, + log_inv_rate, + ) + .await + { + Ok(signed_block) => { + let block_root = signed_block.block.hash_tree_root(); + info!( + slot = propose_slot, + block_root = %format!("0x{:x}", block_root), + "Validator task: block signed, sending to chain" ); - break; + let send_result = chain_sender_for_spawn + .send(ChainMessage::ProcessBlock { + signed_block, + is_trusted: true, + should_gossip: true, + cached_post_state: Some(cached_post_state), + }) + .await; + if send_result.is_ok() { + METRICS.get().map(|m| { + m.grandine_chain_message_channel_depth.inc() + }); + } + if send_result.is_err() { + warn!( + slot = propose_slot, + "Validator task: chain message channel closed" + ); + } + } + Err(e) => { + warn!(slot = propose_slot, error = %e, "Validator task: failed to sign block") } } - Err(e) => { - warn!(slot = current_slot, error = %e, "Validator task: failed to sign block") - } - } + })); } } last_proposal_slot = Some(current_slot); diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 771938e4..04ad77c5 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -8,14 +8,13 @@ use serde::Deserialize; use anyhow::{Context, Result, anyhow, bail}; use containers::{ - AggregatedSignatureProof, AggregationBits, AttestationData, AttestationSignatures, Block, - BlockSignatures, SignedAggregatedAttestation, SignedAttestation, SignedBlock, Slot, + AggregatedSignatureProof, AggregationBits, AttestationData, Block, MultiMessageAggregate, + SignatureKey, SignedAggregatedAttestation, SignedAttestation, SignedBlock, Slot, Validators, }; use dedicated_executor::DedicatedExecutor; -use fork_choice::store::Store; +use fork_choice::store::{INTERVALS_PER_SLOT, Store}; use futures::stream::{FuturesUnordered, StreamExt}; use metrics::{METRICS, stop_and_discard, stop_and_record}; -use rayon::prelude::*; use ssz::H256; use ssz::SszHash; use tracing::{info, warn}; @@ -24,7 +23,142 @@ use try_from_iterator::TryFromIterator as _; pub mod keys; use keys::KeyManager; -use xmss::{PublicKey, Signature}; +use xmss::{AggregatedSignature, PublicKey, Signature}; + +pub struct AggregationJob { + pub data_root: H256, + pub attestation_data: AttestationData, + pub children: Vec<(Vec, AggregatedSignatureProof)>, + pub accepted_child_ids: Vec, + pub raw_pubkeys: Vec, + pub raw_sigs: Vec, + pub raw_ids: Vec, +} + +pub struct AggregationSnapshot { + pub jobs: Vec, +} + +pub const AGGREGATION_SLOT_LOOKBACK: u64 = 1; + +pub fn snapshot_aggregation_inputs(store: &Store) -> Option { + if store.gossip_signatures.is_empty() && store.latest_new_aggregated_payloads.is_empty() { + return None; + } + + let head_state = store.states.get(&store.head)?; + let head_validators = &head_state.validators; + + let current_slot = store.time / INTERVALS_PER_SLOT; + let min_eligible_slot = current_slot.saturating_sub(AGGREGATION_SLOT_LOOKBACK); + + let mut gossip_groups: HashMap> = HashMap::new(); + for (sig_key, signature) in &store.gossip_signatures { + gossip_groups + .entry(sig_key.data_root) + .or_default() + .push((sig_key.validator_id, signature.clone())); + } + + 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); + } + + let mut jobs: Vec = Vec::with_capacity(all_data_roots.len()); + + for data_root in all_data_roots { + let Some(attestation_data) = store.attestation_data_by_root.get(&data_root) else { + continue; + }; + + if attestation_data.slot.0 < min_eligible_slot { + continue; + } + + let mut children_refs: 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_refs, + &mut covered_by_children, + ); + extend_children_greedily( + store + .latest_known_aggregated_payloads + .get(&data_root) + .map(|v| v.as_slice()) + .unwrap_or(&[]), + &mut children_refs, + &mut covered_by_children, + ); + + let mut entries: Vec<(u64, Signature)> = gossip_groups + .get(&data_root) + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|(vid, _)| { + !covered_by_children.contains(vid) && head_validators.get(*vid).is_ok() + }) + .collect(); + entries.sort_by_key(|(vid, _)| *vid); + + let mut raw_ids: Vec = Vec::with_capacity(entries.len()); + let mut raw_pubkeys: Vec = Vec::with_capacity(entries.len()); + let mut raw_sigs: Vec = Vec::with_capacity(entries.len()); + for (vid, sig) in entries { + let validator = head_validators.get(vid).unwrap(); + raw_ids.push(vid); + raw_pubkeys.push(validator.attestation_pubkey.clone()); + raw_sigs.push(sig); + } + + if raw_ids.is_empty() && children_refs.len() < 2 { + continue; + } + + let mut children: Vec<(Vec, AggregatedSignatureProof)> = + Vec::with_capacity(children_refs.len()); + for child in &children_refs { + let pks: Vec = child + .participants + .to_validator_indices() + .into_iter() + .filter_map(|vid| { + head_validators + .get(vid) + .ok() + .map(|v| v.attestation_pubkey.clone()) + }) + .collect(); + children.push((pks, (*child).clone())); + } + + let mut accepted_child_ids: Vec = covered_by_children.into_iter().collect(); + accepted_child_ids.sort(); + + jobs.push(AggregationJob { + data_root, + attestation_data: attestation_data.clone(), + children, + accepted_child_ids, + raw_pubkeys, + raw_sigs, + raw_ids, + }); + } + + if jobs.is_empty() { + return None; + } + + Some(AggregationSnapshot { jobs }) +} /// Entry in an annotated_validators.yaml file. /// Each validator index has two entries: one for the attester key and one for the proposer key, @@ -107,6 +241,15 @@ pub struct ValidatorService { /// Shared CPU pool used for offloading XMSS attestation signing off the /// validator task / chain task. cpu_normal_executor: Arc, + /// Pool used for non-aggregation SNARK verify work (block-verify, + /// reaggregate, gossip-verify). Kept for symmetry with the main executor + /// split — not used by block-signing, which lives on `cpu_snark_executor`. + cpu_verify_executor: Arc, + /// Pool used for the SNARK-heavy block-signing work + /// (Type-1 wrap + Type-2 merge inside `sign_block_with_data`). Same pool as + /// aggregation; the propose/aggregation timelines never overlap (interval 0 + /// vs interval 2), so co-tenancy here doesn't queue. + cpu_snark_executor: Arc, /// Whether this node performs aggregation duties (devnet-3). /// Uses `AtomicBool` for interior mutability so the admin API can toggle /// the flag at runtime without requiring `&mut self` or a write lock. @@ -163,14 +306,25 @@ impl ValidatorService { config: ValidatorConfig, num_validators: u64, cpu_normal_executor: Arc, + cpu_verify_executor: Arc, + cpu_snark_executor: Arc, ) -> Self { - Self::new_with_aggregator(config, num_validators, cpu_normal_executor, false) + Self::new_with_aggregator( + config, + num_validators, + cpu_normal_executor, + cpu_verify_executor, + cpu_snark_executor, + false, + ) } pub fn new_with_aggregator( config: ValidatorConfig, num_validators: u64, cpu_normal_executor: Arc, + cpu_verify_executor: Arc, + cpu_snark_executor: Arc, is_aggregator: bool, ) -> Self { info!( @@ -192,6 +346,8 @@ impl ValidatorService { num_validators, key_manager: None, cpu_normal_executor, + cpu_verify_executor, + cpu_snark_executor, is_aggregator: AtomicBool::new(is_aggregator), } } @@ -201,12 +357,16 @@ impl ValidatorService { num_validators: u64, keys_dir: impl AsRef, cpu_normal_executor: Arc, + cpu_verify_executor: Arc, + cpu_snark_executor: Arc, ) -> Result> { Self::new_with_keys_and_aggregator( config, num_validators, keys_dir, cpu_normal_executor, + cpu_verify_executor, + cpu_snark_executor, false, ) } @@ -216,6 +376,8 @@ impl ValidatorService { num_validators: u64, keys_dir: impl AsRef, cpu_normal_executor: Arc, + cpu_verify_executor: Arc, + cpu_snark_executor: Arc, is_aggregator: bool, ) -> Result> { let mut key_manager = KeyManager::new(&keys_dir)?; @@ -253,6 +415,8 @@ impl ValidatorService { num_validators, key_manager: Some(Arc::new(key_manager)), cpu_normal_executor, + cpu_verify_executor, + cpu_snark_executor, is_aggregator: AtomicBool::new(is_aggregator), }) } @@ -307,240 +471,103 @@ impl ValidatorService { /// Returns `None` if this node has no aggregation duty or nothing to aggregate. pub fn maybe_aggregate( &self, - store: &Store, + snapshot: &AggregationSnapshot, slot: Slot, log_inv_rate: usize, + cancel: &std::sync::atomic::AtomicBool, ) -> Option<(Vec, HashSet)> { if !self.is_aggregator_for_slot(slot) { return None; } - // Get the head state to access validator public keys. - let head_state = store.states.get(&store.head)?; - - // Group fresh individual gossip signatures by data_root. - let mut gossip_groups: HashMap> = HashMap::new(); - for (sig_key, signature) in &store.gossip_signatures { - gossip_groups - .entry(sig_key.data_root) - .or_default() - .push((sig_key.validator_id, signature.clone())); - } - - // 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"); + if snapshot.jobs.is_empty() { return None; } - // Parallelise over data_roots: each XMSS prove_execution call is independent. - // gossip_groups/store/head_state are shared immutably across Rayon threads. - let all_data_roots_vec: Vec = all_data_roots.into_iter().collect(); + let mut aggregated_attestations: Vec = + Vec::with_capacity(snapshot.jobs.len()); + let mut consumed_data_roots: HashSet = HashSet::with_capacity(snapshot.jobs.len()); - let results: Vec<(SignedAggregatedAttestation, H256)> = all_data_roots_vec - .into_par_iter() - .filter_map(|data_root| { - let Some(attestation_data) = - store.attestation_data_by_root.get(&data_root).cloned() - else { - warn!( - data_root = %format!("0x{:x}", data_root), - "Could not find attestation data for aggregation group" - ); - return None; - }; - - // Only aggregate attestations for the current slot. - if attestation_data.slot != slot { - return None; - } - - // ── 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, + for job in &snapshot.jobs { + if cancel.load(Ordering::Relaxed) { + warn!( + slot = slot.0, + completed = aggregated_attestations.len(), + remaining = snapshot.jobs.len() - aggregated_attestations.len(), + "Aggregation cancelled after deadline; surfacing partial results" ); + break; + } - // ── 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, _)| { - !covered_by_children.contains(vid) - && head_state.validators.get(*vid).is_ok() - }) - .collect(); - entries.sort_by_key(|(vid, _)| *vid); - - 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 { - let validator = head_state.validators.get(vid).unwrap(); - fresh_validator_ids.push(vid); - fresh_public_keys.push(validator.attestation_pubkey.clone()); - fresh_signatures.push(sig); - } + let timer = METRICS.get().map(|m| { + m.lean_committee_signatures_aggregation_time_seconds + .start_timer() + }); - if children.is_empty() && fresh_validator_ids.is_empty() { - return None; - } + let children_arg: Vec<(&[PublicKey], &AggregatedSignatureProof)> = job + .children + .iter() + .map(|(pks, proof)| (pks.as_slice(), proof)) + .collect(); - // ── 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 { - return None; + let mut all_validator_ids: Vec = job.accepted_child_ids.clone(); + all_validator_ids.extend_from_slice(&job.raw_ids); + all_validator_ids.sort(); + all_validator_ids.dedup(); + let all_participants = AggregationBits::from_validator_indices(&all_validator_ids); + + let type1_start = std::time::Instant::now(); + let proof = match AggregatedSignatureProof::aggregate_with_children( + all_participants, + &children_arg, + job.raw_pubkeys.clone(), + job.raw_sigs.clone(), + job.data_root, + job.attestation_data.slot.0 as u32, + log_inv_rate, + ) { + Ok(p) => { + stop_and_record(timer); + info!( + slot = slot.0, + raws = job.raw_ids.len(), + children = job.children.len(), + duration_ms = type1_start.elapsed().as_millis() as u64, + "type1_prove", + ); + p } + Err(e) => { + stop_and_discard(timer); + warn!( + error = %e, + data_root = %format!("0x{:x}", job.data_root), + "Aggregated signature build failed" + ); + continue; + } + }; + + info!( + slot = slot.0, + validators = job.raw_ids.len() + job.accepted_child_ids.len(), + children = job.children.len(), + data_root = %format!("0x{:x}", job.data_root), + "Created aggregated attestation" + ); - // ── Phase 3: Aggregate ─────────────────────────────────────────────── - let timer = METRICS.get().map(|m| { - m.lean_committee_signatures_aggregation_time_seconds - .start_timer() - }); - - 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"); - return None; - } - } - } 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" - ); - return None; - } - } - }; - - info!( - slot = slot.0, - validators = fresh_validator_ids.len() + covered_by_children.len(), - children = children.len(), - data_root = %format!("0x{:x}", data_root), - "Created aggregated attestation" - ); - - Some(( - SignedAggregatedAttestation { - data: attestation_data, - proof, - }, - data_root, - )) - }) - .collect(); + aggregated_attestations.push(SignedAggregatedAttestation { + data: job.attestation_data.clone(), + proof, + }); - if results.is_empty() { - return None; + if !job.raw_ids.is_empty() { + consumed_data_roots.insert(job.data_root); + } } - // 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 aggregated_attestations = Vec::with_capacity(results.len()); - let mut consumed_data_roots = HashSet::with_capacity(results.len()); - for (attestation, root) in results { - aggregated_attestations.push(attestation); - consumed_data_roots.insert(root); + if aggregated_attestations.is_empty() { + return None; } Some((aggregated_attestations, consumed_data_roots)) @@ -552,36 +579,87 @@ impl ValidatorService { /// The validator task calls `BuildAttestationData` on the chain task first, /// receives the `AttestationData` via oneshot, then calls this method. /// This keeps XMSS signing (~170ms) entirely off the chain task's thread. - pub fn sign_block_with_data( + pub async fn sign_block_with_data( &self, block: Block, validator_index: u64, attestation_signatures: Vec, + validators: Validators, + log_inv_rate: usize, ) -> Result { let Some(key_manager) = self.key_manager.as_ref() else { bail!("unable to sign block - keymanager not configured"); }; - let proposer_signature = { + let exec = self.cpu_snark_executor.clone(); + let km = Arc::clone(key_manager); + + let job = exec.spawn(async move { + let block_root = block.hash_tree_root(); + let block_slot = block.slot.0 as u32; + + let proposer = validators + .get(validator_index) + .context(format!("proposer {validator_index} not found in state"))?; + let proposer_proposal_pubkey = proposer.proposal_pubkey.clone(); + let sign_timer = METRICS.get().map(|metrics| { metrics .lean_pq_sig_attestation_signing_time_seconds .start_timer() }); + let proposer_raw_signature = km + .sign_proposal(validator_index, block_slot, block_root) + .context("failed to sign block root") + .inspect_err(|_| stop_and_discard(sign_timer))?; + + let proposer_type1 = AggregatedSignature::aggregate( + [proposer_proposal_pubkey.clone()], + [proposer_raw_signature], + block_root, + block_slot, + log_inv_rate, + ) + .context("failed to wrap proposer signature into Type-1 aggregate")?; + + let mut pubkeys_owned: Vec> = + Vec::with_capacity(attestation_signatures.len() + 1); + for sig in &attestation_signatures { + let mut pks = Vec::new(); + for vid in sig.participants.to_validator_indices() { + let v = validators + .get(vid) + .context(format!("attester {vid} not found in state"))?; + pks.push(v.attestation_pubkey.clone()); + } + pubkeys_owned.push(pks); + } + pubkeys_owned.push(vec![proposer_proposal_pubkey]); - key_manager - .sign_proposal(validator_index, block.slot.0 as u32, block.hash_tree_root()) - .context("failed to sign block") - .inspect_err(|_| stop_and_discard(sign_timer))? - }; + let mut parts: Vec<(&AggregatedSignature, &[xmss::PublicKey])> = + Vec::with_capacity(attestation_signatures.len() + 1); + for (i, sig) in attestation_signatures.iter().enumerate() { + parts.push((&sig.proof_data, pubkeys_owned[i].as_slice())); + } + parts.push(( + &proposer_type1, + pubkeys_owned[attestation_signatures.len()].as_slice(), + )); + + let type2_start = std::time::Instant::now(); + let proof = MultiMessageAggregate::aggregate(&parts, log_inv_rate) + .context("failed to assemble Type-2 block proof")?; + info!( + block_slot, + parts = parts.len(), + duration_ms = type2_start.elapsed().as_millis() as u64, + "type2_prove", + ); - let signature = BlockSignatures { - attestation_signatures: AttestationSignatures::try_from_iter(attestation_signatures) - .context("invalid attestation signatures")?, - proposer_signature, - }; + Ok(SignedBlock { block, proof }) + }); - Ok(SignedBlock { block, signature }) + job.await.unwrap_or_else(|e| Err(anyhow!("executor: {e}"))) } /// Create and sign attestations for all validators given pre-fetched attestation data. diff --git a/lean_client/validator/tests/unit_tests.rs b/lean_client/validator/tests/unit_tests.rs index a19ccd58..4bef9827 100644 --- a/lean_client/validator/tests/unit_tests.rs +++ b/lean_client/validator/tests/unit_tests.rs @@ -12,7 +12,8 @@ fn test_proposer_selection() { validator_indices: vec![2], key_files: Default::default(), }; - let service = ValidatorService::new(config, 4, Arc::new(DedicatedExecutor::new_testing())); + let exec = Arc::new(DedicatedExecutor::new_testing()); + let service = ValidatorService::new(config, 4, exec.clone(), exec.clone(), exec); // Validator 2 should propose at slots 2, 6, 10, ... assert!(service.get_proposer_for_slot(Slot(2)).is_some()); diff --git a/lean_client/xmss/src/aggregated_signature.rs b/lean_client/xmss/src/aggregated_signature.rs index 9d522ae6..42b06824 100644 --- a/lean_client/xmss/src/aggregated_signature.rs +++ b/lean_client/xmss/src/aggregated_signature.rs @@ -4,27 +4,26 @@ use std::{str::FromStr, sync::Once}; use crate::{PublicKey, Signature}; use anyhow::{Context, Error, Result, anyhow, bail}; use ethereum_types::H256; +use leansig_wrapper::XmssPublicKey; use metrics::{METRICS, stop_and_discard}; use rec_aggregation::{ - AggregatedXMSS, init_aggregation_bytecode, xmss_aggregate, xmss_verify_aggregation, + SingleMessageAggregateSignature, aggregate_single_message_signatures, + init_aggregation_bytecode, verify_single_message_aggregate, }; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use ssz::{ByteList, ReadError, Size, SszHash, SszRead, SszSize, SszWrite, U1, WriteError}; -use typenum::U1048576; +use typenum::U524288; -/// Max size currently is 1MiB by spec. -type AggregatedSignatureSizeLimit = U1048576; +/// Spec: `SingleMessageAggregate.proof: ByteList512KiB` (524 288 bytes). +type AggregatedSignatureSizeLimit = U524288; /// Cryptographic proof that a set of validators signed a message. /// -/// Note: this doesn't follow spec a bit - in spec this would be a `proof_data` -/// field of AggregatedSignatureProof type. Implemented like this to have a -/// bit of nice encapsulation, so that xmss-related types don't leak -/// abstraction into containers crate. -/// -/// 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`. +/// Wire form: the lean-multisig `compress_without_pubkeys()` output. Pubkeys +/// are not baked into the bytes — verifiers must supply them externally, +/// matching `SingleMessageAggregate.proof: ByteList512KiB` in leanSpec +/// (`forks/lstar/containers/aggregation.py`) and zeam's `xmss_verify_type_1` +/// FFI which takes `pks + msg + slot + wire` as separate args. #[derive(Debug, Clone)] pub struct AggregatedSignature(ByteList); @@ -35,9 +34,6 @@ impl SszSize for AggregatedSignature { impl SszRead for AggregatedSignature { fn from_ssz_unchecked(context: &C, bytes: &[u8]) -> Result { let inner = ByteList::::from_ssz_unchecked(context, bytes)?; - AggregatedXMSS::decompress(inner.as_bytes()).ok_or(ReadError::Custom { - message: "invalid aggregated XMSS signature", - })?; Ok(Self(inner)) } } @@ -64,11 +60,7 @@ pub fn setup_aggregation() { impl AggregatedSignature { pub fn new(bytes: &[u8]) -> Result { let bytes = ByteList::try_from(bytes.to_vec()) - .context("signature too large - currently max 1MiB signatures allowed")?; - - AggregatedXMSS::decompress(bytes.as_bytes()) - .ok_or_else(|| anyhow!("invalid aggregated signature"))?; - + .context("signature too large - exceeds 512 KiB cap")?; Ok(Self(bytes)) } @@ -131,27 +123,15 @@ impl AggregatedSignature { .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 + let children_arg: Vec = children .iter() - .map(|(_, agg)| agg.as_lean()) + .map(|(pks, agg)| agg.as_lean(sorted_dedup_lean_pubkeys(pks))) .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( + let agg = aggregate_single_message_signatures( &children_arg, raw_xmss, - message.as_fixed_bytes(), + *message.as_fixed_bytes(), slot, log_inv_rate, )?; @@ -162,9 +142,9 @@ impl AggregatedSignature { .inc_by(sig_count as u64) }); - let bytes = agg.compress(); + let bytes = agg.compress_without_pubkeys(); Ok(Self(ByteList::try_from(bytes).context( - "aggregated proof too large - exceeds 1MiB limit", + "aggregated proof too large - exceeds 512 KiB cap", )?)) } @@ -182,32 +162,48 @@ impl AggregatedSignature { .start_timer() }); - let pub_keys = public_keys + let mut expected_pubkeys = public_keys .into_iter() .map(|k| k.as_lean()) .collect::>(); + expected_pubkeys.sort(); + expected_pubkeys.dedup(); + + let agg = self.as_lean(expected_pubkeys)?; - let agg = self.as_lean()?; + if agg.info.without_pubkeys.message != *message.as_fixed_bytes() { + bail!("aggregated signature bound to a different message than expected"); + } + if agg.info.without_pubkeys.slot != slot { + bail!("aggregated signature bound to a different slot than expected"); + } - xmss_verify_aggregation(pub_keys, &agg, message.as_fixed_bytes(), slot) + let result = verify_single_message_aggregate(&agg) .map(|_| ()) - .map_err(|err| anyhow!("{err:?}")) - .inspect(|_| { + .map_err(|err| anyhow!("{err:?}")); + + match &result { + Ok(()) => { METRICS .get() .map(|metrics| metrics.lean_pq_sig_aggregated_signatures_valid_total.inc()); - }) - .inspect_err(|_| { + } + Err(_) => { METRICS.get().map(|metrics| { metrics .lean_pq_sig_aggregated_signatures_invalid_total .inc() }); - }) + } + } + result } - fn as_lean(&self) -> Result { - AggregatedXMSS::decompress(self.0.as_bytes()) + pub(crate) fn as_lean( + &self, + pubkeys: Vec, + ) -> Result { + SingleMessageAggregateSignature::decompress_without_pubkeys(self.0.as_bytes(), pubkeys) .ok_or_else(|| anyhow!("invalid aggregated XMSS signature")) } @@ -217,6 +213,13 @@ impl AggregatedSignature { } } +fn sorted_dedup_lean_pubkeys(pks: &[PublicKey]) -> Vec { + let mut lean: Vec = pks.iter().map(|pk| pk.as_lean()).collect(); + lean.sort(); + lean.dedup(); + lean +} + impl Display for AggregatedSignature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "0x{}", hex::encode(self.0.as_bytes())) diff --git a/lean_client/xmss/src/lib.rs b/lean_client/xmss/src/lib.rs index e27c8231..08441db7 100644 --- a/lean_client/xmss/src/lib.rs +++ b/lean_client/xmss/src/lib.rs @@ -1,9 +1,11 @@ mod aggregated_signature; +mod multi_message; mod public_key; mod secret_key; mod signature; pub use aggregated_signature::{AggregatedSignature, setup_aggregation}; +pub use multi_message::MultiMessageAggregate; pub use public_key::PublicKey; pub use secret_key::SecretKey; pub use signature::Signature; diff --git a/lean_client/xmss/src/multi_message.rs b/lean_client/xmss/src/multi_message.rs new file mode 100644 index 00000000..fa4f5a59 --- /dev/null +++ b/lean_client/xmss/src/multi_message.rs @@ -0,0 +1,171 @@ +use core::fmt::{self, Display}; +use std::str::FromStr; + +use anyhow::{Context, Error, Result, anyhow, bail}; +use ethereum_types::H256; +use leansig_wrapper::XmssPublicKey; +use rec_aggregation::{ + MultiMessageAggregateSignature, merge_single_message_aggregates, + split_multi_message_aggregate_by_message, verify_multi_message_aggregate, +}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; +use ssz::{ByteList, Ssz}; +use typenum::U524288; + +use crate::{AggregatedSignature, PublicKey, aggregated_signature::setup_aggregation}; + +type MultiMessageAggregateSizeLimit = U524288; + +#[derive(Clone, Debug, Default, Ssz)] +pub struct MultiMessageAggregate { + pub proof: ByteList, +} + +impl MultiMessageAggregate { + pub fn new(bytes: &[u8]) -> Result { + let proof = ByteList::try_from(bytes.to_vec()) + .context("multi-message aggregate too large - max 512 KiB")?; + Ok(Self { proof }) + } + + pub fn aggregate( + parts: &[(&AggregatedSignature, &[PublicKey])], + log_inv_rate: usize, + ) -> Result { + setup_aggregation(); + + if parts.is_empty() { + bail!("multi-message aggregate requires at least one Type-1 component"); + } + + let parts_lean = parts + .iter() + .map(|(sig, pks)| { + let mut lean: Vec = pks.iter().map(|pk| pk.as_lean()).collect(); + lean.sort(); + lean.dedup(); + sig.as_lean(lean) + }) + .collect::>>()?; + + let merged = merge_single_message_aggregates(parts_lean, log_inv_rate)?; + let bytes = merged.compress_without_pubkeys(); + Self::new(&bytes) + } + + pub fn verify( + &self, + pubkeys_per_message: &[&[PublicKey]], + messages: &[(H256, u32)], + ) -> Result<()> { + setup_aggregation(); + + if pubkeys_per_message.len() != messages.len() { + bail!( + "binding length mismatch: {} pubkey sets vs {} messages", + pubkeys_per_message.len(), + messages.len() + ); + } + + let pubkeys_per_info = sorted_dedup_lean_pubkeys(pubkeys_per_message); + + let sig = MultiMessageAggregateSignature::decompress_without_pubkeys( + self.proof.as_bytes(), + pubkeys_per_info, + ) + .ok_or_else(|| anyhow!("invalid multi-message aggregate bytes"))?; + + if sig.info.len() != messages.len() { + bail!( + "component count mismatch: proof has {}, expected {}", + sig.info.len(), + messages.len() + ); + } + for (i, (expected_message, expected_slot)) in messages.iter().enumerate() { + if sig.info[i].without_pubkeys.message != *expected_message.as_fixed_bytes() { + bail!("component {i} bound to a different message than expected"); + } + if sig.info[i].without_pubkeys.slot != *expected_slot { + bail!("component {i} bound to a different slot than expected"); + } + } + + verify_multi_message_aggregate(&sig) + .map(|_| ()) + .map_err(|err| anyhow!("{err:?}")) + } + + pub fn split_by_message( + &self, + pubkeys_per_message: &[&[PublicKey]], + message: H256, + log_inv_rate: usize, + ) -> Result { + setup_aggregation(); + + let pubkeys_per_info = sorted_dedup_lean_pubkeys(pubkeys_per_message); + let sig = MultiMessageAggregateSignature::decompress_without_pubkeys( + self.proof.as_bytes(), + pubkeys_per_info, + ) + .ok_or_else(|| anyhow!("invalid multi-message aggregate bytes"))?; + + let recovered = + split_multi_message_aggregate_by_message(sig, *message.as_fixed_bytes(), log_inv_rate)?; + let bytes = recovered.compress_without_pubkeys(); + AggregatedSignature::new(&bytes) + } +} + +fn sorted_dedup_lean_pubkeys(per_component: &[&[PublicKey]]) -> Vec> { + per_component + .iter() + .map(|pks| { + let mut lean: Vec = pks.iter().map(|pk| pk.as_lean()).collect(); + lean.sort(); + lean.dedup(); + lean + }) + .collect() +} + +impl Display for MultiMessageAggregate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{}", hex::encode(self.proof.as_bytes())) + } +} + +impl FromStr for MultiMessageAggregate { + type Err = Error; + + fn from_str(s: &str) -> Result { + let data = s.strip_prefix("0x").unwrap_or(s); + let bytes = hex::decode(data)?; + Self::new(&bytes) + } +} + +impl Serialize for MultiMessageAggregate { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl<'de> Deserialize<'de> for MultiMessageAggregate { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct DataWrapper { + data: String, + } + let value = DataWrapper::deserialize(deserializer)?; + value.data.parse().map_err(de::Error::custom) + } +} diff --git a/lean_client/xmss/src/secret_key.rs b/lean_client/xmss/src/secret_key.rs index ba93b51c..b5cae65d 100644 --- a/lean_client/xmss/src/secret_key.rs +++ b/lean_client/xmss/src/secret_key.rs @@ -8,19 +8,20 @@ use leansig::signature::generalized_xmss::instantiations_aborting::lifetime_2_to }; use rand::CryptoRng; use ssz::H256; -use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{PublicKey, Signature}; -#[derive(Clone, Zeroize, ZeroizeOnDrop, Debug)] +// TODO(zeroize): upstream `XmssSecretKey` does not derive `Zeroize`, so we cannot +// derive `ZeroizeOnDrop` on the wrapper. Acceptable for devnet bring-up; before +// mainnet, either upstream a `Zeroize` derive on `GeneralizedXMSSSecretKey` or +// implement `Drop` here manually (zeroize the inner buffers via accessor). +#[derive(Debug)] #[debug("[REDACTED]")] -pub struct SecretKey(Vec); +pub struct SecretKey(XmssSecretKey); impl SecretKey { pub fn sign(&self, message: H256, epoch: u32) -> Result { - 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()) + let sig = XmssScheme::sign(&self.0, epoch, message.as_fixed_bytes()) .map_err(|_| anyhow!("failed to sign message"))?; Ok(Signature::from_lean(sig)) } @@ -32,7 +33,7 @@ impl SecretKey { ) -> (PublicKey, SecretKey) { let (pk, sk) = XmssScheme::key_gen(rng, activation_epoch as usize, num_active_epochs as usize); - (PublicKey::from_lean(pk), SecretKey(sk.to_bytes())) + (PublicKey::from_lean(pk), SecretKey(sk)) } } @@ -40,7 +41,8 @@ impl TryFrom<&[u8]> for SecretKey { type Error = Error; fn try_from(value: &[u8]) -> Result { - XmssSecretKey::from_bytes(value).map_err(|_| anyhow!("value is not valid secret key"))?; - Ok(Self(value.to_vec())) + let sk = XmssSecretKey::from_bytes(value) + .map_err(|_| anyhow!("value is not valid secret key"))?; + Ok(Self(sk)) } } From a1bc30935fd04d631642fee59e553d673e753c78 Mon Sep 17 00:00:00 2001 From: bomanaps Date: Thu, 11 Jun 2026 04:56:46 +0100 Subject: [PATCH 2/7] pin nightl --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f97bf6d1..d44ec443 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = '1.92' +channel = 'nightly' profile = 'minimal' components = ['clippy', 'rustfmt'] From 10b329cd7fe95375f6392c733aed3306e6bc91b3 Mon Sep 17 00:00:00 2001 From: bomanaps Date: Thu, 11 Jun 2026 22:18:17 +0100 Subject: [PATCH 3/7] add aggregation executor, dedup, skipped metric --- lean_client/metrics/src/metrics.rs | 47 +++++- lean_client/networking/src/network/service.rs | 140 ++++++++++++------ lean_client/src/aggregation.rs | 6 +- lean_client/src/main.rs | 38 +++-- lean_client/validator/src/lib.rs | 8 +- 5 files changed, 169 insertions(+), 70 deletions(-) diff --git a/lean_client/metrics/src/metrics.rs b/lean_client/metrics/src/metrics.rs index 39cf7f92..ac9f334a 100644 --- a/lean_client/metrics/src/metrics.rs +++ b/lean_client/metrics/src/metrics.rs @@ -65,6 +65,7 @@ pub struct Metrics { pub lean_fork_choice_block_processing_time_seconds: Histogram, /// Total number of valid attestations + pub lean_aggregator_skipped_total: IntCounterVec, pub lean_attestations_valid_total: IntCounterVec, /// Total number of invalid attestations @@ -189,7 +190,9 @@ pub struct Metrics { pub grandine_validator_chain_message_channel_depth: IntGauge, pub grandine_verify_result_channel_depth: IntGauge, pub grandine_cpu_normal_executor_tasks_in_flight: IntGauge, - pub grandine_cpu_low_executor_tasks_in_flight: IntGauge, + pub grandine_cpu_snark_executor_tasks_in_flight: IntGauge, + pub grandine_cpu_verify_executor_tasks_in_flight: IntGauge, + pub grandine_cpu_aggregation_executor_tasks_in_flight: IntGauge, pub grandine_reaggregate_candidates_selected_total: IntCounterVec, pub grandine_reaggregate_split_outcomes_total: IntCounterVec, @@ -339,6 +342,13 @@ impl Metrics { "Time taken to process block", vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0, 1.25, 1.5, 2.0, 4.0] ))?, + lean_aggregator_skipped_total: IntCounterVec::new( + opts!( + "lean_aggregator_skipped_total", + "Aggregation submissions skipped, by reason", + ), + &["reason"], + )?, lean_attestations_valid_total: IntCounterVec::new( opts!( "lean_attestations_valid_total", @@ -562,9 +572,17 @@ impl Metrics { "grandine_cpu_normal_executor_tasks_in_flight", "Active tasks on cpu_normal DedicatedExecutor (XMSS block verify + attestation signing combined)", )?, - grandine_cpu_low_executor_tasks_in_flight: IntGauge::new( - "grandine_cpu_low_executor_tasks_in_flight", - "Active tasks on cpu_low DedicatedExecutor (reaggregate split_type_2 work)", + grandine_cpu_snark_executor_tasks_in_flight: IntGauge::new( + "grandine_cpu_snark_executor_tasks_in_flight", + "Active tasks on cpu_snark DedicatedExecutor (propose-build + propose-sign)", + )?, + grandine_cpu_verify_executor_tasks_in_flight: IntGauge::new( + "grandine_cpu_verify_executor_tasks_in_flight", + "Active tasks on cpu_verify DedicatedExecutor (reaggregate split + cascade-verify + gossip-block-verify + gossip-agg-att-verify)", + )?, + grandine_cpu_aggregation_executor_tasks_in_flight: IntGauge::new( + "grandine_cpu_aggregation_executor_tasks_in_flight", + "Active tasks on cpu_aggregation DedicatedExecutor (aggregation worker, isolated from propose)", )?, grandine_reaggregate_candidates_selected_total: IntCounterVec::new( opts!( @@ -795,6 +813,18 @@ impl Metrics { default_registry.register(Box::new( self.lean_fork_choice_block_processing_time_seconds.clone(), ))?; + default_registry.register(Box::new(self.lean_aggregator_skipped_total.clone()))?; + for &reason in &[ + "not_aggregator", + "not_synced", + "missing_state", + "spawn_failed", + "other", + ] { + self.lean_aggregator_skipped_total + .with_label_values(&[reason]) + .inc_by(0); + } default_registry.register(Box::new(self.lean_attestations_valid_total.clone()))?; default_registry.register(Box::new(self.lean_attestations_invalid_total.clone()))?; default_registry.register(Box::new( @@ -874,7 +904,14 @@ impl Metrics { self.grandine_cpu_normal_executor_tasks_in_flight.clone(), ))?; default_registry.register(Box::new( - self.grandine_cpu_low_executor_tasks_in_flight.clone(), + self.grandine_cpu_snark_executor_tasks_in_flight.clone(), + ))?; + default_registry.register(Box::new( + self.grandine_cpu_verify_executor_tasks_in_flight.clone(), + ))?; + default_registry.register(Box::new( + self.grandine_cpu_aggregation_executor_tasks_in_flight + .clone(), ))?; default_registry.register(Box::new( self.grandine_reaggregate_candidates_selected_total.clone(), diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 75d66d58..a2e771e0 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -197,6 +197,12 @@ where blocks_by_range_provider: CanonicalBlocksProvider, /// Pending BlocksByRoot requests for retry on empty response pending_blocks_by_root: HashMap, + /// In-flight BlocksByRange requests, deduplicated by (peer, start_slot, count). + /// `inflight_range_keys` is the O(1) dedup check; `inflight_blocks_by_range` + /// maps the libp2p request id to its key so we can drop both entries when + /// a Response, OutboundFailure, or peer disconnect arrives. + inflight_blocks_by_range: HashMap, + inflight_range_keys: HashSet<(PeerId, u64, u64)>, /// Depth tracking per block root for limiting backward chain walking pending_block_depths: HashMap, /// Roots currently in-flight to deduplicate network-layer pipelining vs chain-side requests @@ -331,6 +337,8 @@ where status_provider, blocks_by_range_provider, pending_blocks_by_root: HashMap::new(), + inflight_blocks_by_range: HashMap::new(), + inflight_range_keys: HashSet::new(), pending_block_depths: HashMap::new(), in_flight_roots: HashSet::new(), network_finalized_slot, @@ -511,6 +519,9 @@ where self.peer_finalized_slots.remove(&peer_id); self.peer_head_slots.remove(&peer_id); + self.inflight_blocks_by_range + .retain(|_, (p, _, _)| *p != peer_id); + self.inflight_range_keys.retain(|(p, _, _)| *p != peer_id); self.recompute_network_finalized_slot(); info!(peer = %peer_id, ?cause, "Disconnected from peer (total: {})", connected); @@ -1497,6 +1508,18 @@ where ); return; } + + let key = (peer_id, start_slot, count); + if self.inflight_range_keys.contains(&key) { + trace!( + peer = %peer_id, + start_slot, + count, + "Skipping duplicate BlocksByRange request" + ); + return; + } + info!( peer = %peer_id, start_slot, @@ -1504,11 +1527,13 @@ where "Sending BlocksByRange request" ); let request = LeanRequest::BlocksByRange { start_slot, count }; - let _request_id = self + let request_id = self .swarm .behaviour_mut() .blocks_by_range_req_resp .send_request(&peer_id, request); + self.inflight_range_keys.insert(key); + self.inflight_blocks_by_range.insert(request_id, key); } pub fn send_blocks_by_root_request_with_depth( @@ -1627,61 +1652,78 @@ where warn!(peer = %peer, ?e, "Failed to send BlocksByRange response"); } } - Message::Response { response, .. } => match response { - LeanResponse::BlocksByRange(blocks) => { - info!( - peer = %peer, - num_blocks = blocks.len(), - "Received BlocksByRange response" - ); + Message::Response { + request_id, + response, + .. + } => { + if let Some(key) = self.inflight_blocks_by_range.remove(&request_id) { + self.inflight_range_keys.remove(&key); + } + match response { + LeanResponse::BlocksByRange(blocks) => { + info!( + peer = %peer, + num_blocks = blocks.len(), + "Received BlocksByRange response" + ); - { - let mut provider = self.signed_block_provider.write(); - for block in &blocks { - let root = block.block.hash_tree_root(); - provider.insert(root, block.clone()); - } - 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.block.slot.0)) - .collect(); - slots.sort_by_key(|(_, slot)| *slot); - for (root, _) in slots.into_iter().take(to_remove) { - provider.remove(&root); + { + let mut provider = self.signed_block_provider.write(); + for block in &blocks { + let root = block.block.hash_tree_root(); + provider.insert(root, block.clone()); } + 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.block.slot.0)) + .collect(); + slots.sort_by_key(|(_, slot)| *slot); + for (root, _) in slots.into_iter().take(to_remove) { + provider.remove(&root); + } + } + METRICS.get().map(|m| { + m.grandine_signed_block_provider_size + .set(provider.len() as i64) + }); } - METRICS.get().map(|m| { - m.grandine_signed_block_provider_size - .set(provider.len() as i64) - }); - } - let chain_sink = self.chain_message_sink.clone(); - tokio::spawn(async move { - for block in blocks { - let slot = block.block.slot.0; - if let Err(e) = chain_sink - .send(ChainMessage::ProcessBlock { - signed_block: block, - is_trusted: false, - should_gossip: false, - cached_post_state: None, - }) - .await - { - warn!(slot, ?e, "Failed to forward range block to chain"); + let chain_sink = self.chain_message_sink.clone(); + tokio::spawn(async move { + for block in blocks { + let slot = block.block.slot.0; + if let Err(e) = chain_sink + .send(ChainMessage::ProcessBlock { + signed_block: block, + is_trusted: false, + should_gossip: false, + cached_post_state: None, + }) + .await + { + warn!(slot, ?e, "Failed to forward range block to chain"); + } } - } - }); - } - _ => { - warn!(peer = %peer, "Unexpected response type on BlocksByRange protocol"); + }); + } + _ => { + warn!(peer = %peer, "Unexpected response type on BlocksByRange protocol"); + } } - }, + } }, - Event::OutboundFailure { peer, error, .. } => { + Event::OutboundFailure { + peer, + request_id, + error, + .. + } => { + if let Some(key) = self.inflight_blocks_by_range.remove(&request_id) { + self.inflight_range_keys.remove(&key); + } warn!(peer = %peer, ?error, "BlocksByRange outbound request failed"); } Event::InboundFailure { peer, error, .. } => { diff --git a/lean_client/src/aggregation.rs b/lean_client/src/aggregation.rs index 67123843..cc4c4847 100644 --- a/lean_client/src/aggregation.rs +++ b/lean_client/src/aggregation.rs @@ -44,7 +44,7 @@ impl AggregationService { vs: Arc, store: Arc>, log_rate: usize, - cpu_snark_executor: Arc, + cpu_aggregation_executor: Arc, ) -> Self { let (agg_tx, mut agg_rx) = watch::channel::>(None); let (res_tx, res_rx) = mpsc::channel::<( @@ -106,8 +106,8 @@ impl AggregationService { }); let cancel_for_worker = cancel.clone(); - let snark_exec = cpu_snark_executor.clone(); - let job = snark_exec.spawn(async move { + let agg_exec = cpu_aggregation_executor.clone(); + let job = agg_exec.spawn(async move { vs.maybe_aggregate(&snapshot, Slot(slot), log_rate, &cancel_for_worker) }); let result = job.await.unwrap_or(None); diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index 71bfb9ae..4ea3e133 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -505,6 +505,8 @@ async fn main() -> Result<()> { )); let cpu_snark_executor = Arc::new(DedicatedExecutor::new("lean-snark", 1, None, None)); let cpu_verify_executor = Arc::new(DedicatedExecutor::new("lean-verify", 1, None, None)); + let cpu_aggregation_executor = + Arc::new(DedicatedExecutor::new("lean-aggregation", 1, None, None)); // Verified blocks travel back from the executor to the chain task on this channel. // Tuple: (block_root, signed_block, should_gossip, verify outcome). @@ -1131,7 +1133,7 @@ async fn main() -> Result<()> { vs, store.clone(), chain_log_inv_rate, - cpu_snark_executor.clone(), + cpu_aggregation_executor.clone(), ) }); // Whether this node has any validator service at all. Used for sync-state @@ -1201,13 +1203,16 @@ async fn main() -> Result<()> { .is_some(); on_tick(&mut *store.write(), now_millis, has_proposal); - // Sample the cpu_normal executor's queue depth once per tick. - // Counts both XMSS block verify and attestation signing tasks. + // Sample executor queue depths once per tick. METRICS.get().map(|m| { m.grandine_cpu_normal_executor_tasks_in_flight .set(cpu_normal_executor.tasks() as i64); - m.grandine_cpu_low_executor_tasks_in_flight - .set(cpu_snark_executor.tasks() as i64) + m.grandine_cpu_snark_executor_tasks_in_flight + .set(cpu_snark_executor.tasks() as i64); + m.grandine_cpu_verify_executor_tasks_in_flight + .set(cpu_verify_executor.tasks() as i64); + m.grandine_cpu_aggregation_executor_tasks_in_flight + .set(cpu_aggregation_executor.tasks() as i64) }); let (current_slot, current_interval) = { @@ -1255,17 +1260,26 @@ async fn main() -> Result<()> { // AggregationService::is_aggregator_for_slot routes through the // ValidatorService AtomicBool flag, so admin-API runtime toggles // are still honored. - if let Some(ref agg) = aggregator { - if agg.is_aggregator_for_slot(Slot(current_slot)) - && current_interval >= 2 - && current_slot > last_agg_slot - { - last_agg_slot = current_slot; + if current_interval >= 2 && current_slot > last_agg_slot { + last_agg_slot = current_slot; + let is_agg = aggregator + .as_ref() + .map(|a| a.is_aggregator_for_slot(Slot(current_slot))) + .unwrap_or(false); + if is_agg { METRICS.get().map(|m| { m.lean_aggregation_snapshots_triggered_total.inc() }); - agg.trigger(current_slot); + if let Some(ref agg) = aggregator { + agg.trigger(current_slot); + } info!(slot = current_slot, "Aggregation phase - triggered"); + } else { + METRICS.get().map(|m| { + m.lean_aggregator_skipped_total + .with_label_values(&["not_aggregator"]) + .inc() + }); } } diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 04ad77c5..171edd23 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -490,12 +490,18 @@ impl ValidatorService { for job in &snapshot.jobs { if cancel.load(Ordering::Relaxed) { + let remaining = snapshot.jobs.len() - aggregated_attestations.len(); warn!( slot = slot.0, completed = aggregated_attestations.len(), - remaining = snapshot.jobs.len() - aggregated_attestations.len(), + remaining, "Aggregation cancelled after deadline; surfacing partial results" ); + METRICS.get().map(|m| { + m.lean_aggregator_skipped_total + .with_label_values(&["other"]) + .inc_by(remaining as u64) + }); break; } From caa536b3b9307159d240db017b5b8f3388454dc2 Mon Sep 17 00:00:00 2001 From: bomanaps Date: Wed, 17 Jun 2026 15:12:00 +0100 Subject: [PATCH 4/7] range_sync, ancestor check, codec fix --- lean_client/Cargo.lock | 42 ++--- lean_client/Cargo.toml | 2 +- lean_client/fork_choice/src/handlers.rs | 48 ++++- lean_client/networking/src/network/mod.rs | 1 + .../networking/src/network/range_sync.rs | 69 +++++++ lean_client/networking/src/network/service.rs | 176 ++++++++++++++---- lean_client/networking/src/req_resp.rs | 6 + rust-toolchain.toml | 2 +- 8 files changed, 280 insertions(+), 66 deletions(-) create mode 100644 lean_client/networking/src/network/range_sync.rs diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index aa5cab34..660c5977 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -3087,7 +3087,7 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libp2p" version = "0.57.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "bytes", "either", @@ -3119,7 +3119,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.7.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3129,7 +3129,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.7.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3139,7 +3139,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.44.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "either", "fnv", @@ -3163,7 +3163,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.45.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "hickory-resolver", @@ -3177,7 +3177,7 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.50.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "async-channel", "asynchronous-codec", @@ -3206,7 +3206,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.48.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "asynchronous-codec", "either", @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.49.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "hickory-proto", @@ -3264,7 +3264,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.18.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "libp2p-core", @@ -3280,7 +3280,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.47.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "asynchronous-codec", "bytes", @@ -3302,7 +3302,7 @@ dependencies = [ [[package]] name = "libp2p-quic" version = "0.14.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "futures-timer", @@ -3323,7 +3323,7 @@ dependencies = [ [[package]] name = "libp2p-request-response" version = "0.30.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "futures-bounded", @@ -3338,7 +3338,7 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.48.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "either", "fnv", @@ -3359,7 +3359,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.36.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "heck", "quote 1.0.45", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.45.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "futures-timer", @@ -3384,7 +3384,7 @@ dependencies = [ [[package]] name = "libp2p-tls" version = "0.7.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "futures-rustls", @@ -3402,7 +3402,7 @@ dependencies = [ [[package]] name = "libp2p-upnp" version = "0.7.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "futures-timer", @@ -3416,7 +3416,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.48.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "either", "futures", @@ -3745,7 +3745,7 @@ dependencies = [ [[package]] name = "multistream-select" version = "0.14.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "bytes", "futures", @@ -4630,7 +4630,7 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" version = "0.4.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "asynchronous-codec", "bytes", @@ -5253,7 +5253,7 @@ dependencies = [ [[package]] name = "rw-stream-sink" version = "0.5.0" -source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01cfb6a02326c90628c4bba521c#2f14d0ec9665a01cfb6a02326c90628c4bba521c" +source = "git+https://github.com/libp2p/rust-libp2p.git?rev=91e8931e275bcd1c72791d18b09fea8b77209baf#91e8931e275bcd1c72791d18b09fea8b77209baf" dependencies = [ "futures", "pin-project", diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index 85d2c5bd..cd54dbf7 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -254,7 +254,7 @@ k256 = "0.13" rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df4e30fdddbbf8ae26a333116c68cec7026" } leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" } leansig_wrapper = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df4e30fdddbbf8ae26a333116c68cec7026" } -libp2p = { git = "https://github.com/lambdaclass/rust-libp2p.git", rev = "2f14d0ec9665a01cfb6a02326c90628c4bba521c", default-features = false, features = [ +libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "91e8931e275bcd1c72791d18b09fea8b77209baf", default-features = false, features = [ 'dns', 'gossipsub', 'identify', diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index ebfd17b7..c97106bd 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use anyhow::{Context, Result, anyhow, bail, ensure}; use containers::{ - AttestationData, SignatureKey, SignedAggregatedAttestation, SignedAttestation, SignedBlock, - State, + AttestationData, Checkpoint, SignatureKey, SignedAggregatedAttestation, SignedAttestation, + SignedBlock, State, }; use metrics::METRICS; use parking_lot::RwLock; @@ -40,6 +40,7 @@ pub fn on_tick(store: &mut Store, time_millis: u64, has_proposal: bool) { /// 2. A vote cannot span backwards in time (source > target). /// 3. A vote cannot be for a future slot. /// 4. Checkpoint slots must match block slots. +/// 5. Source, target, and head must lie on one parent chain. fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<()> { // Topology: history is linear and monotonic — source <= target <= head. ensure!( @@ -81,6 +82,23 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<() head_block.slot.0 ); + ensure!( + checkpoint_is_ancestor(store, &data.source, &data.target), + "Source checkpoint {} (slot {}) is not an ancestor of target {} (slot {})", + data.source.root, + data.source.slot.0, + data.target.root, + data.target.slot.0, + ); + ensure!( + checkpoint_is_ancestor(store, &data.target, &data.head), + "Target checkpoint {} (slot {}) is not an ancestor of head {} (slot {})", + data.target.root, + data.target.slot.0, + data.head.root, + data.head.slot.0, + ); + // Honest validators emit votes only after their slot has begun. Allow exactly // one interval (~800 ms) of clock skew between peers; a whole-slot margin would // let an adversary pre-publish next-slot aggregates ahead of any honest @@ -99,6 +117,32 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<() Ok(()) } +/// Returns whether `ancestor` lies on `descendant`'s parent chain. +/// +/// An unknown root in the walk yields false (lineage cannot be proven). +fn checkpoint_is_ancestor( + store: &Store, + ancestor: &Checkpoint, + descendant: &Checkpoint, +) -> bool { + if ancestor.slot > descendant.slot { + return false; + } + + let mut current_root = descendant.root; + while let Some(current_block) = store.blocks.get(¤t_root) { + if current_block.slot == ancestor.slot { + return current_root == ancestor.root; + } + if current_block.slot < ancestor.slot { + return false; + } + current_root = current_block.parent_root; + } + + false +} + /// Returns the first block root (source, target, or head) referenced by `attestation_data` /// that is not yet present in the store, or `None` if all are known. fn find_unknown_attestation_block( diff --git a/lean_client/networking/src/network/mod.rs b/lean_client/networking/src/network/mod.rs index 1900ed87..6b506bb6 100644 --- a/lean_client/networking/src/network/mod.rs +++ b/lean_client/networking/src/network/mod.rs @@ -1,4 +1,5 @@ mod behaviour; +mod range_sync; mod service; pub use behaviour::{LeanNetworkBehaviour, LeanNetworkBehaviourEvent}; diff --git a/lean_client/networking/src/network/range_sync.rs b/lean_client/networking/src/network/range_sync.rs new file mode 100644 index 00000000..8a42cdcb --- /dev/null +++ b/lean_client/networking/src/network/range_sync.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; +use std::ops::Range; + +use libp2p_identity::PeerId; + +use crate::req_resp; + +/// Cap on slots covered by a single long-range sync session. +pub const MAX_SYNC_RANGE: u64 = req_resp::MAX_REQUEST_BLOCKS as u64 * 64; + +/// Long-range sync session triggered when a peer's Status reveals it is ahead. +pub(crate) struct RangeSyncState { + pub current_range: Range, + pub peer_set: HashMap, + pub in_flight: bool, +} + +impl RangeSyncState { + pub fn new(current_range: Range, peer: PeerId, peer_head: u64) -> Self { + Self { + current_range, + peer_set: HashMap::from([(peer, peer_head)]), + in_flight: false, + } + } + + pub fn merge_peer(&mut self, peer: PeerId, peer_head: u64, end_exclusive: u64) { + self.peer_set.insert(peer, peer_head); + self.current_range.end = self.current_range.end.max(end_exclusive); + self.drop_stale_peers(); + } + + pub fn next_batch(&self) -> Option<(PeerId, Range)> { + if self.in_flight || self.current_range.is_empty() { + return None; + } + let (&peer, &peer_head) = self + .peer_set + .iter() + .filter(|(_, head)| **head >= self.current_range.start) + .max_by_key(|(_, head)| **head)?; + let peer_end = peer_head.saturating_add(1); + let batch_end = self + .current_range + .start + .saturating_add(req_resp::MAX_REQUEST_BLOCKS as u64) + .min(self.current_range.end) + .min(peer_end); + (batch_end > self.current_range.start) + .then_some((peer, self.current_range.start..batch_end)) + } + + pub fn complete_batch(&mut self, end_slot: u64) { + self.in_flight = false; + self.current_range.start = self.current_range.start.max(end_slot.saturating_add(1)); + self.drop_stale_peers(); + } + + pub fn fail_peer(&mut self, peer: &PeerId) { + self.in_flight = false; + self.peer_set.remove(peer); + self.drop_stale_peers(); + } + + fn drop_stale_peers(&mut self) { + let start_slot = self.current_range.start; + self.peer_set.retain(|_, head| *head >= start_slot); + } +} diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index a2e771e0..d583dc6d 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -42,6 +42,7 @@ use crate::{ enr_ext::EnrExt, gossipsub::{self, config::GossipsubConfig, message::GossipsubMessage, topic::GossipsubKind}, network::behaviour::{LeanNetworkBehaviour, LeanNetworkBehaviourEvent}, + network::range_sync::{MAX_SYNC_RANGE, RangeSyncState}, req_resp::{self, LeanRequest, ReqRespMessage}, types::{ CanonicalBlocksProvider, ChainMessage, ChainMessageSink, ConnectionState, @@ -212,6 +213,8 @@ where /// Peer head slots reported via Status; used by the caller to decide /// when a backfill should switch from per-root BlocksByRoot to batched BlocksByRange. peer_head_slots: HashMap, + /// Active long-range sync session; `None` when up-to-date with peers. + range_sync_state: Option, status_notify: Arc, } @@ -344,6 +347,7 @@ where network_finalized_slot, peer_finalized_slots: HashMap::new(), peer_head_slots: HashMap::new(), + range_sync_state: None, status_notify, }; @@ -519,6 +523,12 @@ where self.peer_finalized_slots.remove(&peer_id); self.peer_head_slots.remove(&peer_id); + if let Some(state) = &mut self.range_sync_state { + state.fail_peer(&peer_id); + if state.peer_set.is_empty() { + self.range_sync_state = None; + } + } self.inflight_blocks_by_range .retain(|_, (p, _, _)| *p != peer_id); self.inflight_range_keys.retain(|(p, _, _)| *p != peer_id); @@ -1272,21 +1282,62 @@ where self.peer_head_slots.insert(peer, peer_head_slot); self.recompute_network_finalized_slot(); - if (peer_finalized_slot > our_finalized_slot || peer_head_slot > our_head_slot) - && !peer_head_root.is_zero() - { + if peer_head_slot > our_head_slot { + let gap = peer_head_slot - our_head_slot; + let start_slot = our_head_slot.saturating_add(1); + let end_exclusive = start_slot.saturating_add(gap.min(MAX_SYNC_RANGE)); + + match &mut self.range_sync_state { + Some(state) => state.merge_peer(peer, peer_head_slot, end_exclusive), + None => { + self.range_sync_state = Some(RangeSyncState::new( + start_slot..end_exclusive, + peer, + peer_head_slot, + )); + } + } + + info!( + peer = %peer, + start_slot, + gap, + "Long-range sync: triggering BlocksByRange" + ); + self.drain_range_sync_batches(); + return; + } + + if peer_finalized_slot > our_finalized_slot && !peer_head_root.is_zero() { info!( peer = %peer, - peer_head_slot, - our_head_slot, peer_finalized = peer_finalized_slot, our_finalized = our_finalized_slot, - "Peer is ahead — requesting head block to trigger backfill" + "Peer ahead on finalized only — fetching head root" ); self.send_blocks_by_root_request(peer, vec![peer_head_root]); } } + /// Must only be called from the swarm event loop. `next_batch`'s + /// `in_flight` guard is not atomic against concurrent callers. + fn drain_range_sync_batches(&mut self) { + let Some(state) = &self.range_sync_state else { + return; + }; + let Some((peer, range)) = state.next_batch() else { + return; + }; + + let start_slot = range.start; + let count = range.end - range.start; + self.send_blocks_by_range_request(peer, start_slot, count); + + if let Some(state) = &mut self.range_sync_state { + state.in_flight = true; + } + } + fn send_status_to_all_connected_peers(&mut self) { let peers: Vec = self .peer_table @@ -1657,8 +1708,9 @@ where response, .. } => { - if let Some(key) = self.inflight_blocks_by_range.remove(&request_id) { - self.inflight_range_keys.remove(&key); + let requested = self.inflight_blocks_by_range.remove(&request_id); + if let Some(ref key) = requested { + self.inflight_range_keys.remove(key); } match response { LeanResponse::BlocksByRange(blocks) => { @@ -1668,49 +1720,83 @@ where "Received BlocksByRange response" ); - { - let mut provider = self.signed_block_provider.write(); - for block in &blocks { - let root = block.block.hash_tree_root(); - provider.insert(root, block.clone()); + if blocks.is_empty() { + if let Some(state) = &mut self.range_sync_state { + state.fail_peer(&peer); + if state.peer_set.is_empty() { + self.range_sync_state = None; + warn!("Long-range sync abandoned: no peers remaining after empty response"); + } } - 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.block.slot.0)) - .collect(); - slots.sort_by_key(|(_, slot)| *slot); - for (root, _) in slots.into_iter().take(to_remove) { - provider.remove(&root); + self.drain_range_sync_batches(); + } else { + { + let mut provider = self.signed_block_provider.write(); + for block in &blocks { + let root = block.block.hash_tree_root(); + provider.insert(root, block.clone()); } + 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.block.slot.0)) + .collect(); + slots.sort_by_key(|(_, slot)| *slot); + for (root, _) in slots.into_iter().take(to_remove) { + provider.remove(&root); + } + } + METRICS.get().map(|m| { + m.grandine_signed_block_provider_size + .set(provider.len() as i64) + }); } - METRICS.get().map(|m| { - m.grandine_signed_block_provider_size - .set(provider.len() as i64) + + let chain_sink = self.chain_message_sink.clone(); + tokio::spawn(async move { + for block in blocks { + let slot = block.block.slot.0; + if let Err(e) = chain_sink + .send(ChainMessage::ProcessBlock { + signed_block: block, + is_trusted: false, + should_gossip: false, + cached_post_state: None, + }) + .await + { + warn!(slot, ?e, "Failed to forward range block to chain"); + } + } }); - } - let chain_sink = self.chain_message_sink.clone(); - tokio::spawn(async move { - for block in blocks { - let slot = block.block.slot.0; - if let Err(e) = chain_sink - .send(ChainMessage::ProcessBlock { - signed_block: block, - is_trusted: false, - should_gossip: false, - cached_post_state: None, - }) - .await - { - warn!(slot, ?e, "Failed to forward range block to chain"); + if let Some((_, start_slot, count)) = requested { + let requested_end_slot = + start_slot.saturating_add(count).saturating_sub(1); + if let Some(state) = &mut self.range_sync_state { + state.complete_batch(requested_end_slot); + if state.current_range.is_empty() + || state.peer_set.is_empty() + { + self.range_sync_state = None; + info!("Long-range sync complete"); + } } } - }); + self.drain_range_sync_batches(); + } } _ => { warn!(peer = %peer, "Unexpected response type on BlocksByRange protocol"); + if let Some(state) = &mut self.range_sync_state { + state.fail_peer(&peer); + if state.peer_set.is_empty() { + self.range_sync_state = None; + warn!("Long-range sync abandoned: no peers remaining after codec mismatch"); + } + } + self.drain_range_sync_batches(); } } } @@ -1724,7 +1810,15 @@ where if let Some(key) = self.inflight_blocks_by_range.remove(&request_id) { self.inflight_range_keys.remove(&key); } + if let Some(state) = &mut self.range_sync_state { + state.fail_peer(&peer); + if state.peer_set.is_empty() { + self.range_sync_state = None; + warn!("Long-range sync abandoned: no peers remaining"); + } + } warn!(peer = %peer, ?error, "BlocksByRange outbound request failed"); + self.drain_range_sync_batches(); } Event::InboundFailure { peer, error, .. } => { warn!(peer = %peer, ?error, "BlocksByRange inbound request failed"); diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index 9e90be2c..8e359a7e 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -455,6 +455,12 @@ impl LeanCodec { /// [code][varint][snappy(block1)][code][varint][snappy(block2)]... fn decode_response(protocol: &str, data: &[u8]) -> io::Result { if data.is_empty() { + if protocol.contains("blocks_by_range") { + return Ok(LeanResponse::BlocksByRange(Vec::new())); + } + if protocol.contains("blocks_by_root") { + return Ok(LeanResponse::BlocksByRoot(Vec::new())); + } return Ok(LeanResponse::Empty); } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d44ec443..9ac576e4 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = 'nightly' +channel = '1.93' profile = 'minimal' components = ['clippy', 'rustfmt'] From 87ddab25075e92f0a3740e4f522400ebfe20ee18 Mon Sep 17 00:00:00 2001 From: bomanaps Date: Wed, 17 Jun 2026 17:32:57 +0100 Subject: [PATCH 5/7] address review comment --- lean_client/Cargo.toml | 4 +--- lean_client/fork_choice/src/handlers.rs | 6 +----- lean_client/networking/src/network/service.rs | 14 +++++++++++--- lean_client/xmss/src/multi_message.rs | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index cd54dbf7..c1dec2e9 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -339,7 +339,5 @@ reqwest = { workspace = true } tikv-jemallocator = { workspace = true } [profile.release] -debug = true -split-debuginfo = "packed" -lto = "thin" +lto = true codegen-units = 1 diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index c97106bd..478facab 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -120,11 +120,7 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<() /// Returns whether `ancestor` lies on `descendant`'s parent chain. /// /// An unknown root in the walk yields false (lineage cannot be proven). -fn checkpoint_is_ancestor( - store: &Store, - ancestor: &Checkpoint, - descendant: &Checkpoint, -) -> bool { +fn checkpoint_is_ancestor(store: &Store, ancestor: &Checkpoint, descendant: &Checkpoint) -> bool { if ancestor.slot > descendant.slot { return false; } diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index d583dc6d..79de1586 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -1725,7 +1725,9 @@ where state.fail_peer(&peer); if state.peer_set.is_empty() { self.range_sync_state = None; - warn!("Long-range sync abandoned: no peers remaining after empty response"); + warn!( + "Long-range sync abandoned: no peers remaining after empty response" + ); } } self.drain_range_sync_batches(); @@ -1766,7 +1768,11 @@ where }) .await { - warn!(slot, ?e, "Failed to forward range block to chain"); + warn!( + slot, + ?e, + "Failed to forward range block to chain" + ); } } }); @@ -1793,7 +1799,9 @@ where state.fail_peer(&peer); if state.peer_set.is_empty() { self.range_sync_state = None; - warn!("Long-range sync abandoned: no peers remaining after codec mismatch"); + warn!( + "Long-range sync abandoned: no peers remaining after codec mismatch" + ); } } self.drain_range_sync_batches(); diff --git a/lean_client/xmss/src/multi_message.rs b/lean_client/xmss/src/multi_message.rs index fa4f5a59..c67940ec 100644 --- a/lean_client/xmss/src/multi_message.rs +++ b/lean_client/xmss/src/multi_message.rs @@ -18,7 +18,7 @@ type MultiMessageAggregateSizeLimit = U524288; #[derive(Clone, Debug, Default, Ssz)] pub struct MultiMessageAggregate { - pub proof: ByteList, + proof: ByteList, } impl MultiMessageAggregate { From dc12dc5256c055f22019bafdfd25d8e6e6086ae5 Mon Sep 17 00:00:00 2001 From: bomanaps Date: Fri, 19 Jun 2026 07:10:43 +0100 Subject: [PATCH 6/7] rayon cap, exec split, spec pr port --- lean_client/Cargo.lock | 7 +- lean_client/Cargo.toml | 2 +- lean_client/fork_choice/src/handlers.rs | 114 ++++++++----------- lean_client/fork_choice/src/store.rs | 49 +++++++- lean_client/src/main.rs | 38 ++++--- lean_client/validator/src/lib.rs | 7 ++ lean_client/xmss/Cargo.toml | 2 + lean_client/xmss/src/aggregated_signature.rs | 15 ++- lean_client/xmss/src/lib.rs | 2 +- 9 files changed, 142 insertions(+), 94 deletions(-) diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index 660c5977..8f507b99 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -1294,7 +1294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 1.0.109", ] [[package]] @@ -2947,7 +2947,6 @@ dependencies = [ "networking", "num_cpus", "parking_lot", - "rayon", "reqwest", "ssz", "tikv-jemallocator", @@ -5897,7 +5896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -7086,6 +7085,7 @@ name = "xmss" version = "0.0.0" dependencies = [ "anyhow", + "backend", "bls", "derive_more", "ethereum-types", @@ -7096,6 +7096,7 @@ dependencies = [ "metrics", "rand 0.10.1", "rand_chacha 0.10.0", + "rayon", "rec_aggregation", "serde", "ssz", diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index c1dec2e9..5a394eac 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -252,6 +252,7 @@ http-body-util = "0.1" http_api_utils = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" } k256 = "0.13" rec_aggregation = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df4e30fdddbbf8ae26a333116c68cec7026" } +backend = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df4e30fdddbbf8ae26a333116c68cec7026" } leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" } leansig_wrapper = { git = "https://github.com/leanEthereum/leanVM.git", rev = "e2592df4e30fdddbbf8ae26a333116c68cec7026" } libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "91e8931e275bcd1c72791d18b09fea8b77209baf", default-features = false, features = [ @@ -328,7 +329,6 @@ metrics = { workspace = true } networking = { workspace = true } num_cpus = { workspace = true } parking_lot = { workspace = true } -rayon = { workspace = true } ssz = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 478facab..d93083ea 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -761,7 +761,7 @@ pub fn apply_verified_block( } let justified_updated = new_state.latest_justified.slot > store.latest_justified.slot; - let finalized_updated = new_state.latest_finalized.slot > store.latest_finalized.slot; + let prev_finalized_slot = store.latest_finalized.slot; if justified_updated { tracing::info!( @@ -779,69 +779,6 @@ pub fn apply_verified_block( metrics.lean_latest_justified_slot.set(slot); }); } - if finalized_updated { - tracing::info!( - old_finalized = store.latest_finalized.slot.0, - new_finalized = new_state.latest_finalized.slot.0, - "Store finalized checkpoint updated!" - ); - store.latest_finalized = new_state.latest_finalized.clone(); - store.finalized_ever_updated = true; - METRICS.get().map(|metrics| { - let Some(slot) = new_state.latest_finalized.slot.0.try_into().ok() else { - warn!("unable to set latest_finalized slot in metrics"); - return; - }; - metrics.lean_latest_finalized_slot.set(slot); - }); - - let keep_from = store - .latest_finalized - .slot - .0 - .saturating_sub(STATE_PRUNE_BUFFER); - store.states.retain(|_, state| state.slot.0 >= keep_from); - store.blocks.retain(|_, block| block.slot.0 >= keep_from); - - // 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 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(|data_root, _| { - adr.get(data_root) - .map_or(true, |data| data.target.slot.0 > finalized_slot) - }); - store.latest_new_aggregated_payloads.retain(|data_root, _| { - adr.get(data_root) - .map_or(true, |data| data.target.slot.0 > finalized_slot) - }); - store - .attestation_data_by_root - .retain(|_, data| data.target.slot.0 > finalized_slot); - METRICS.get().map(|m| { - m.grandine_attestation_data_by_root - .set(store.attestation_data_by_root.len() as i64); - }); - } - - if !justified_updated && !finalized_updated { - tracing::debug!( - block_slot = block.slot.0, - store_justified = store.latest_justified.slot.0, - store_finalized = store.latest_finalized.slot.0, - state_justified = new_state.latest_justified.slot.0, - state_finalized = new_state.latest_finalized.slot.0, - "No checkpoint updates from this block" - ); - } let aggregated_attestations = &block.body.attestations; @@ -893,6 +830,55 @@ pub fn apply_verified_block( update_head(store); + if store.latest_finalized.slot > prev_finalized_slot { + tracing::info!( + old_finalized = prev_finalized_slot.0, + new_finalized = store.latest_finalized.slot.0, + "Store finalized checkpoint updated!" + ); + + let keep_from = store + .latest_finalized + .slot + .0 + .saturating_sub(STATE_PRUNE_BUFFER); + store.states.retain(|_, state| state.slot.0 >= keep_from); + store.blocks.retain(|_, block| block.slot.0 >= keep_from); + + 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(|data_root, _| { + adr.get(data_root) + .map_or(true, |data| data.target.slot.0 > finalized_slot) + }); + store.latest_new_aggregated_payloads.retain(|data_root, _| { + adr.get(data_root) + .map_or(true, |data| data.target.slot.0 > finalized_slot) + }); + store + .attestation_data_by_root + .retain(|_, data| data.target.slot.0 > finalized_slot); + METRICS.get().map(|m| { + m.grandine_attestation_data_by_root + .set(store.attestation_data_by_root.len() as i64); + }); + } else if !justified_updated { + tracing::debug!( + block_slot = block.slot.0, + store_justified = store.latest_justified.slot.0, + store_finalized = store.latest_finalized.slot.0, + state_justified = new_state.latest_justified.slot.0, + state_finalized = new_state.latest_finalized.slot.0, + "No checkpoint updates from this block" + ); + } + prune_with_retention_bounds(store); Ok(()) diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 05293fa9..6e99434e 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -180,6 +180,7 @@ impl Store { let attestations = extract_attestations_from_aggregated_payloads( &self.latest_known_aggregated_payloads, &self.attestation_data_by_root, + self.latest_finalized.slot, ); let start_slot = self.latest_finalized.slot; @@ -378,15 +379,45 @@ pub fn get_latest_justified(states: &HashMap) -> Option<&Checkpoint pub fn update_head(store: &mut Store) { let old_head = store.head; - // Compute new head using LMD-GHOST from latest justified root - let new_head = get_fork_choice_head( - store, - store.latest_justified.root, - &store.latest_known_attestations, - 0, + let latest_votes = extract_attestations_from_aggregated_payloads( + &store.latest_known_aggregated_payloads, + &store.attestation_data_by_root, + store.latest_finalized.slot, ); + + // Compute new head using LMD-GHOST from latest justified root + let new_head = get_fork_choice_head(store, store.latest_justified.root, &latest_votes, 0); store.head = new_head; + if let Some(head_state) = store.states.get(&new_head) { + let finalized_slot = head_state.latest_finalized.slot; + let mut finalized_root = new_head; + while let Some(block) = store.blocks.get(&finalized_root) { + if block.slot <= finalized_slot { + break; + } + let parent_root = block.parent_root; + if !store.blocks.contains_key(&parent_root) { + break; + } + finalized_root = parent_root; + } + if let Some(block) = store.blocks.get(&finalized_root) { + if block.slot == finalized_slot { + store.latest_finalized = Checkpoint { + root: finalized_root, + slot: finalized_slot, + }; + store.finalized_ever_updated = true; + METRICS.get().map(|m| { + if let Ok(s) = i64::try_from(finalized_slot.0) { + m.lean_latest_finalized_slot.set(s); + } + }); + } + } + } + // Detect reorg if head changed and new head's parent is not old head if new_head != old_head && !old_head.is_zero() { if let Some(new_head_block) = store.blocks.get(&new_head) { @@ -448,6 +479,7 @@ pub fn update_head(store: &mut Store) { fn extract_attestations_from_aggregated_payloads( payloads: &IndexMap>, attestation_data_by_root: &HashMap, + latest_finalized_slot: Slot, ) -> HashMap { let mut attestations: HashMap = HashMap::new(); @@ -457,6 +489,10 @@ fn extract_attestations_from_aggregated_payloads( continue; }; + if attestation_data.head.slot <= latest_finalized_slot { + continue; + } + // For each proof, extract participating validators for proof in proofs { for (bit_idx, bit) in proof.participants.0.iter().enumerate() { @@ -508,6 +544,7 @@ pub fn update_safe_target(store: &mut Store) { let attestations = extract_attestations_from_aggregated_payloads( &store.latest_new_aggregated_payloads, &store.attestation_data_by_root, + store.latest_finalized.slot, ); // Run LMD-GHOST with 2/3 threshold to find safe target diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index 4ea3e133..b0eef68f 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -422,22 +422,8 @@ struct Args { #[tokio::main] async fn main() -> Result<()> { - let available = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(2); - let rayon_threads = available.saturating_sub(2).max(1); - match rayon::ThreadPoolBuilder::new() - .num_threads(rayon_threads) - .build_global() - { - Ok(()) => eprintln!( - "configured global rayon pool: available={available} rayon_threads={rayon_threads}" - ), - Err(e) => eprintln!( - "global rayon pool already initialized; rayon_threads cap NOT applied: available={available} err={e}" - ), - } - + let rayon_threads = num_cpus::get().saturating_sub(3).max(1); + xmss::configure_rayon_pool(rayon_threads); xmss::setup_aggregation(); tracing_subscriber::fmt() @@ -497,6 +483,7 @@ async fn main() -> Result<()> { mpsc::unbounded_channel::(); let num_cpus = num_cpus::get(); + let cpu_normal_executor = Arc::new(DedicatedExecutor::new( "lean-cpu-normal", (num_cpus / 2).max(2), @@ -1838,7 +1825,7 @@ async fn main() -> Result<()> { Ok(inputs) => { let agg_payload_count = inputs.aggregated_payloads.len(); let validators = inputs.head_state.validators.clone(); - let exec = cpu_snark_executor.clone(); + let exec = cpu_normal_executor.clone(); let exec_start = Instant::now(); tokio::spawn(async move { let job = exec.spawn(async move { @@ -1853,7 +1840,7 @@ async fn main() -> Result<()> { let exec_elapsed = exec_start.elapsed(); let total_elapsed = block_build_start.elapsed(); match &result { - Ok(_) => { + Ok((block, _sigs, _validators, post_state)) => { METRICS.get().map(|m| { m.lean_block_building_success_total.inc(); m.lean_block_building_time_seconds @@ -1863,6 +1850,21 @@ async fn main() -> Result<()> { m.lean_block_aggregated_payloads .observe(agg_payload_count as f64); }); + info!( + block_slot = block.slot.0, + state_slot = post_state.slot.0, + state_root = %post_state.hash_tree_root(), + slot_h = %post_state.slot.hash_tree_root(), + lbh = %post_state.latest_block_header.hash_tree_root(), + lj = %post_state.latest_justified.hash_tree_root(), + lf = %post_state.latest_finalized.hash_tree_root(), + hbh = %post_state.historical_block_hashes.hash_tree_root(), + js = %post_state.justified_slots.hash_tree_root(), + v = %post_state.validators.hash_tree_root(), + jr = %post_state.justifications_roots.hash_tree_root(), + jv = %post_state.justifications_validators.hash_tree_root(), + "proposer post-state per-field" + ); } Err(_) => { METRICS.get().map(|m| m.lean_block_building_failures_total.inc()); diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 171edd23..909f5ae7 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -142,6 +142,13 @@ pub fn snapshot_aggregation_inputs(store: &Store) -> Option let mut accepted_child_ids: Vec = covered_by_children.into_iter().collect(); accepted_child_ids.sort(); + info!( + data_root = %data_root, + raw_sigs = raw_ids.len(), + children = children_refs.len(), + "aggregation job built" + ); + jobs.push(AggregationJob { data_root, attestation_data: attestation_data.clone(), diff --git a/lean_client/xmss/Cargo.toml b/lean_client/xmss/Cargo.toml index c48bd30b..4c941a4a 100644 --- a/lean_client/xmss/Cargo.toml +++ b/lean_client/xmss/Cargo.toml @@ -13,10 +13,12 @@ eth_ssz = { workspace = true } ethereum-types = { workspace = true } hex = { workspace = true } rec_aggregation = { workspace = true } +backend = { workspace = true } leansig = { workspace = true } leansig_wrapper = { workspace = true } metrics = { workspace = true } rand = { workspace = true } +rayon = { workspace = true } ssz = { workspace = true } typenum = { workspace = true } serde = { workspace = true } diff --git a/lean_client/xmss/src/aggregated_signature.rs b/lean_client/xmss/src/aggregated_signature.rs index 42b06824..4c9fc355 100644 --- a/lean_client/xmss/src/aggregated_signature.rs +++ b/lean_client/xmss/src/aggregated_signature.rs @@ -54,7 +54,20 @@ impl SszHash for AggregatedSignature { pub fn setup_aggregation() { static SETUP: Once = Once::new(); - SETUP.call_once(init_aggregation_bytecode); + SETUP.call_once(|| { + init_aggregation_bytecode(); + backend::precompute_dft_twiddles::(1 << 24); + }); +} + +/// Cap the global rayon pool so the leansig prover doesn't oversubscribe physical +/// cores. Idempotent; safe to call once at process startup before any aggregation. +pub fn configure_rayon_pool(num_threads: usize) { + drop( + rayon::ThreadPoolBuilder::new() + .num_threads(num_threads) + .build_global(), + ); } impl AggregatedSignature { diff --git a/lean_client/xmss/src/lib.rs b/lean_client/xmss/src/lib.rs index 08441db7..4aab3236 100644 --- a/lean_client/xmss/src/lib.rs +++ b/lean_client/xmss/src/lib.rs @@ -4,7 +4,7 @@ mod public_key; mod secret_key; mod signature; -pub use aggregated_signature::{AggregatedSignature, setup_aggregation}; +pub use aggregated_signature::{AggregatedSignature, configure_rayon_pool, setup_aggregation}; pub use multi_message::MultiMessageAggregate; pub use public_key::PublicKey; pub use secret_key::SecretKey; From ace40864560a03cf7721e9aa71f7874d3a653717 Mon Sep 17 00:00:00 2001 From: bomanaps Date: Fri, 19 Jun 2026 09:20:37 +0100 Subject: [PATCH 7/7] add rpc backpressure with drop metric --- lean_client/metrics/src/metrics.rs | 14 +++ lean_client/networking/src/network/service.rs | 90 ++++++++++++------- lean_client/networking/src/types.rs | 12 +++ 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/lean_client/metrics/src/metrics.rs b/lean_client/metrics/src/metrics.rs index ac9f334a..51917685 100644 --- a/lean_client/metrics/src/metrics.rs +++ b/lean_client/metrics/src/metrics.rs @@ -66,6 +66,7 @@ pub struct Metrics { /// Total number of valid attestations pub lean_aggregator_skipped_total: IntCounterVec, + pub lean_chain_message_drop_total: IntCounterVec, pub lean_attestations_valid_total: IntCounterVec, /// Total number of invalid attestations @@ -349,6 +350,13 @@ impl Metrics { ), &["reason"], )?, + lean_chain_message_drop_total: IntCounterVec::new( + opts!( + "lean_chain_message_drop_total", + "Chain-message channel drops on full, by protocol", + ), + &["protocol"], + )?, lean_attestations_valid_total: IntCounterVec::new( opts!( "lean_attestations_valid_total", @@ -814,6 +822,12 @@ impl Metrics { self.lean_fork_choice_block_processing_time_seconds.clone(), ))?; default_registry.register(Box::new(self.lean_aggregator_skipped_total.clone()))?; + default_registry.register(Box::new(self.lean_chain_message_drop_total.clone()))?; + for &protocol in &["blocks_by_root", "blocks_by_range"] { + self.lean_chain_message_drop_total + .with_label_values(&[protocol]) + .inc_by(0); + } for &reason in &[ "not_aggregator", "not_synced", diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 79de1586..04d2ec95 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -989,31 +989,39 @@ where } } - // Step 3: Feed received blocks to chain for state processing. - // This runs in parallel with the pipelined network request above. let chain_sink = self.chain_message_sink.clone(); tokio::spawn(async move { for block in blocks { let slot = block.block.slot.0; - if let Err(e) = chain_sink - .send(ChainMessage::ProcessBlock { - signed_block: block, - is_trusted: false, - should_gossip: false, - cached_post_state: None, - }) - .await - { - warn!( - slot = slot, - ?e, - "Failed to send requested block to chain" - ); - } else { - debug!( + match chain_sink.try_send(ChainMessage::ProcessBlock { + signed_block: block, + is_trusted: false, + should_gossip: false, + cached_post_state: None, + }) { + Ok(()) => debug!( slot = slot, "Queued requested block for processing" - ); + ), + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + warn!( + slot = slot, + protocol = "blocks_by_root", + "Dropping RPC chunk: chain channel full" + ); + METRICS.get().map(|m| { + m.lean_chain_message_drop_total + .with_label_values(&["blocks_by_root"]) + .inc() + }); + } + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => { + warn!( + slot = slot, + "Failed to send requested block to chain: channel closed" + ); + break; + } } } }); @@ -1759,20 +1767,36 @@ where tokio::spawn(async move { for block in blocks { let slot = block.block.slot.0; - if let Err(e) = chain_sink - .send(ChainMessage::ProcessBlock { - signed_block: block, - is_trusted: false, - should_gossip: false, - cached_post_state: None, - }) - .await - { - warn!( - slot, - ?e, - "Failed to forward range block to chain" - ); + match chain_sink.try_send(ChainMessage::ProcessBlock { + signed_block: block, + is_trusted: false, + should_gossip: false, + cached_post_state: None, + }) { + Ok(()) => {} + Err(tokio::sync::mpsc::error::TrySendError::Full( + _, + )) => { + warn!( + slot, + protocol = "blocks_by_range", + "Dropping RPC chunk: chain channel full" + ); + METRICS.get().map(|m| { + m.lean_chain_message_drop_total + .with_label_values(&["blocks_by_range"]) + .inc() + }); + } + Err( + tokio::sync::mpsc::error::TrySendError::Closed(_), + ) => { + warn!( + slot, + "Failed to forward range block to chain: channel closed" + ); + break; + } } } }); diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index f1f3dab6..23d357f2 100644 --- a/lean_client/networking/src/types.rs +++ b/lean_client/networking/src/types.rs @@ -263,6 +263,8 @@ pub enum OutboundP2pRequest { #[async_trait] pub trait ChainMessageSink: Send + Sync + Clone { async fn send(&self, message: M) -> Result<()>; + + fn try_send(&self, message: M) -> Result<(), mpsc::error::TrySendError>; } #[async_trait] @@ -279,6 +281,16 @@ impl ChainMessageSink for mpsc::Sender { } result } + + fn try_send(&self, message: M) -> Result<(), mpsc::error::TrySendError> { + let result = mpsc::Sender::try_send(self, message); + if result.is_ok() { + METRICS.get().map(|m| { + m.grandine_chain_message_channel_depth.inc(); + }); + } + result + } } #[async_trait]