From 293de72869d2281e889ae3cfd6bed0578219ed6b Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 7 Sep 2026 20:02:35 -0400 Subject: [PATCH] validator_store: align Gloas aggregation timing Pin Lighthouse to upstream unstable 1256bd99e and align aggregation round origins and shared metadata scheduling with the duty slot fork. Add regression coverage for the first Gloas slot. Addresses #1292. --- Cargo.lock | 103 ++++--- Cargo.toml | 34 +-- anchor/validator_store/src/lib.rs | 41 ++- .../validator_store/src/metadata_service.rs | 55 +++- .../src/testing/aggregation_timing.rs | 258 ++++++++++++++++++ anchor/validator_store/src/testing/common.rs | 8 +- anchor/validator_store/src/testing/mod.rs | 1 + 7 files changed, 417 insertions(+), 83 deletions(-) create mode 100644 anchor/validator_store/src/testing/aggregation_timing.rs diff --git a/Cargo.lock b/Cargo.lock index cb0944df8..36a320342 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -425,7 +425,7 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "lru", + "lru 0.16.3", "parking_lot", "pin-project", "reqwest", @@ -791,7 +791,7 @@ dependencies = [ "fork", "futures", "hex", - "lru", + "lru 0.16.3", "metrics", "openssl", "parking_lot", @@ -1465,7 +1465,7 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "beacon_node_fallback" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", "clap", @@ -1565,7 +1565,7 @@ dependencies = [ [[package]] name = "bls" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "alloy-primitives 1.5.7", "blst", @@ -1662,6 +1662,25 @@ dependencies = [ "serde", ] +[[package]] +name = "builder_types" +version = "0.1.0" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" +dependencies = [ + "bls", + "context_deserialize", + "ethereum_serde_utils 0.8.0", + "ethereum_ssz", + "ethereum_ssz_derive", + "sensitive_url", + "serde", + "ssz_types", + "tree_hash", + "tree_hash_derive", + "typenum", + "types", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -2693,7 +2712,7 @@ dependencies = [ [[package]] name = "eip_3076" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", "ethereum_serde_utils 0.8.0", @@ -2917,9 +2936,10 @@ dependencies = [ [[package]] name = "eth2" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", + "builder_types", "context_deserialize", "educe", "eip_3076", @@ -2947,7 +2967,7 @@ dependencies = [ [[package]] name = "eth2_config" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "paste", "types", @@ -2956,7 +2976,7 @@ dependencies = [ [[package]] name = "eth2_interop_keypairs" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", "ethereum_hashing 0.8.0", @@ -2969,7 +2989,7 @@ dependencies = [ [[package]] name = "eth2_key_derivation" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", "num-bigint-dig", @@ -2981,7 +3001,7 @@ dependencies = [ [[package]] name = "eth2_keystore" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "aes", "bls", @@ -3005,7 +3025,7 @@ dependencies = [ [[package]] name = "eth2_network_config" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bytes", "discv5", @@ -3228,7 +3248,7 @@ dependencies = [ [[package]] name = "filesystem" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "winapi", "windows-acl", @@ -3255,7 +3275,7 @@ dependencies = [ [[package]] name = "fixed_bytes" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "alloy-primitives 1.5.7", "safe_arith", @@ -3331,7 +3351,7 @@ dependencies = [ [[package]] name = "fork_choice" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", @@ -3610,7 +3630,7 @@ dependencies = [ [[package]] name = "graffiti_file" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", "serde", @@ -3668,6 +3688,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", "serde", ] @@ -3697,7 +3719,7 @@ dependencies = [ [[package]] name = "health_metrics" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "eth2", "metrics", @@ -4243,7 +4265,7 @@ dependencies = [ [[package]] name = "int_to_bytes" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bytes", ] @@ -4466,7 +4488,7 @@ dependencies = [ [[package]] name = "kzg" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "educe", "ethereum_hashing 0.8.0", @@ -4992,7 +5014,7 @@ dependencies = [ [[package]] name = "logging" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "chrono", "logroller", @@ -5020,6 +5042,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "lru" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "lru" version = "0.16.3" @@ -5038,7 +5069,7 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lru_cache" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "fnv", ] @@ -5119,7 +5150,7 @@ dependencies = [ [[package]] name = "merkle_proof" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "alloy-primitives 1.5.7", "ethereum_hashing 0.8.0", @@ -5218,7 +5249,7 @@ dependencies = [ [[package]] name = "metrics" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "prometheus", ] @@ -5465,7 +5496,7 @@ dependencies = [ [[package]] name = "network_utils" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "discv5", "libp2p-identity", @@ -6043,7 +6074,7 @@ dependencies = [ [[package]] name = "pretty_reqwest_error" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "reqwest", "sensitive_url", @@ -6264,7 +6295,7 @@ dependencies = [ [[package]] name = "proto_array" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", @@ -7545,7 +7576,7 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slashing_protection" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", "eip_3076", @@ -7565,7 +7596,7 @@ dependencies = [ [[package]] name = "slot_clock" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "metrics", "parking_lot", @@ -7736,7 +7767,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state_processing" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", "educe", @@ -7747,9 +7778,11 @@ dependencies = [ "int_to_bytes", "integer-sqrt", "itertools 0.14.0", + "lru 0.13.0", "merkle_proof", "metrics", "milhouse", + "parking_lot", "rand 0.9.2", "rayon", "safe_arith", @@ -7841,7 +7874,7 @@ dependencies = [ [[package]] name = "swap_or_not_shuffle" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "alloy-primitives 1.5.7", "ethereum_hashing 0.8.0", @@ -7944,7 +7977,7 @@ checksum = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" [[package]] name = "task_executor" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "async-channel 1.9.0", "futures", @@ -8476,7 +8509,7 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "types" version = "0.2.1" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "alloy-primitives 1.5.7", "alloy-rlp", @@ -8665,7 +8698,7 @@ dependencies = [ [[package]] name = "validator_metrics" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "metrics", ] @@ -8673,7 +8706,7 @@ dependencies = [ [[package]] name = "validator_services" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "beacon_node_fallback", "bls", @@ -8698,7 +8731,7 @@ dependencies = [ [[package]] name = "validator_store" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "bls", "eth2", @@ -9467,7 +9500,7 @@ dependencies = [ [[package]] name = "workspace_members" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?rev=e58ec88fe#e58ec88fe9a8f755a0d92a80b25fd76100d08d49" +source = "git+https://github.com/sigp/lighthouse?rev=1256bd99e1b6c5d1290241849354b7e27dbc0ed9#1256bd99e1b6c5d1290241849354b7e27dbc0ed9" dependencies = [ "cargo_metadata", "quote", diff --git a/Cargo.toml b/Cargo.toml index 20232afb7..eb43e6a88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,23 +75,23 @@ ssv_types = { path = "anchor/common/ssv_types" } subnet_service = { path = "anchor/subnet_service" } version = { path = "anchor/common/version" } -# Lighthouse latest from unstable (e58ec88fe) -beacon_node_fallback = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -bls = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -eth2 = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -eth2_keystore = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -eth2_network_config = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -health_metrics = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -metrics = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -network_utils = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -slashing_protection = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -slot_clock = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -task_executor = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -types = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -validator_metrics = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -validator_services = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -validator_store = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } -workspace_members = { git = "https://github.com/sigp/lighthouse", rev = "e58ec88fe" } +# Lighthouse latest from unstable (1256bd99e1b6c5d1290241849354b7e27dbc0ed9) +beacon_node_fallback = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +bls = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +eth2 = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +eth2_keystore = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +eth2_network_config = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +health_metrics = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +metrics = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +network_utils = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +slashing_protection = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +slot_clock = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +task_executor = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +types = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +validator_metrics = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +validator_services = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +validator_store = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } +workspace_members = { git = "https://github.com/sigp/lighthouse", rev = "1256bd99e1b6c5d1290241849354b7e27dbc0ed9" } alloy = { version = "1.2.1", features = [ "sol-types", diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 2d813a76a..61ac418e6 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -312,7 +312,7 @@ pub struct AnchorValidatorStore< voting_context_tx: watch::Sender>>, /// Watch channel for `VotingAssignments` (cached at slot start) voting_assignments_tx: watch::Sender>>, - /// Watch channel for `AggregationAssignments` (cached at 2/3 slot) + /// Watch channel for `AggregationAssignments` (cached at the aggregation deadline) aggregation_assignments_tx: watch::Sender>>>, gas_limit: u64, // MEV configuration is applied at the operator level and applies to all validators this @@ -678,7 +678,7 @@ impl + 'static> AnchorValidator Ok(drain_signatures(pending).await) } - /// Run `AggregatorCommittee` QBFT consensus for a committee at 2/3 slot. + /// Run `AggregatorCommittee` QBFT consensus at the fork-specific aggregation deadline. /// /// Called once per `(committee, slot)` by the post-consensus execution in /// [`crate::aggregator_post_consensus`], which supplies the value this operator proposes. @@ -693,7 +693,7 @@ impl + 'static> AnchorValidator metrics::start_timer_vec(&metrics::CONSENSUS_TIMES, &[metrics::AGGREGATOR_COMMITTEE]); let timeout_mode = TimeoutMode::SlotTime { round_deadline_origin: self - .get_instant_in_slot(slot, self.spec.get_slot_duration() * 2 / 3)?, + .get_instant_in_slot(slot, self.spec.get_aggregate_attestation_due::(slot))?, }; let completed = self @@ -1331,8 +1331,7 @@ impl + 'static> AnchorValidator /// Get aggregator voting assignments, waiting if not yet available for this slot. /// /// This method waits until `AggregationAssignments` for the requested slot becomes available. - /// Called by `run_aggregator_post_consensus` (via `run_aggregator_committee_consensus`) - /// at 2/3 slot. + /// Consumed by aggregation duties at the fork-specific aggregation deadline. /// /// Returns an error if the requested slot has already passed or if the watch channel is closed. pub async fn get_aggregation_assignments( @@ -1361,10 +1360,10 @@ impl + 'static> AnchorValidator } } - /// Update aggregator voting assignments (called by `MetadataService` Phase 3 at 2/3 slot). + /// Update aggregator voting assignments at the fork-specific aggregation deadline. /// /// This publishes the `AggregationAssignments` to all subscribers via the watch channel. - /// At 2/3 slot, selection proofs have been computed by Lighthouse, so + /// Once selection proofs have been computed by Lighthouse, /// `DutyAndProof.selection_proof.is_some()` accurately indicates `is_aggregator`. /// /// It also starts each Boole+ committee's post-consensus signing execution, before publishing, @@ -1664,7 +1663,8 @@ impl + 'static> AnchorValidator let timeout_mode = TimeoutMode::SlotTime { round_deadline_origin: self.get_instant_in_slot( message.aggregate().data().slot, - self.spec.get_slot_duration() * 2 / 3, + self.spec + .get_aggregate_attestation_due::(message.aggregate().data().slot), )?, }; @@ -1769,7 +1769,7 @@ impl + 'static> AnchorValidator selection_proof: contribution.selection_proof, }; - // Get aggregator voting assignments from Phase 3 (published at 2/3 slot) + // Get aggregator voting assignments from Phase 3 at the aggregation deadline let aggregator_info = self.get_aggregation_assignments(slot).await?; let signing_data = match aggregator_info @@ -1808,7 +1808,7 @@ impl + 'static> AnchorValidator ); let timeout_mode = TimeoutMode::SlotTime { round_deadline_origin: self - .get_instant_in_slot(slot, self.spec.get_slot_duration() * 2 / 3)?, + .get_instant_in_slot(slot, self.spec.get_contribution_message_due::(slot))?, }; let completed = self @@ -2762,13 +2762,13 @@ impl VotingAssignments { } } -/// Aggregator-specific voting assignments, cached at 2/3 slot when selection proofs are known. +/// Aggregator-specific voting assignments, cached at the aggregation deadline. /// /// This struct is separate from `VotingAssignments` because: /// - `VotingAssignments` is cached at slot start, before selection proofs are computed -/// - `AggregationAssignments` is cached at 2/3 slot, after Lighthouse fills in selection proofs +/// - `AggregationAssignments` is cached at the aggregation deadline, after selection proofs /// -/// At 2/3 slot, `DutyAndProof.selection_proof.is_some()` indicates `is_aggregator = true`. +/// `DutyAndProof.selection_proof.is_some()` indicates `is_aggregator = true`. /// /// Also tracks multi-subnet sync aggregators. When a validator aggregates for multiple /// sync subnets, `produce_signed_contribution_and_proof` is called multiple times (once @@ -4939,18 +4939,29 @@ mod tests { fn attestation_due_switches_at_gloas_boundary() { use types::MainnetEthSpec; + // Arrange let mut spec = ChainSpec::mainnet(); let gloas_activation_epoch = Epoch::new(100); spec.gloas_fork_epoch = Some(gloas_activation_epoch); let first_gloas_slot = gloas_activation_epoch.start_slot(MainnetEthSpec::slots_per_epoch()); let last_pre_gloas_slot = first_gloas_slot - 1; + // Act let pre = spec.get_attestation_due::(last_pre_gloas_slot); let post = spec.get_attestation_due::(first_gloas_slot); + // Assert assert_ne!(pre, post); - assert_eq!(pre, spec.unaggregated_attestation_due); - assert_eq!(post, spec.unaggregated_attestation_due_gloas); + assert_eq!( + pre, + spec.compute_slot_component_duration(spec.attestation_due_bps) + .expect("mainnet attestation BPS should be valid"), + ); + assert_eq!( + post, + spec.compute_slot_component_duration(spec.attestation_due_bps_gloas) + .expect("mainnet Gloas attestation BPS should be valid"), + ); } // ==================== SlotVote accessor tests ==================== diff --git a/anchor/validator_store/src/metadata_service.rs b/anchor/validator_store/src/metadata_service.rs index caa95f8df..68cffe3ae 100644 --- a/anchor/validator_store/src/metadata_service.rs +++ b/anchor/validator_store/src/metadata_service.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + future::Future, sync::Arc, time::Duration, }; @@ -324,6 +325,34 @@ async fn run_slot_start_publisher(slot_clock: T, mut publish: impl } } +/// Drive aggregation publication at the upcoming slot's fork-specific deadline. +pub(super) async fn run_aggregation_publisher>( + slot_clock: T, + spec: Arc, + mut publish: impl FnMut() -> F, +) { + loop { + // Sample once so the target slot and remaining delay agree across a slot boundary. + let delay = slot_clock.now_duration().and_then(|now| { + let next_slot = slot_clock + .slot_of(now) + .map_or_else(|| slot_clock.genesis_slot(), |slot| slot + 1); + slot_clock + .start_of(next_slot)? + .checked_add(spec.get_aggregate_attestation_due::(next_slot))? + .checked_sub(now) + }); + + if let Some(delay) = delay { + sleep(delay).await; + publish().await; + } else { + error!("Failed to read slot clock"); + sleep(slot_clock.slot_duration()).await; + } + } +} + impl MetadataService { pub fn new( duties_service: Arc, T>>, @@ -453,7 +482,7 @@ impl MetadataService { ); // ═══════════════════════════════════════════════════════════════════════ - // PHASE 3: AggregationAssignments (2/3 slot) + // PHASE 3: AggregationAssignments (fork-specific aggregation deadline) // Re-fetches `duties_service.attesters()` after selection proofs are computed. // At this point, `DutyAndProof.selection_proof.is_some()` accurately indicates // `is_aggregator` for attestation duties. @@ -461,21 +490,16 @@ impl MetadataService { let self_clone_phase3 = self.clone(); executor.spawn( async move { - loop { - if let Some(duration_to_next_slot) = - self_clone_phase3.slot_clock.duration_to_next_slot() - { - // Sleep until 2/3 into slot - sleep(duration_to_next_slot + slot_duration * 2 / 3).await; - + run_aggregation_publisher::( + self_clone_phase3.slot_clock.clone(), + self_clone_phase3.spec.clone(), + || async { if let Err(err) = self_clone_phase3.update_aggregation_assignments().await { error!(err, "Failed to update aggregator voting assignments"); } - } else { - error!("Failed to read slot clock"); - sleep(slot_duration).await; - } - } + }, + ) + .await; }, "aggregation_assignments_service", ); @@ -636,7 +660,8 @@ impl MetadataService { } } - /// Phase 3: Build and publish `AggregationAssignments` at 2/3 slot. + /// Phase 3: Build and publish `AggregationAssignments` at the fork-specific aggregation + /// deadline. /// /// Uses single-pass data transformation to minimize iterations: /// - ONE pass over attesters (those with `selection_proof`) to build all attester-related data @@ -761,7 +786,7 @@ impl MetadataService { .update_aggregation_assignments(aggregator_info); self.spawn_aggregate_publisher(slot, new_executions); - trace!(%slot, "Published AggregationAssignments at 2/3 slot"); + trace!(%slot, "Published AggregationAssignments"); Ok(()) } diff --git a/anchor/validator_store/src/testing/aggregation_timing.rs b/anchor/validator_store/src/testing/aggregation_timing.rs new file mode 100644 index 000000000..f302c888b --- /dev/null +++ b/anchor/validator_store/src/testing/aggregation_timing.rs @@ -0,0 +1,258 @@ +//! Regression coverage for issue #1292 at the first Gloas slot. +//! +//! These tests exercise Anchor's actual consensus callers and Phase 3 scheduler. The scheduler +//! test supplies assignments directly, so it does not cover beacon API fetching or duty discovery. + +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use bls::{FixedBytesExtended, Signature}; +use fork::Fork; +use futures::{FutureExt, StreamExt}; +use qbft_manager::TimeoutMode; +use slot_clock::{ManualSlotClock, SlotClock}; +use ssv_types::{ + OperatorId, + consensus::{AggregatorCommitteeConsensusData, AssignedAggregator}, +}; +use ssz_types::VariableList; +use tokio::time::Instant; +use types::{Attestation, Checkpoint, Epoch, EthSpec, Hash256, MainnetEthSpec, Slot}; +use validator_store::ValidatorStore; + +use super::common::*; +use crate::{AggregationAssignments, metadata_service::run_aggregation_publisher}; + +const OUR_OPERATOR_ID: OperatorId = OperatorId(1); +const COMMITTEE_INDEX: usize = 0; +const VALIDATOR_INDEX: usize = 0; +const VALIDATOR_COUNT: usize = 1; +const SYNC_SUBCOMMITTEE: u64 = 0; +const GLOAS_EPOCH: Epoch = Epoch::new(1); +const PRE_GLOAS_OFFSET: Duration = Duration::from_millis(8_000); +const GLOAS_OFFSET: Duration = Duration::from_millis(6_000); +const BEFORE_DEADLINE: Duration = Duration::from_millis(1); + +fn boundary_slots() -> [(Slot, Duration); 2] { + let first_gloas_slot = GLOAS_EPOCH.start_slot(MainnetEthSpec::slots_per_epoch()); + [ + (first_gloas_slot - 1, PRE_GLOAS_OFFSET), + (first_gloas_slot, GLOAS_OFFSET), + ] +} + +fn timing_harness(active_fork: Fork) -> ValidatorStoreTestHarness { + ValidatorStoreTestHarness::new_with_options( + vec![create_primary_committee_setup(VALIDATOR_COUNT)], + OUR_OPERATOR_ID, + HarnessOptions { + spec: gloas_at_epoch_spec(GLOAS_EPOCH), + active_fork, + ..Default::default() + }, + ) +} + +/// Legacy aggregate callbacks must use the duty slot's fork for their cumulative round origin. +#[tokio::test(start_paused = true)] +async fn legacy_aggregate_origin_switches_at_gloas_boundary() { + for (slot, expected_offset) in boundary_slots() { + // Arrange: use the same mainnet spec on both sides of the fork boundary. + let harness = timing_harness(Fork::Alan); + harness.slot_clock.set_slot(slot.as_u64()); + let mut aggregate = harness.create_aggregate(COMMITTEE_INDEX, VALIDATOR_INDEX); + aggregate.aggregate = Attestation::empty_for_signing( + COMMITTEE_INDEX as u64, + VALIDATOR_COUNT, + slot, + Hash256::zero(), + Checkpoint::default(), + Checkpoint { + epoch: slot.epoch(MainnetEthSpec::slots_per_epoch()), + root: Hash256::zero(), + }, + false, + &harness.spec, + ) + .expect("test aggregate should match the duty slot's fork"); + let slot_start = Instant::now(); + + // Act: run the real Lighthouse callback through signing and the consensus decider. + let results = harness.collect_aggregates(vec![aggregate]).await; + + // Assert: completion and the exact origin supplied to QBFT, without invoking a getter + // to calculate the expected value. + assert_eq!(results.len(), 1); + assert_eq!(results[0].as_ref().expect("aggregate should sign").len(), 1); + assert_eq!( + harness.captured_consensus_timeouts.lock().as_slice(), + [TimeoutMode::SlotTime { + round_deadline_origin: slot_start + expected_offset, + }], + "legacy aggregate origin is wrong for slot {slot}", + ); + } +} + +/// Boole's shared committee decision must use the same origin as its Phase 3 scheduler. +#[tokio::test(start_paused = true)] +async fn boole_aggregator_committee_origin_switches_at_gloas_boundary() { + for (slot, expected_offset) in boundary_slots() { + // Arrange: one selected sync contributor gives the committee a valid worklist. + let harness = timing_harness(Fork::Boole); + harness.slot_clock.set_slot(slot.as_u64()); + let validator = harness.validator_metadata(COMMITTEE_INDEX, VALIDATOR_INDEX); + let (_, cluster) = harness + .validator_store + .get_validator_and_cluster(validator.public_key) + .expect("test validator should belong to its cluster"); + let mut contribution = + harness.create_contribution(COMMITTEE_INDEX, VALIDATOR_INDEX, SYNC_SUBCOMMITTEE); + contribution.contribution.slot = slot; + let data = AggregatorCommitteeConsensusData { + version: harness + .spec + .fork_name_at_slot::(slot) + .into(), + aggregators: VariableList::empty(), + aggregator_committee_indexes: VariableList::empty(), + aggregated_attestations: VariableList::empty(), + contributors: VariableList::new(vec![AssignedAggregator { + validator_index: validator + .index + .expect("test validator should have an index"), + selection_proof: Signature::empty(), + committee_index: SYNC_SUBCOMMITTEE, + }]) + .expect("one contributor should fit"), + sync_committee_contributions: VariableList::new(vec![contribution.contribution]) + .expect("one contribution should fit"), + }; + let slot_start = Instant::now(); + + // Act: call the consensus path used by the Boole post-consensus execution. + let decided = harness + .validator_store + .run_aggregator_committee_consensus(cluster.committee_id(), slot, &cluster, &data) + .await + .expect("committee consensus should succeed"); + + // Assert: both the returned worklist and the captured origin come from the real caller. + assert_eq!(decided, data); + assert_eq!( + harness.captured_consensus_timeouts.lock().as_slice(), + [TimeoutMode::SlotTime { + round_deadline_origin: slot_start + expected_offset, + }], + "Boole committee origin is wrong for slot {slot}", + ); + } +} + +/// The scheduler is armed before the fork, so reading the current slot's offset instead of the +/// upcoming slot's offset would leave the first Gloas contribution waiting until eight seconds. +/// Driving both clocks together also checks that the published metadata belongs to the new slot. +#[tokio::test(start_paused = true)] +async fn phase3_scheduler_releases_legacy_contributions_at_the_gloas_boundary() { + // Arrange: start one slot before the last pre-Gloas duty, with no assignments cached. + let harness = timing_harness(Fork::Alan); + let [(last_pre_gloas_slot, _), _] = boundary_slots(); + harness + .slot_clock + .set_slot((last_pre_gloas_slot - 1).as_u64()); + let publisher_store = Arc::clone(&harness.validator_store); + let publisher_clock = harness.slot_clock.clone(); + let publisher = tokio::spawn(run_aggregation_publisher::( + harness.slot_clock.clone(), + Arc::clone(&harness.spec), + move || { + let store = Arc::clone(&publisher_store); + let slot = publisher_clock + .now() + .expect("manual clock should be readable"); + async move { + let executions = store.update_aggregation_assignments(AggregationAssignments { + slot, + aggregator_committees: HashMap::new(), + multi_sync_aggregators: HashMap::new(), + consensus_data_by_ssv_committee: HashMap::new(), + }); + assert!( + executions.is_empty(), + "legacy metadata has no Boole worklist" + ); + } + }, + )); + tokio::task::yield_now().await; + + for (index, (slot, expected_offset)) in boundary_slots().into_iter().enumerate() { + let to_slot_start = harness + .slot_clock + .duration_to_next_slot() + .expect("manual clock should have a next slot"); + advance_in_lockstep(&harness.slot_clock, to_slot_start).await; + let slot_start = Instant::now(); + let mut contribution = + harness.create_contribution(COMMITTEE_INDEX, VALIDATOR_INDEX, SYNC_SUBCOMMITTEE); + contribution.contribution.slot = slot; + let callback_store = Arc::clone(&harness.validator_store); + let callback = tokio::spawn(async move { + callback_store + .sign_sync_committee_contributions(vec![contribution]) + .collect::() + .await + }); + tokio::task::yield_now().await; + + // Act: reach just before the expected trigger, then cross it by one millisecond. + advance_in_lockstep(&harness.slot_clock, expected_offset - BEFORE_DEADLINE).await; + + // Assert: the real contribution callback still awaits Phase 3 metadata before the trigger. + assert!( + !callback.is_finished(), + "slot {slot} contribution started early" + ); + assert_eq!(harness.captured_consensus_timeouts.lock().len(), index); + assert!( + harness + .validator_store + .get_aggregation_assignments(slot) + .now_or_never() + .is_none(), + "slot {slot} assignments were published before their deadline", + ); + + advance_in_lockstep(&harness.slot_clock, BEFORE_DEADLINE).await; + let assignments = harness + .validator_store + .get_aggregation_assignments(slot) + .now_or_never() + .expect("Phase 3 must publish at the duty slot's aggregation deadline") + .expect("published assignments should match the requested slot"); + assert_eq!(assignments.slot, slot); + let results = callback + .await + .expect("contribution callback should complete"); + assert_eq!(results.len(), 1); + assert_eq!( + results[0].as_ref().expect("contribution should sign").len(), + 1 + ); + assert_eq!( + harness.captured_consensus_timeouts.lock()[index], + TimeoutMode::SlotTime { + round_deadline_origin: slot_start + expected_offset, + }, + "legacy contribution origin is wrong for slot {slot}", + ); + } + + publisher.abort(); +} + +/// Wall time moves first so a woken scheduler reads the slot that the virtual timer reached. +async fn advance_in_lockstep(slot_clock: &ManualSlotClock, duration: Duration) { + slot_clock.advance_time(duration); + tokio::time::advance(duration).await; + tokio::task::yield_now().await; +} diff --git a/anchor/validator_store/src/testing/common.rs b/anchor/validator_store/src/testing/common.rs index 026c6ee00..18dfb9956 100644 --- a/anchor/validator_store/src/testing/common.rs +++ b/anchor/validator_store/src/testing/common.rs @@ -110,6 +110,7 @@ pub(super) async fn run_sign_attestations( pub(super) struct MockConsensusDecider { forced_gloas_index: Option, fixed_decision: Option<(Vec, Arc)>, + captured_timeouts: Arc>>, } impl MockConsensusDecider { @@ -142,9 +143,10 @@ impl ConsensusDecider for MockConsensusDecider { _id: D::Id, initial: D, _validator: Box>, - _timeout_mode: TimeoutMode, + timeout_mode: TimeoutMode, _committee_members: &IndexSet, ) -> Result, QbftError> { + self.captured_timeouts.lock().push(timeout_mode); // `D: QbftDecidable` requires `'static`, so this downcast is sound. Only the Gloas // seed type carries `attestation_data_index`; every other `D` falls through to the echo. if let Some(index) = self.forced_gloas_index { @@ -534,6 +536,8 @@ pub(super) struct ValidatorStoreTestHarness { committee_setups: Vec, pub(super) captured_calls: CapturedCalls, pub(super) captured_disseminations: CapturedDisseminations, + /// Timeout origins supplied by the real signing and committee consensus callers. + pub(super) captured_consensus_timeouts: Arc>>, /// The dissemination handoff store the store awaits on; tests insert into it to stand in /// for the message receiver. pub(super) dissemination_store: Arc, @@ -716,6 +720,7 @@ impl ValidatorStoreTestHarness { let (is_synced_tx, is_synced_rx) = watch::channel(true); let decider = options.decider; + let captured_consensus_timeouts = Arc::clone(&decider.captured_timeouts); let spec = Arc::clone(&options.spec); let genesis_validators_root = Hash256::zero(); @@ -746,6 +751,7 @@ impl ValidatorStoreTestHarness { committee_setups, captured_calls, captured_disseminations, + captured_consensus_timeouts, dissemination_store, signature_collection_fails, signature_collection_hangs, diff --git a/anchor/validator_store/src/testing/mod.rs b/anchor/validator_store/src/testing/mod.rs index 6a1ff7fb8..b03bb3649 100644 --- a/anchor/validator_store/src/testing/mod.rs +++ b/anchor/validator_store/src/testing/mod.rs @@ -1,5 +1,6 @@ mod common; +mod aggregation_timing; mod aggregator_post_consensus; mod committee_aggregate; mod committee_attestation;