diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 0ee9dbb622..0f91c86617 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -65,8 +65,7 @@ jobs: - name: Install dependencies run: apt update && apt install -y cmake libclang-dev - name: Check for deadlocks - run: | - cargo lockbud -k deadlock -b -l tokio_util + run: ./scripts/ci/check-lockbud.sh target-branch-check: name: target-branch-check diff --git a/Cargo.lock b/Cargo.lock index 20d2548d09..c51e3583d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -46,7 +46,7 @@ dependencies = [ "eth2_keystore", "eth2_wallet", "filesystem", - "rand", + "rand 0.8.5", "regex", "rpassword", "serde", @@ -135,7 +135,7 @@ dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -204,9 +204,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec878088ec6283ce1e90d280316aadd3d6ce3de06ff63d68953c855e7e447e92" +checksum = "bc1360603efdfba91151e623f13a4f4d3dc4af4adc1cbd90bf37c81e84db4c77" dependencies = [ "alloy-rlp", "arbitrary", @@ -225,7 +225,7 @@ dependencies = [ "paste", "proptest", "proptest-derive", - "rand", + "rand 0.8.5", "ruint", "rustc-hash 2.1.0", "serde", @@ -252,7 +252,7 @@ checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -467,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -477,7 +477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -522,7 +522,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -534,7 +534,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -543,6 +543,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -592,18 +602,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -666,7 +676,7 @@ checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -821,7 +831,7 @@ dependencies = [ "operation_pool", "parking_lot 0.12.3", "proto_array", - "rand", + "rand 0.8.5", "rayon", "safe_arith", "sensitive_url", @@ -850,7 +860,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "6.0.1" +version = "7.0.0-beta.0" dependencies = [ "account_utils", "beacon_chain", @@ -884,10 +894,12 @@ dependencies = [ name = "beacon_node_fallback" version = "0.1.0" dependencies = [ + "clap", "environment", "eth2", "futures", "itertools 0.10.5", + "logging", "serde", "slog", "slot_clock", @@ -895,6 +907,7 @@ dependencies = [ "tokio", "types", "validator_metrics", + "validator_test_rig", ] [[package]] @@ -947,7 +960,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.96", + "syn 2.0.98", "which", ] @@ -1048,7 +1061,7 @@ dependencies = [ "ethereum_ssz", "fixed_bytes", "hex", - "rand", + "rand 0.8.5", "safe_arith", "serde", "tree_hash", @@ -1078,7 +1091,7 @@ dependencies = [ "ff 0.13.0", "group 0.13.0", "pairing", - "rand_core", + "rand_core 0.6.4", "serde", "subtle", ] @@ -1095,7 +1108,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "6.0.1" +version = "7.0.0-beta.0" dependencies = [ "beacon_node", "bytes", @@ -1136,10 +1149,12 @@ name = "builder_client" version = "0.1.0" dependencies = [ "eth2", + "ethereum_ssz", "lighthouse_version", "reqwest", "sensitive_url", "serde", + "serde_json", ] [[package]] @@ -1245,9 +1260,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.10" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "jobserver", "libc", @@ -1401,7 +1416,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1482,6 +1497,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "compare_fields" version = "0.2.0" @@ -1574,9 +1599,9 @@ dependencies = [ [[package]] name = "crate_crypto_internal_eth_kzg_bls12_381" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23be5253f1bd7fd411721a58712308c4747d0a41d040bbf8ebb78d52909a480" +checksum = "48603155907d588e487aea229f61a28d9a918c95c9aa987055ba29502225810b" dependencies = [ "blst", "blstrs", @@ -1588,9 +1613,9 @@ dependencies = [ [[package]] name = "crate_crypto_internal_eth_kzg_erasure_codes" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2067ce20ef380ff33a93ce0af62bea22d35531b7f3586224d8d5176ec6cf578" +checksum = "cdf616e4b4f1799191bb1e70b8a29f65e95ab5d74c59972a34998de488d01efd" dependencies = [ "crate_crypto_internal_eth_kzg_bls12_381", "crate_crypto_internal_eth_kzg_polynomial", @@ -1598,24 +1623,24 @@ dependencies = [ [[package]] name = "crate_crypto_internal_eth_kzg_maybe_rayon" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558f50324ff016e5fe93113c78a72776d790d52f244ae9602a8013a67a189b66" +checksum = "f1ddd0330f34f0b92a9f0b29bc3f8494b30d596ab8b951233ec90b2d72ab132c" [[package]] name = "crate_crypto_internal_eth_kzg_polynomial" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e051c4f5aa5696bd7c504930485436ec62bf14f30a4c2d78504f3f8ec6a3daf" +checksum = "7488314261926373e1c20121c404fabf5b57ca09f48eddc7fef38be1df79a006" dependencies = [ "crate_crypto_internal_eth_kzg_bls12_381", ] [[package]] name = "crate_crypto_kzg_multi_open_fk20" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ed6bf8993d9f3b361da4ed38f067503e08c0b948af0d6f4bb941dd647c0f2c" +checksum = "d24efdb64e7518848f11069dd9de23bd04455146a9fd5486345d99ed8bfdb049" dependencies = [ "crate_crypto_internal_eth_kzg_bls12_381", "crate_crypto_internal_eth_kzg_maybe_rayon", @@ -1716,7 +1741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1728,7 +1753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1740,7 +1765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -1816,7 +1841,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1864,7 +1889,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1886,7 +1911,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1938,7 +1963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" dependencies = [ "data-encoding", - "syn 2.0.96", + "syn 1.0.109", ] [[package]] @@ -2052,7 +2077,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2065,7 +2090,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2085,15 +2110,15 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "unicode-xid", ] [[package]] name = "diesel" -version = "2.2.6" +version = "2.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12" +checksum = "04001f23ba8843dc315804fa324000376084dfb1c30794ff68dd279e6e5696d5" dependencies = [ "bitflags 2.8.0", "byteorder", @@ -2113,7 +2138,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2133,7 +2158,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" dependencies = [ - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2231,7 +2256,7 @@ dependencies = [ "more-asserts", "multiaddr", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "smallvec", "socket2", "tokio", @@ -2248,7 +2273,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2270,16 +2295,16 @@ dependencies = [ [[package]] name = "dsl_auto_type" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" dependencies = [ "darling 0.20.10", "either", "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2338,7 +2363,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2 0.10.8", "subtle", @@ -2396,7 +2421,7 @@ dependencies = [ "ff 0.12.1", "generic-array", "group 0.12.1", - "rand_core", + "rand_core 0.6.4", "sec1 0.3.0", "subtle", "zeroize", @@ -2416,7 +2441,7 @@ dependencies = [ "group 0.13.0", "pem-rfc7468", "pkcs8 0.10.2", - "rand_core", + "rand_core 0.6.4", "sec1 0.7.3", "subtle", "zeroize", @@ -2444,7 +2469,7 @@ dependencies = [ "hex", "k256 0.13.4", "log", - "rand", + "rand 0.8.5", "serde", "sha3 0.10.8", "zeroize", @@ -2459,7 +2484,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2646,7 +2671,7 @@ dependencies = [ "hex", "hmac 0.11.0", "pbkdf2 0.8.0", - "rand", + "rand 0.8.5", "scrypt", "serde", "serde_json", @@ -2688,7 +2713,7 @@ dependencies = [ "eth2_key_derivation", "eth2_keystore", "hex", - "rand", + "rand 0.8.5", "serde", "serde_json", "serde_repr", @@ -2840,7 +2865,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2919,7 +2944,7 @@ dependencies = [ "k256 0.11.6", "once_cell", "open-fastrlp", - "rand", + "rand 0.8.5", "rlp", "rlp-derive", "serde", @@ -3056,7 +3081,7 @@ dependencies = [ "metrics", "parking_lot 0.12.3", "pretty_reqwest_error", - "rand", + "rand 0.8.5", "reqwest", "sensitive_url", "serde", @@ -3146,7 +3171,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3157,7 +3182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "bitvec 1.0.1", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3198,7 +3223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -3210,7 +3235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -3385,7 +3410,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3395,7 +3420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.21", + "rustls 0.23.22", "rustls-pki-types", ] @@ -3551,7 +3576,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3593,7 +3618,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "quickcheck", - "rand", + "rand 0.8.5", "regex", "serde", "sha2 0.10.8", @@ -3621,7 +3646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff 0.12.1", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3632,8 +3657,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff 0.13.0", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.4", "rand_xorshift", "subtle", ] @@ -3840,9 +3865,9 @@ checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" [[package]] name = "hickory-proto" -version = "0.25.0-alpha.4" +version = "0.25.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d063c0692ee669aa6d261988aa19ca5510f1cc40e4f211024f50c888499a35d7" +checksum = "1d00147af6310f4392a31680db52a3ed45a2e0f68eb18e8c3fe5537ecc96d9e2" dependencies = [ "async-recursion", "async-trait", @@ -3855,7 +3880,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand", + "rand 0.9.0", "socket2", "thiserror 2.0.11", "tinyvec", @@ -3877,7 +3902,7 @@ dependencies = [ "moka", "once_cell", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror 2.0.11", @@ -4038,7 +4063,7 @@ dependencies = [ "operation_pool", "parking_lot 0.12.3", "proto_array", - "rand", + "rand 0.8.5", "safe_arith", "sensitive_url", "serde", @@ -4327,7 +4352,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4405,7 +4430,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "log", - "rand", + "rand 0.8.5", "tokio", "url", "xmltree", @@ -4426,7 +4451,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "log", - "rand", + "rand 0.8.5", "tokio", "url", "xmltree", @@ -4485,7 +4510,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4528,7 +4553,7 @@ dependencies = [ "lockfile", "metrics", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -4756,11 +4781,6 @@ version = "0.1.0" dependencies = [ "arbitrary", "c-kzg", - "crate_crypto_internal_eth_kzg_bls12_381", - "crate_crypto_internal_eth_kzg_erasure_codes", - "crate_crypto_internal_eth_kzg_maybe_rayon", - "crate_crypto_internal_eth_kzg_polynomial", - "crate_crypto_kzg_multi_open_fk20", "criterion", "derivative", "ethereum_hashing", @@ -4791,7 +4811,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "6.0.1" +version = "7.0.0-beta.0" dependencies = [ "account_utils", "beacon_chain", @@ -4885,7 +4905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -4981,7 +5001,7 @@ dependencies = [ "parking_lot 0.12.3", "pin-project", "quick-protobuf", - "rand", + "rand 0.8.5", "rw-stream-sink", "thiserror 2.0.11", "tracing", @@ -5040,7 +5060,7 @@ dependencies = [ "multihash", "p256", "quick-protobuf", - "rand", + "rand 0.8.5", "sec1 0.7.3", "sha2 0.10.8", "thiserror 1.0.69", @@ -5060,7 +5080,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand", + "rand 0.8.5", "smallvec", "socket2", "tokio", @@ -5096,7 +5116,7 @@ dependencies = [ "libp2p-identity", "nohash-hasher", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "smallvec", "tracing", "unsigned-varint 0.8.0", @@ -5117,7 +5137,7 @@ dependencies = [ "multihash", "once_cell", "quick-protobuf", - "rand", + "rand 0.8.5", "snow", "static_assertions", "thiserror 2.0.11", @@ -5155,9 +5175,9 @@ dependencies = [ "libp2p-identity", "libp2p-tls", "quinn", - "rand", + "rand 0.8.5", "ring 0.17.8", - "rustls 0.23.21", + "rustls 0.23.22", "socket2", "thiserror 2.0.11", "tokio", @@ -5180,7 +5200,7 @@ dependencies = [ "lru", "multistream-select", "once_cell", - "rand", + "rand 0.8.5", "smallvec", "tokio", "tracing", @@ -5196,7 +5216,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5227,7 +5247,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.17.8", - "rustls 0.23.21", + "rustls 0.23.22", "rustls-webpki 0.101.7", "thiserror 2.0.11", "x509-parser", @@ -5287,7 +5307,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand", + "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", @@ -5346,7 +5366,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "6.0.1" +version = "7.0.0-beta.0" dependencies = [ "account_manager", "account_utils", @@ -5412,6 +5432,7 @@ dependencies = [ "libp2p", "libp2p-mplex", "lighthouse_version", + "local-ip-address", "logging", "lru", "lru_cache", @@ -5420,7 +5441,7 @@ dependencies = [ "prometheus-client", "quickcheck", "quickcheck_macros", - "rand", + "rand 0.8.5", "regex", "serde", "sha2 0.9.9", @@ -5492,6 +5513,18 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "local-ip-address" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3669cf5561f8d27e8fc84cc15e58350e70f557d4d65f70e3154e54cd2f8e1782" +dependencies = [ + "libc", + "neli", + "thiserror 1.0.69", + "windows-sys 0.59.0", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -5796,6 +5829,30 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9366861eb2a2c436c20b12c8dbec5f798cea6b47ad99216be0282942e2c81ea0" +[[package]] +name = "mockito" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "log", + "rand 0.8.5", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + [[package]] name = "moka" version = "0.12.10" @@ -5911,6 +5968,31 @@ dependencies = [ "tempfile", ] +[[package]] +name = "neli" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9" +dependencies = [ + "byteorder", + "libc", + "log", + "neli-proc-macros", +] + +[[package]] +name = "neli-proc-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -5999,6 +6081,7 @@ dependencies = [ "hex", "igd-next 0.16.0", "itertools 0.10.5", + "k256 0.13.4", "kzg", "lighthouse_network", "logging", @@ -6007,7 +6090,8 @@ dependencies = [ "metrics", "operation_pool", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", + "rand_chacha 0.3.1", "serde_json", "slog", "slog-async", @@ -6132,7 +6216,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "serde", "smallvec", "zeroize", @@ -6254,9 +6338,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.69" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -6275,7 +6359,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6295,9 +6379,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", @@ -6319,7 +6403,7 @@ dependencies = [ "maplit", "metrics", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "rayon", "serde", "state_processing", @@ -6468,7 +6552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -6580,7 +6664,7 @@ checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6701,9 +6785,9 @@ checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "postgres-protocol" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" +checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" dependencies = [ "base64 0.22.1", "byteorder", @@ -6712,16 +6796,16 @@ dependencies = [ "hmac 0.12.1", "md-5", "memchr", - "rand", + "rand 0.9.0", "sha2 0.10.8", "stringprep", ] [[package]] name = "postgres-types" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" +checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" dependencies = [ "bytes", "fallible-iterator", @@ -6740,15 +6824,16 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] name = "pq-sys" -version = "0.6.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc05d7ea95200187117196eee9edd0644424911821aeb28a18ce60ea0b8793" +checksum = "30b51d65ebe1cb1f40641b15abae017fed35ccdda46e3dab1ff8768f625a3222" dependencies = [ + "libc", "vcpkg", ] @@ -6767,7 +6852,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6822,7 +6907,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.22.22", + "toml_edit 0.22.23", ] [[package]] @@ -6884,7 +6969,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6898,8 +6983,8 @@ dependencies = [ "bitflags 2.8.0", "lazy_static", "num-traits", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax 0.8.5", "rusty-fork", @@ -6915,7 +7000,7 @@ checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6991,7 +7076,7 @@ checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger 0.8.4", "log", - "rand", + "rand 0.8.5", ] [[package]] @@ -7017,7 +7102,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.0", - "rustls 0.23.21", + "rustls 0.23.22", "socket2", "thiserror 2.0.11", "tokio", @@ -7032,10 +7117,10 @@ checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", "getrandom 0.2.15", - "rand", + "rand 0.8.5", "ring 0.17.8", "rustc-hash 2.1.0", - "rustls 0.23.21", + "rustls 0.23.22", "rustls-pki-types", "slab", "thiserror 2.0.11", @@ -7107,11 +7192,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", "serde", ] +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.14", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -7119,7 +7215,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", ] [[package]] @@ -7131,13 +7237,23 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.14", +] + [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -7460,7 +7576,7 @@ dependencies = [ "parity-scale-codec 3.6.12", "primitive-types 0.12.2", "proptest", - "rand", + "rand 0.8.5", "rlp", "ruint-macro", "serde", @@ -7490,9 +7606,9 @@ dependencies = [ [[package]] name = "rust_eth_kzg" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3291fd0d9c629a56537d74bbc1e7bcaf5be610f2f7b55af85c4fea843c6aeca3" +checksum = "a237a478ee68e491a0f40bbcbb958b79ba9b37aacce459f7ab3ba78f3cbfa9d0" dependencies = [ "crate_crypto_internal_eth_kzg_bls12_381", "crate_crypto_internal_eth_kzg_erasure_codes", @@ -7608,9 +7724,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" dependencies = [ "once_cell", "ring 0.17.8", @@ -7746,7 +7862,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -7926,7 +8042,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -7959,7 +8075,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -8116,7 +8232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -8126,7 +8242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -8145,6 +8261,12 @@ dependencies = [ "validator_metrics", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "simple_asn1" version = "0.6.3" @@ -8210,7 +8332,7 @@ dependencies = [ "maplit", "metrics", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "rayon", "redb", "safe_arith", @@ -8394,7 +8516,7 @@ dependencies = [ "blake2", "chacha20poly1305", "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "ring 0.17.8", "rustc_version 0.4.1", "sha2 0.10.8", @@ -8484,7 +8606,7 @@ dependencies = [ "itertools 0.10.5", "merkle_proof", "metrics", - "rand", + "rand 0.8.5", "rayon", "safe_arith", "smallvec", @@ -8529,7 +8651,7 @@ dependencies = [ "lru", "metrics", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "redb", "safe_arith", "serde", @@ -8633,9 +8755,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -8662,7 +8784,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -8849,7 +8971,7 @@ dependencies = [ "hex", "hmac 0.12.1", "log", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2 0.10.8", @@ -8881,7 +9003,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -8892,7 +9014,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -8997,7 +9119,7 @@ dependencies = [ "hmac 0.12.1", "once_cell", "pbkdf2 0.11.0", - "rand", + "rand 0.8.5", "rustc-hash 1.1.0", "sha2 0.10.8", "thiserror 1.0.69", @@ -9060,6 +9182,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -9085,7 +9208,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -9100,9 +9223,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" +checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" dependencies = [ "async-trait", "byteorder", @@ -9117,7 +9240,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand", + "rand 0.9.0", "socket2", "tokio", "tokio-util", @@ -9190,7 +9313,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.22", + "toml_edit 0.22.23", ] [[package]] @@ -9215,15 +9338,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.25", + "winnow 0.7.0", ] [[package]] @@ -9286,7 +9409,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -9377,7 +9500,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -9442,7 +9565,7 @@ dependencies = [ "milhouse", "parking_lot 0.12.3", "paste", - "rand", + "rand 0.8.5", "rand_xorshift", "rayon", "regex", @@ -9693,7 +9816,7 @@ dependencies = [ "filesystem", "hex", "lockfile", - "rand", + "rand 0.8.5", "tempfile", "tree_hash", "types", @@ -9722,7 +9845,7 @@ dependencies = [ "lighthouse_version", "logging", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "sensitive_url", "serde", "signing_method", @@ -9838,6 +9961,20 @@ dependencies = [ "validator_metrics", ] +[[package]] +name = "validator_test_rig" +version = "0.1.0" +dependencies = [ + "eth2", + "logging", + "mockito", + "regex", + "sensitive_url", + "serde_json", + "slog", + "types", +] + [[package]] name = "valuable" version = "0.1.1" @@ -9985,7 +10122,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -10020,7 +10157,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -10082,7 +10219,7 @@ dependencies = [ "logging", "network", "r2d2", - "rand", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -10288,7 +10425,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -10299,7 +10436,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -10555,9 +10692,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.25" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310" +checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" dependencies = [ "memchr", ] @@ -10634,7 +10771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "serde", "zeroize", ] @@ -10667,7 +10804,7 @@ dependencies = [ "futures-util", "libc", "log", - "rand", + "rand 0.8.5", ] [[package]] @@ -10707,7 +10844,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.3", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", ] @@ -10722,7 +10859,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.3", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", "web-time", ] @@ -10756,7 +10893,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -10767,7 +10904,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", ] [[package]] @@ -10778,7 +10924,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -10798,7 +10955,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -10820,7 +10977,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -10842,7 +10999,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e30b6aa2b6..73912f6082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,8 +86,10 @@ members = [ "testing/simulator", "testing/state_transition_vectors", "testing/test-test_logger", + "testing/validator_test_rig", "testing/web3signer_tests", + "validator_client", "validator_client/beacon_node_fallback", "validator_client/doppelganger_service", @@ -132,14 +134,7 @@ delay_map = "0.4" derivative = "2" dirs = "3" either = "1.9" -# TODO: rust_eth_kzg is pinned for now while a perf regression is investigated -# The crate_crypto_* dependencies can be removed from this file completely once we update -rust_eth_kzg = "=0.5.1" -crate_crypto_internal_eth_kzg_bls12_381 = "=0.5.1" -crate_crypto_internal_eth_kzg_erasure_codes = "=0.5.1" -crate_crypto_internal_eth_kzg_maybe_rayon = "=0.5.1" -crate_crypto_internal_eth_kzg_polynomial = "=0.5.1" -crate_crypto_kzg_multi_open_fk20 = "=0.5.1" +rust_eth_kzg = "0.5.3" discv5 = { version = "0.9", features = ["libp2p"] } env_logger = "0.9" ethereum_hashing = "0.7.0" @@ -162,6 +157,7 @@ log = "0.4" lru = "0.12" maplit = "1" milhouse = "0.3" +mockito = "1.5.0" num_cpus = "1" parking_lot = "0.12" paste = "1" @@ -268,6 +264,7 @@ malloc_utils = { path = "common/malloc_utils" } merkle_proof = { path = "consensus/merkle_proof" } monitoring_api = { path = "common/monitoring_api" } network = { path = "beacon_node/network" } +node_test_rig = { path = "testing/node_test_rig" } operation_pool = { path = "beacon_node/operation_pool" } pretty_reqwest_error = { path = "common/pretty_reqwest_error" } proto_array = { path = "consensus/proto_array" } @@ -290,6 +287,7 @@ validator_http_api = { path = "validator_client/http_api" } validator_http_metrics = { path = "validator_client/http_metrics" } validator_metrics = { path = "validator_client/validator_metrics" } validator_store = { path = "validator_client/validator_store" } +validator_test_rig = { path = "testing/validator_test_rig" } warp_utils = { path = "common/warp_utils" } xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "50d63cdf1878e5cf3538e9aae5eed34a22c64e4a" } zstd = "0.13" diff --git a/Makefile b/Makefile index 81477634fe..f621f38a63 100644 --- a/Makefile +++ b/Makefile @@ -250,7 +250,7 @@ install-audit: cargo install --force cargo-audit audit-CI: - cargo audit --ignore RUSTSEC-2024-0421 + cargo audit # Runs `cargo vendor` to make sure dependencies can be vendored for packaging, reproducibility and archival purpose. vendor: diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 534939cf6b..44ec638a09 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -2,11 +2,8 @@ mod common; pub mod validator; pub mod wallet; -use clap::Arg; -use clap::ArgAction; use clap::ArgMatches; use clap::Command; -use clap_utils::FLAG_HEADER; use environment::Environment; use types::EthSpec; @@ -21,15 +18,6 @@ pub fn cli_app() -> Command { .visible_aliases(["a", "am", "account"]) .about("Utilities for generating and managing Ethereum 2.0 accounts.") .display_order(0) - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER), - ) .subcommand(wallet::cli_app()) .subcommand(validator::cli_app()) } diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 61584cbfbb..b699301cde 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -8,7 +8,6 @@ pub mod slashing_protection; use crate::{VALIDATOR_DIR_FLAG, VALIDATOR_DIR_FLAG_ALIAS}; use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; use directory::{parse_path_or_default_with_flag, DEFAULT_VALIDATOR_DIR}; use environment::Environment; use std::path::PathBuf; @@ -20,16 +19,6 @@ pub fn cli_app() -> Command { Command::new(CMD) .display_order(0) .about("Provides commands for managing Eth2 validators.") - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER) - .global(true), - ) .arg( Arg::new(VALIDATOR_DIR_FLAG) .long(VALIDATOR_DIR_FLAG) diff --git a/account_manager/src/wallet/mod.rs b/account_manager/src/wallet/mod.rs index c34f0363a4..f6f3bb0419 100644 --- a/account_manager/src/wallet/mod.rs +++ b/account_manager/src/wallet/mod.rs @@ -4,7 +4,6 @@ pub mod recover; use crate::WALLETS_DIR_FLAG; use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; use directory::{parse_path_or_default_with_flag, DEFAULT_WALLET_DIR}; use std::fs::create_dir_all; use std::path::PathBuf; @@ -15,16 +14,6 @@ pub fn cli_app() -> Command { Command::new(CMD) .about("Manage wallets, from which validator keys can be derived.") .display_order(0) - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER) - .global(true) - ) .arg( Arg::new(WALLETS_DIR_FLAG) .long(WALLETS_DIR_FLAG) diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 7da65ad742..f6948e8743 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "6.0.1" +version = "7.0.0-beta.0" authors = [ "Paul Hauner ", "Age Manning BeaconChain { let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?; - for effective_balance_eth in 1..=self.max_effective_balance_increment_steps()? { + for effective_balance_eth in + 1..=self.max_effective_balance_increment_steps(previous_epoch)? + { let effective_balance = effective_balance_eth.safe_mul(spec.effective_balance_increment)?; let base_reward = @@ -321,11 +323,14 @@ impl BeaconChain { }) } - fn max_effective_balance_increment_steps(&self) -> Result { + fn max_effective_balance_increment_steps( + &self, + rewards_epoch: Epoch, + ) -> Result { let spec = &self.spec; - let max_steps = spec - .max_effective_balance - .safe_div(spec.effective_balance_increment)?; + let fork_name = spec.fork_name_at_epoch(rewards_epoch); + let max_effective_balance = spec.max_effective_balance_for_fork(fork_name); + let max_steps = max_effective_balance.safe_div(spec.effective_balance_increment)?; Ok(max_steps) } @@ -386,7 +391,9 @@ impl BeaconChain { let mut ideal_attestation_rewards_list = Vec::new(); let sqrt_total_active_balance = SqrtTotalActiveBalance::new(total_balances.current_epoch()); - for effective_balance_step in 1..=self.max_effective_balance_increment_steps()? { + for effective_balance_step in + 1..=self.max_effective_balance_increment_steps(previous_epoch)? + { let effective_balance = effective_balance_step.safe_mul(spec.effective_balance_increment)?; let base_reward = diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index a69eb99a51..a70a2caa4f 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -60,9 +60,9 @@ use std::borrow::Cow; use strum::AsRefStr; use tree_hash::TreeHash; use types::{ - Attestation, AttestationRef, BeaconCommittee, BeaconStateError::NoCommitteeFound, ChainSpec, - CommitteeIndex, Epoch, EthSpec, Hash256, IndexedAttestation, SelectionProof, - SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, + Attestation, AttestationData, AttestationRef, BeaconCommittee, + BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256, + IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; @@ -115,6 +115,17 @@ pub enum Error { /// /// The peer has sent an invalid message. AggregatorNotInCommittee { aggregator_index: u64 }, + /// The `attester_index` for a `SingleAttestation` is not a member of the committee defined + /// by its `beacon_block_root`, `committee_index` and `slot`. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + AttesterNotInCommittee { + attester_index: u64, + committee_index: u64, + slot: Slot, + }, /// The aggregator index refers to a validator index that we have not seen. /// /// ## Peer scoring @@ -485,7 +496,11 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). // // We do not queue future attestations for later processing. - verify_propagation_slot_range(&chain.slot_clock, attestation, &chain.spec)?; + verify_propagation_slot_range::<_, T::EthSpec>( + &chain.slot_clock, + attestation.data(), + &chain.spec, + )?; // Check the attestation's epoch matches its target. if attestation.data().slot.epoch(T::EthSpec::slots_per_epoch()) @@ -817,7 +832,11 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). // // We do not queue future attestations for later processing. - verify_propagation_slot_range(&chain.slot_clock, attestation, &chain.spec)?; + verify_propagation_slot_range::<_, T::EthSpec>( + &chain.slot_clock, + attestation.data(), + &chain.spec, + )?; // Check to ensure that the attestation is "unaggregated". I.e., it has exactly one // aggregation bit set. @@ -1133,10 +1152,10 @@ fn verify_head_block_is_known( /// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. pub fn verify_propagation_slot_range( slot_clock: &S, - attestation: AttestationRef, + attestation: &AttestationData, spec: &ChainSpec, ) -> Result<(), Error> { - let attestation_slot = attestation.data().slot; + let attestation_slot = attestation.slot; let latest_permissible_slot = slot_clock .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)?; diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 26276457c0..a51e7873f3 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3129,10 +3129,9 @@ impl BeaconChain { /// Only completed sampling results are received. Blocks are unavailable by default and should /// be pruned on finalization, on a timeout or by a max count. pub async fn process_sampling_completed(self: &Arc, block_root: Hash256) { - // TODO(das): update fork-choice + // TODO(das): update fork-choice, act on sampling result, adjust log level // NOTE: It is possible that sampling complets before block is imported into fork choice, // in that case we may need to update availability cache. - // TODO(das): These log levels are too high, reduce once DAS matures info!(self.log, "Sampling completed"; "block_root" => %block_root); } diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index b8a607c886..fcdd57abbc 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -124,7 +124,7 @@ impl Default for ChainConfig { genesis_backfill: false, always_prepare_payload: false, epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION, - enable_light_client_server: false, + enable_light_client_server: true, malicious_withhold_count: 0, enable_sampling: false, blob_publication_batches: 4, diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index aa4689121c..f10d59ca1a 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -27,8 +27,8 @@ mod overflow_lru_cache; mod state_lru_cache; use crate::data_column_verification::{ - verify_kzg_for_data_column, verify_kzg_for_data_column_list, CustodyDataColumn, - GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, + verify_kzg_for_data_column_list_with_scoring, CustodyDataColumn, GossipVerifiedDataColumn, + KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, }; use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, @@ -230,19 +230,14 @@ impl DataAvailabilityChecker { block_root: Hash256, custody_columns: DataColumnSidecarList, ) -> Result, AvailabilityCheckError> { - // TODO(das): report which column is invalid for proper peer scoring - // TODO(das): batch KZG verification here, but fallback into checking each column - // individually to report which column(s) are invalid. - let verified_custody_columns = custody_columns + // Attributes fault to the specific peer that sent an invalid column + let kzg_verified_columns = KzgVerifiedDataColumn::from_batch(custody_columns, &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + + let verified_custody_columns = kzg_verified_columns .into_iter() - .map(|column| { - let index = column.index; - Ok(KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::new(column, &self.kzg) - .map_err(|e| AvailabilityCheckError::InvalidColumn(index, e))?, - )) - }) - .collect::, AvailabilityCheckError>>()?; + .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) + .collect::>(); self.availability_cache.put_kzg_verified_data_columns( block_root, @@ -365,7 +360,8 @@ impl DataAvailabilityChecker { .iter() .map(|custody_column| custody_column.as_data_column()), &self.kzg, - )?; + ) + .map_err(AvailabilityCheckError::InvalidColumn)?; Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, @@ -432,8 +428,9 @@ impl DataAvailabilityChecker { // verify kzg for all data columns at once if !all_data_columns.is_empty() { - // TODO: Need to also attribute which specific block is faulty - verify_kzg_for_data_column_list_with_scoring(all_data_columns.iter(), &self.kzg)?; + // Attributes fault to the specific peer that sent an invalid column + verify_kzg_for_data_column_list_with_scoring(all_data_columns.iter(), &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; } for block in blocks { @@ -716,32 +713,6 @@ async fn availability_cache_maintenance_service( } } -fn verify_kzg_for_data_column_list_with_scoring<'a, E: EthSpec, I>( - data_column_iter: I, - kzg: &'a Kzg, -) -> Result<(), AvailabilityCheckError> -where - I: Iterator>> + Clone, -{ - let Err(batch_err) = verify_kzg_for_data_column_list(data_column_iter.clone(), kzg) else { - return Ok(()); - }; - - let data_columns = data_column_iter.collect::>(); - // Find which column is invalid. If len is 1 or 0 continue to default case below. - // If len > 1 at least one column MUST fail. - if data_columns.len() > 1 { - for data_column in data_columns { - if let Err(e) = verify_kzg_for_data_column(data_column.clone(), kzg) { - return Err(AvailabilityCheckError::InvalidColumn(data_column.index, e)); - } - } - } - - // len 0 should never happen - Err(AvailabilityCheckError::InvalidColumn(0, batch_err)) -} - /// A fully available block that is ready to be imported into fork choice. #[derive(Clone, Debug, PartialEq)] pub struct AvailableBlock { diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index cfdb3cfe91..1ab85ab105 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -4,7 +4,7 @@ use types::{BeaconStateError, ColumnIndex, Hash256}; #[derive(Debug)] pub enum Error { InvalidBlobs(KzgError), - InvalidColumn(ColumnIndex, KzgError), + InvalidColumn(Vec<(ColumnIndex, KzgError)>), ReconstructColumnsError(KzgError), KzgCommitmentMismatch { blob_commitment: KzgCommitment, diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index cd793c8394..7592ffd149 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -110,15 +110,6 @@ impl PendingComponents { self.get_cached_blobs().iter().flatten().count() } - /// Checks if a data column of a given index exists in the cache. - /// - /// Returns: - /// - `true` if a data column for the given index exists. - /// - `false` otherwise. - fn data_column_exists(&self, data_column_index: u64) -> bool { - self.get_cached_data_column(data_column_index).is_some() - } - /// Returns the number of data columns that have been received and are stored in the cache. pub fn num_received_data_columns(&self) -> usize { self.verified_data_columns.len() @@ -182,8 +173,7 @@ impl PendingComponents { kzg_verified_data_columns: I, ) -> Result<(), AvailabilityCheckError> { for data_column in kzg_verified_data_columns { - // TODO(das): Add equivalent checks for data columns if necessary - if !self.data_column_exists(data_column.index()) { + if self.get_cached_data_column(data_column.index()).is_none() { self.verified_data_columns.push(data_column); } } diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 565e76704e..1262fcdeb8 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -239,6 +239,18 @@ impl KzgVerifiedDataColumn { pub fn new(data_column: Arc>, kzg: &Kzg) -> Result { verify_kzg_for_data_column(data_column, kzg) } + + pub fn from_batch( + data_columns: Vec>>, + kzg: &Kzg, + ) -> Result, Vec<(ColumnIndex, KzgError)>> { + verify_kzg_for_data_column_list_with_scoring(data_columns.iter(), kzg)?; + Ok(data_columns + .into_iter() + .map(|column| Self { data: column }) + .collect()) + } + pub fn to_data_column(self) -> Arc> { self.data } @@ -378,6 +390,38 @@ where Ok(()) } +/// Complete kzg verification for a list of `DataColumnSidecar`s. +/// +/// If there's at least one invalid column, it re-verifies all columns individually to identify the +/// first column that is invalid. This is necessary to attribute fault to the specific peer that +/// sent bad data. The re-verification cost should not be significant. If a peer sends invalid data it +/// will be quickly banned. +pub fn verify_kzg_for_data_column_list_with_scoring<'a, E: EthSpec, I>( + data_column_iter: I, + kzg: &'a Kzg, +) -> Result<(), Vec<(ColumnIndex, KzgError)>> +where + I: Iterator>> + Clone, +{ + if verify_kzg_for_data_column_list(data_column_iter.clone(), kzg).is_ok() { + return Ok(()); + }; + + // Find all columns that are invalid and identify by index. If we hit this condition there + // should be at least one invalid column + let errors = data_column_iter + .filter_map(|data_column| { + if let Err(e) = verify_kzg_for_data_column(data_column.clone(), kzg) { + Some((data_column.index, e)) + } else { + None + } + }) + .collect::>(); + + Err(errors) +} + pub fn validate_data_column_sidecar_for_gossip( data_column: Arc>, subnet: u64, diff --git a/beacon_node/beacon_chain/src/fetch_blobs.rs b/beacon_node/beacon_chain/src/fetch_blobs.rs index 5bc2b92ec3..6e365f936d 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs.rs @@ -91,7 +91,7 @@ pub async fn fetch_and_process_engine_blobs( .await .map_err(FetchEngineBlobError::RequestFailed)?; - if response.is_empty() { + if response.is_empty() || response.iter().all(|opt| opt.is_none()) { debug!( log, "No blobs fetched from the EL"; diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index e22ec95a79..a48f32e7b4 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -130,10 +130,20 @@ impl BeaconChain { }); } - let blinded_block = block.clone_as_blinded(); - // Store block in the hot database without payload. - self.store - .blinded_block_as_kv_store_ops(&block_root, &blinded_block, &mut hot_batch); + if !self.store.get_config().prune_payloads { + // If prune-payloads is set to false, store the block which includes the execution payload + self.store + .block_as_kv_store_ops(&block_root, (*block).clone(), &mut hot_batch)?; + } else { + let blinded_block = block.clone_as_blinded(); + // Store block in the hot database without payload. + self.store.blinded_block_as_kv_store_ops( + &block_root, + &blinded_block, + &mut hot_batch, + ); + } + // Store the blobs too if let Some(blobs) = maybe_blobs { new_oldest_blob_slot = Some(block.slot()); diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index fe03420736..fa29a6ea7d 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -55,6 +55,7 @@ mod pre_finalization_cache; pub mod proposer_prep_service; pub mod schema_change; pub mod shuffling_cache; +pub mod single_attestation; pub mod state_advance_timer; pub mod sync_committee_rewards; pub mod sync_committee_verification; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 6071eeae57..a4b74d2793 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -85,12 +85,6 @@ pub static BLOCK_PROCESSING_COMMITTEE: LazyLock> = LazyLock::n "Time spent building/obtaining committees for block processing.", ) }); -pub static BLOCK_PROCESSING_SIGNATURE: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "beacon_block_processing_signature_seconds", - "Time spent doing signature verification for a block.", - ) -}); pub static BLOCK_PROCESSING_CORE: LazyLock> = LazyLock::new(|| { try_create_histogram( "beacon_block_processing_core_seconds", @@ -108,7 +102,7 @@ pub static BLOCK_PROCESSING_POST_EXEC_PROCESSING: LazyLock> = try_create_histogram_with_buckets( "beacon_block_processing_post_exec_pre_attestable_seconds", "Time between finishing execution processing and the block becoming attestable", - linear_buckets(5e-3, 5e-3, 10), + linear_buckets(0.01, 0.01, 15), ) }); pub static BLOCK_PROCESSING_DATA_COLUMNS_WAIT: LazyLock> = LazyLock::new(|| { @@ -615,12 +609,6 @@ pub static FORK_CHOICE_WRITE_LOCK_AQUIRE_TIMES: LazyLock> = La exponential_buckets(1e-3, 4.0, 7), ) }); -pub static FORK_CHOICE_SET_HEAD_LAG_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "beacon_fork_choice_set_head_lag_times", - "Time taken between finding the head and setting the canonical head value", - ) -}); pub static BALANCES_CACHE_HITS: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_balances_cache_hits_total", @@ -675,12 +663,6 @@ pub static DEFAULT_ETH1_VOTES: LazyLock> = LazyLock::new(|| { /* * Chain Head */ -pub static UPDATE_HEAD_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "beacon_update_head_seconds", - "Time taken to update the canonical head", - ) -}); pub static HEAD_STATE_SLOT: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "beacon_head_state_slot", @@ -1571,20 +1553,6 @@ pub static SYNC_CONTRIBUTION_PROCESSING_APPLY_TO_OP_POOL: LazyLock> = - LazyLock::new(|| { - try_create_histogram( - "beacon_sync_contribution_processing_signature_setup_seconds", - "Time spent on setting up for the signature verification of sync contribution processing" - ) - }); -pub static SYNC_CONTRIBUTION_PROCESSING_SIGNATURE_TIMES: LazyLock> = - LazyLock::new(|| { - try_create_histogram( - "beacon_sync_contribution_processing_signature_seconds", - "Time spent on the signature verification of sync contribution processing", - ) - }); /* * General Sync Committee Contribution Processing @@ -1714,13 +1682,6 @@ pub static DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES: LazyLock> = - LazyLock::new(|| { - try_create_int_counter( - "beacon_data_column_sidecar_processing_successes_total", - "Number of data column sidecars verified for gossip", - ) - }); pub static BLOBS_FROM_EL_HIT_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( @@ -1897,15 +1858,6 @@ pub static BLOCK_PRODUCTION_BLOBS_VERIFICATION_TIMES: LazyLock ) }, ); -/* - * Availability related metrics - */ -pub static BLOCK_AVAILABILITY_DELAY: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "block_availability_delay", - "Duration between start of the slot and the time at which all components of the block are available.", - ) -}); /* * Data Availability cache metrics @@ -1924,13 +1876,6 @@ pub static DATA_AVAILABILITY_OVERFLOW_MEMORY_STATE_CACHE_SIZE: LazyLock> = - LazyLock::new(|| { - try_create_int_gauge( - "data_availability_overflow_store_cache_size", - "Number of entries in the data availability overflow store cache.", - ) - }); pub static DATA_AVAILABILITY_RECONSTRUCTION_TIME: LazyLock> = LazyLock::new(|| { try_create_histogram( diff --git a/beacon_node/beacon_chain/src/single_attestation.rs b/beacon_node/beacon_chain/src/single_attestation.rs new file mode 100644 index 0000000000..fa4f98bb07 --- /dev/null +++ b/beacon_node/beacon_chain/src/single_attestation.rs @@ -0,0 +1,46 @@ +use crate::attestation_verification::Error; +use types::{Attestation, AttestationElectra, BitList, BitVector, EthSpec, SingleAttestation}; + +pub fn single_attestation_to_attestation( + single_attestation: &SingleAttestation, + committee: &[usize], +) -> Result, Error> { + let attester_index = single_attestation.attester_index; + let committee_index = single_attestation.committee_index; + let slot = single_attestation.data.slot; + + let aggregation_bit = committee + .iter() + .enumerate() + .find_map(|(i, &validator_index)| { + if attester_index as usize == validator_index { + return Some(i); + } + None + }) + .ok_or(Error::AttesterNotInCommittee { + attester_index, + committee_index, + slot, + })?; + + let mut committee_bits: BitVector = BitVector::default(); + committee_bits + .set(committee_index as usize, true) + .map_err(|e| Error::Invalid(e.into()))?; + + let mut aggregation_bits = + BitList::with_capacity(committee.len()).map_err(|e| Error::Invalid(e.into()))?; + aggregation_bits + .set(aggregation_bit, true) + .map_err(|e| Error::Invalid(e.into()))?; + + // TODO(electra): consider eventually allowing conversion to non-Electra attestations as well + // to maintain invertability (`Attestation` -> `SingleAttestation` -> `Attestation`). + Ok(Attestation::Electra(AttestationElectra { + aggregation_bits, + committee_bits, + data: single_attestation.data.clone(), + signature: single_attestation.signature.clone(), + })) +} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 824b17e275..4a439346cb 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -7,6 +7,7 @@ pub use crate::persisted_beacon_chain::PersistedBeaconChain; pub use crate::{ beacon_chain::{BEACON_CHAIN_DB_KEY, ETH1_CACHE_DB_KEY, FORK_CHOICE_DB_KEY, OP_POOL_DB_KEY}, migrate::MigratorConfig, + single_attestation::single_attestation_to_attestation, sync_committee_verification::Error as SyncCommitteeError, validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig}, BeaconChainError, NotifyExecutionLayer, ProduceBlockVerification, @@ -30,7 +31,7 @@ use execution_layer::{ ExecutionLayer, }; use futures::channel::mpsc::Receiver; -pub use genesis::{interop_genesis_state_with_eth1, DEFAULT_ETH1_BLOCK_HASH}; +pub use genesis::{InteropGenesisBuilder, DEFAULT_ETH1_BLOCK_HASH}; use int_to_bytes::int_to_bytes32; use kzg::trusted_setup::get_trusted_setup; use kzg::{Kzg, TrustedSetup}; @@ -231,6 +232,7 @@ pub struct Builder { mock_execution_layer: Option>, testing_slot_clock: Option, validator_monitor_config: Option, + genesis_state_builder: Option>, import_all_data_columns: bool, runtime: TestRuntime, log: Logger, @@ -252,16 +254,22 @@ impl Builder> { ) .unwrap(), ); + let genesis_state_builder = self.genesis_state_builder.take().unwrap_or_else(|| { + // Set alternating withdrawal credentials if no builder is specified. + InteropGenesisBuilder::default().set_alternating_eth1_withdrawal_credentials() + }); + let mutator = move |builder: BeaconChainBuilder<_>| { let header = generate_genesis_header::(builder.get_spec(), false); - let genesis_state = interop_genesis_state_with_eth1::( - &validator_keypairs, - HARNESS_GENESIS_TIME, - Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), - header, - builder.get_spec(), - ) - .expect("should generate interop state"); + let genesis_state = genesis_state_builder + .set_opt_execution_payload_header(header) + .build_genesis_state( + &validator_keypairs, + HARNESS_GENESIS_TIME, + Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), + builder.get_spec(), + ) + .expect("should generate interop state"); builder .genesis_state(genesis_state) .expect("should build state using recent genesis") @@ -317,16 +325,22 @@ impl Builder> { .clone() .expect("cannot build without validator keypairs"); + let genesis_state_builder = self.genesis_state_builder.take().unwrap_or_else(|| { + // Set alternating withdrawal credentials if no builder is specified. + InteropGenesisBuilder::default().set_alternating_eth1_withdrawal_credentials() + }); + let mutator = move |builder: BeaconChainBuilder<_>| { let header = generate_genesis_header::(builder.get_spec(), false); - let genesis_state = interop_genesis_state_with_eth1::( - &validator_keypairs, - HARNESS_GENESIS_TIME, - Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), - header, - builder.get_spec(), - ) - .expect("should generate interop state"); + let genesis_state = genesis_state_builder + .set_opt_execution_payload_header(header) + .build_genesis_state( + &validator_keypairs, + HARNESS_GENESIS_TIME, + Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), + builder.get_spec(), + ) + .expect("should generate interop state"); builder .genesis_state(genesis_state) .expect("should build state using recent genesis") @@ -374,6 +388,7 @@ where mock_execution_layer: None, testing_slot_clock: None, validator_monitor_config: None, + genesis_state_builder: None, import_all_data_columns: false, runtime, log, @@ -559,6 +574,15 @@ where self } + pub fn with_genesis_state_builder( + mut self, + f: impl FnOnce(InteropGenesisBuilder) -> InteropGenesisBuilder, + ) -> Self { + let builder = self.genesis_state_builder.take().unwrap_or_default(); + self.genesis_state_builder = Some(f(builder)); + self + } + pub fn build(self) -> BeaconChainHarness> { let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1); @@ -1133,7 +1157,8 @@ where let single_attestation = attestation.to_single_attestation_with_attester_index(attester_index as u64)?; - let attestation: Attestation = single_attestation.to_attestation(committee.committee)?; + let attestation: Attestation = + single_attestation_to_attestation(&single_attestation, committee.committee).unwrap(); assert_eq!( single_attestation.committee_index, diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index be7045c54a..41e6467b0f 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -36,6 +36,38 @@ fn get_harness(spec: ChainSpec) -> BeaconChainHarness> { .keypairs(KEYPAIRS.to_vec()) .fresh_ephemeral_store() .chain_config(chain_config) + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + +fn get_electra_harness(spec: ChainSpec) -> BeaconChainHarness> { + let chain_config = ChainConfig { + reconstruct_historic_states: true, + ..Default::default() + }; + + let spec = Arc::new(spec); + + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec.clone()) + .keypairs(KEYPAIRS.to_vec()) + .with_genesis_state_builder(|builder| { + builder.set_initial_balance_fn(Box::new(move |i| { + // Use a variety of balances between min activation balance and max effective balance. + let balance = spec.max_effective_balance_electra + / (i as u64 + 1) + / spec.effective_balance_increment + * spec.effective_balance_increment; + balance.max(spec.min_activation_balance) + })) + }) + .fresh_ephemeral_store() + .chain_config(chain_config) + .mock_execution_layer() .build(); harness.advance_slot(); @@ -560,6 +592,83 @@ async fn test_rewards_altair_inactivity_leak_justification_epoch() { assert_eq!(expected_balances, balances); } +#[tokio::test] +async fn test_rewards_electra() { + let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let harness = get_electra_harness(spec.clone()); + let target_epoch = 0; + + // advance until epoch N + 1 and get initial balances + harness + .extend_slots((E::slots_per_epoch() * (target_epoch + 1)) as usize) + .await; + let mut expected_balances = harness.get_current_state().balances().to_vec(); + + // advance until epoch N + 2 and build proposal rewards map + let mut proposal_rewards_map = HashMap::new(); + let mut sync_committee_rewards_map = HashMap::new(); + for _ in 0..E::slots_per_epoch() { + let state = harness.get_current_state(); + let slot = state.slot() + Slot::new(1); + + // calculate beacon block rewards / penalties + let ((signed_block, _maybe_blob_sidecars), mut state) = + harness.make_block_return_pre_state(state, slot).await; + let beacon_block_reward = harness + .chain + .compute_beacon_block_reward(signed_block.message(), &mut state) + .unwrap(); + + let total_proposer_reward = proposal_rewards_map + .entry(beacon_block_reward.proposer_index) + .or_insert(0); + *total_proposer_reward += beacon_block_reward.total as i64; + + // calculate sync committee rewards / penalties + let reward_payload = harness + .chain + .compute_sync_committee_rewards(signed_block.message(), &mut state) + .unwrap(); + + for reward in reward_payload { + let total_sync_reward = sync_committee_rewards_map + .entry(reward.validator_index) + .or_insert(0); + *total_sync_reward += reward.reward; + } + + harness.extend_slots(1).await; + } + + // compute reward deltas for all validators in epoch N + let StandardAttestationRewards { + ideal_rewards, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(target_epoch), vec![]) + .unwrap(); + + // assert ideal rewards are greater than 0 + assert_eq!( + ideal_rewards.len() as u64, + spec.max_effective_balance_electra / spec.effective_balance_increment + ); + assert!(ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + + // apply attestation, proposal, and sync committee rewards and penalties to initial balances + apply_attestation_rewards(&mut expected_balances, total_rewards); + apply_other_rewards(&mut expected_balances, &proposal_rewards_map); + apply_other_rewards(&mut expected_balances, &sync_committee_rewards_map); + + // verify expected balances against actual balances + let balances: Vec = harness.get_current_state().balances().to_vec(); + + assert_eq!(expected_balances, balances); +} + #[tokio::test] async fn test_rewards_base_subset_only() { let spec = ForkName::Base.make_genesis_spec(E::default_spec()); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 8654b33646..7a2df76970 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -47,7 +47,11 @@ type E = MinimalEthSpec; type TestHarness = BeaconChainHarness>; fn get_store(db_path: &TempDir) -> Arc, BeaconNodeBackend>> { - get_store_generic(db_path, StoreConfig::default(), test_spec::()) + let store_config = StoreConfig { + prune_payloads: false, + ..StoreConfig::default() + }; + get_store_generic(db_path, store_config, test_spec::()) } fn get_store_generic( @@ -2571,6 +2575,15 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { if block_root != prev_block_root { assert_eq!(block.slot(), slot); } + + // Prune_payloads is set to false in the default config, so the payload should exist + if block.message().execution_payload().is_ok() { + assert!(beacon_chain + .store + .execution_payload_exists(&block_root) + .unwrap(),); + } + prev_block_root = block_root; } @@ -3558,7 +3571,6 @@ fn check_split_slot( /// Check that all the states in a chain dump have the correct tree hash. fn check_chain_dump(harness: &TestHarness, expected_len: u64) { let mut chain_dump = harness.chain.chain_dump().unwrap(); - let split_slot = harness.chain.store.get_split_slot(); assert_eq!(chain_dump.len() as u64, expected_len); @@ -3585,13 +3597,12 @@ fn check_chain_dump(harness: &TestHarness, expected_len: u64) { // Check presence of execution payload on disk. if harness.chain.spec.bellatrix_fork_epoch.is_some() { - assert_eq!( + assert!( harness .chain .store .execution_payload_exists(&checkpoint.beacon_block_root) .unwrap(), - checkpoint.beacon_block.slot() >= split_slot, "incorrect payload storage for block at slot {}: {:?}", checkpoint.beacon_block.slot(), checkpoint.beacon_block_root, diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index cbd9e0b872..34b2ca6e0b 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -62,9 +62,9 @@ use task_executor::TaskExecutor; use tokio::sync::mpsc; use tokio::sync::mpsc::error::TrySendError; use types::{ - Attestation, BeaconState, ChainSpec, Hash256, RelativeEpoch, SignedAggregateAndProof, SubnetId, + Attestation, BeaconState, ChainSpec, EthSpec, Hash256, RelativeEpoch, SignedAggregateAndProof, + SingleAttestation, Slot, SubnetId, }; -use types::{EthSpec, Slot}; use work_reprocessing_queue::{ spawn_reprocess_scheduler, QueuedAggregate, QueuedLightClientUpdate, QueuedRpcBlock, QueuedUnaggregate, ReadyWork, @@ -109,8 +109,6 @@ pub struct BeaconProcessorQueueLengths { gossip_voluntary_exit_queue: usize, gossip_proposer_slashing_queue: usize, gossip_attester_slashing_queue: usize, - finality_update_queue: usize, - optimistic_update_queue: usize, unknown_light_client_update_queue: usize, unknown_block_sampling_request_queue: usize, rpc_block_queue: usize, @@ -132,9 +130,11 @@ pub struct BeaconProcessorQueueLengths { dcbroots_queue: usize, dcbrange_queue: usize, gossip_bls_to_execution_change_queue: usize, + lc_gossip_finality_update_queue: usize, + lc_gossip_optimistic_update_queue: usize, lc_bootstrap_queue: usize, - lc_optimistic_update_queue: usize, - lc_finality_update_queue: usize, + lc_rpc_optimistic_update_queue: usize, + lc_rpc_finality_update_queue: usize, lc_update_range_queue: usize, gossip_inclusion_list_queue: usize, api_request_p0_queue: usize, @@ -176,15 +176,13 @@ impl BeaconProcessorQueueLengths { gossip_voluntary_exit_queue: 4096, gossip_proposer_slashing_queue: 4096, gossip_attester_slashing_queue: 4096, - finality_update_queue: 1024, - optimistic_update_queue: 1024, - unknown_block_sampling_request_queue: 16384, unknown_light_client_update_queue: 128, rpc_block_queue: 1024, rpc_blob_queue: 1024, // TODO(das): Placeholder values rpc_custody_column_queue: 1000, rpc_verify_data_column_queue: 1000, + unknown_block_sampling_request_queue: 16384, sampling_result_queue: 1000, chain_segment_queue: 64, backfill_chain_segment: 64, @@ -201,9 +199,11 @@ impl BeaconProcessorQueueLengths { dcbroots_queue: 1024, dcbrange_queue: 1024, gossip_bls_to_execution_change_queue: 16384, + lc_gossip_finality_update_queue: 1024, + lc_gossip_optimistic_update_queue: 1024, lc_bootstrap_queue: 1024, - lc_optimistic_update_queue: 512, - lc_finality_update_queue: 512, + lc_rpc_optimistic_update_queue: 512, + lc_rpc_finality_update_queue: 512, lc_update_range_queue: 512, // TODO(focil) pick proper values gossip_inclusion_list_queue: 64, @@ -507,10 +507,10 @@ impl From for WorkEvent { /// Items required to verify a batch of unaggregated gossip attestations. #[derive(Debug)] -pub struct GossipAttestationPackage { +pub struct GossipAttestationPackage { pub message_id: MessageId, pub peer_id: PeerId, - pub attestation: Box>, + pub attestation: Box, pub subnet_id: SubnetId, pub should_import: bool, pub seen_timestamp: Duration, @@ -552,21 +552,32 @@ pub enum BlockingOrAsync { Blocking(BlockingFn), Async(AsyncFn), } +pub type GossipAttestationBatch = Vec>>; /// Indicates the type of work to be performed and therefore its priority and /// queuing specifics. pub enum Work { GossipAttestation { - attestation: Box>, - process_individual: Box) + Send + Sync>, - process_batch: Box>) + Send + Sync>, + attestation: Box>>, + process_individual: Box>) + Send + Sync>, + process_batch: Box) + Send + Sync>, + }, + // Attestation requiring conversion before processing. + // + // For now this is a `SingleAttestation`, but eventually we will switch this around so that + // legacy `Attestation`s are converted and the main processing pipeline operates on + // `SingleAttestation`s. + GossipAttestationToConvert { + attestation: Box>, + process_individual: + Box) + Send + Sync>, }, UnknownBlockAttestation { process_fn: BlockingFn, }, GossipAttestationBatch { - attestations: Vec>, - process_batch: Box>) + Send + Sync>, + attestations: GossipAttestationBatch, + process_batch: Box) + Send + Sync>, }, GossipAggregate { aggregate: Box>, @@ -643,6 +654,7 @@ impl fmt::Debug for Work { #[strum(serialize_all = "snake_case")] pub enum WorkType { GossipAttestation, + GossipAttestationToConvert, UnknownBlockAttestation, GossipAttestationBatch, GossipAggregate, @@ -695,6 +707,7 @@ impl Work { fn to_type(&self) -> WorkType { match self { Work::GossipAttestation { .. } => WorkType::GossipAttestation, + Work::GossipAttestationToConvert { .. } => WorkType::GossipAttestationToConvert, Work::GossipAttestationBatch { .. } => WorkType::GossipAttestationBatch, Work::GossipAggregate { .. } => WorkType::GossipAggregate, Work::GossipAggregateBatch { .. } => WorkType::GossipAggregateBatch, @@ -855,6 +868,7 @@ impl BeaconProcessor { let mut aggregate_queue = LifoQueue::new(queue_lengths.aggregate_queue); let mut aggregate_debounce = TimeLatch::default(); let mut attestation_queue = LifoQueue::new(queue_lengths.attestation_queue); + let mut attestation_to_convert_queue = LifoQueue::new(queue_lengths.attestation_queue); let mut attestation_debounce = TimeLatch::default(); let mut unknown_block_aggregate_queue = LifoQueue::new(queue_lengths.unknown_block_aggregate_queue); @@ -876,21 +890,16 @@ impl BeaconProcessor { let mut gossip_attester_slashing_queue = FifoQueue::new(queue_lengths.gossip_attester_slashing_queue); - // Using a FIFO queue for light client updates to maintain sequence order. - let mut finality_update_queue = FifoQueue::new(queue_lengths.finality_update_queue); - let mut optimistic_update_queue = FifoQueue::new(queue_lengths.optimistic_update_queue); - let mut unknown_light_client_update_queue = - FifoQueue::new(queue_lengths.unknown_light_client_update_queue); - let mut unknown_block_sampling_request_queue = - FifoQueue::new(queue_lengths.unknown_block_sampling_request_queue); - // Using a FIFO queue since blocks need to be imported sequentially. let mut rpc_block_queue = FifoQueue::new(queue_lengths.rpc_block_queue); let mut rpc_blob_queue = FifoQueue::new(queue_lengths.rpc_blob_queue); let mut rpc_custody_column_queue = FifoQueue::new(queue_lengths.rpc_custody_column_queue); let mut rpc_verify_data_column_queue = FifoQueue::new(queue_lengths.rpc_verify_data_column_queue); + // TODO(das): the sampling_request_queue is never read let mut sampling_result_queue = FifoQueue::new(queue_lengths.sampling_result_queue); + let mut unknown_block_sampling_request_queue = + FifoQueue::new(queue_lengths.unknown_block_sampling_request_queue); let mut chain_segment_queue = FifoQueue::new(queue_lengths.chain_segment_queue); let mut backfill_chain_segment = FifoQueue::new(queue_lengths.backfill_chain_segment); let mut gossip_block_queue = FifoQueue::new(queue_lengths.gossip_block_queue); @@ -909,10 +918,18 @@ impl BeaconProcessor { let mut gossip_bls_to_execution_change_queue = FifoQueue::new(queue_lengths.gossip_bls_to_execution_change_queue); + // Using FIFO queues for light client updates to maintain sequence order. + let mut lc_gossip_finality_update_queue = + FifoQueue::new(queue_lengths.lc_gossip_finality_update_queue); + let mut lc_gossip_optimistic_update_queue = + FifoQueue::new(queue_lengths.lc_gossip_optimistic_update_queue); + let mut unknown_light_client_update_queue = + FifoQueue::new(queue_lengths.unknown_light_client_update_queue); let mut lc_bootstrap_queue = FifoQueue::new(queue_lengths.lc_bootstrap_queue); - let mut lc_optimistic_update_queue = - FifoQueue::new(queue_lengths.lc_optimistic_update_queue); - let mut lc_finality_update_queue = FifoQueue::new(queue_lengths.lc_finality_update_queue); + let mut lc_rpc_optimistic_update_queue = + FifoQueue::new(queue_lengths.lc_rpc_optimistic_update_queue); + let mut lc_rpc_finality_update_queue = + FifoQueue::new(queue_lengths.lc_rpc_finality_update_queue); let mut lc_update_range_queue = FifoQueue::new(queue_lengths.lc_update_range_queue); let mut gossip_inclusion_list_queue = @@ -1190,6 +1207,8 @@ impl BeaconProcessor { } // TODO(focil) figure out priority and maybe introduce batching } else if let Some(item) = gossip_inclusion_list_queue.pop() { + // Convert any gossip attestations that need to be converted. + } else if let Some(item) = attestation_to_convert_queue.pop() { Some(item) // Check sync committee messages after attestations as their rewards are lesser // and they don't influence fork choice. @@ -1248,11 +1267,19 @@ impl BeaconProcessor { } else if let Some(item) = backfill_chain_segment.pop() { Some(item) // Handle light client requests. + } else if let Some(item) = lc_gossip_finality_update_queue.pop() { + Some(item) + } else if let Some(item) = lc_gossip_optimistic_update_queue.pop() { + Some(item) + } else if let Some(item) = unknown_light_client_update_queue.pop() { + Some(item) } else if let Some(item) = lc_bootstrap_queue.pop() { Some(item) - } else if let Some(item) = lc_optimistic_update_queue.pop() { + } else if let Some(item) = lc_rpc_optimistic_update_queue.pop() { Some(item) - } else if let Some(item) = lc_finality_update_queue.pop() { + } else if let Some(item) = lc_rpc_finality_update_queue.pop() { + Some(item) + } else if let Some(item) = lc_update_range_queue.pop() { Some(item) // This statement should always be the final else statement. } else { @@ -1312,6 +1339,9 @@ impl BeaconProcessor { match work { _ if can_spawn => self.spawn_worker(work, idle_tx), Work::GossipAttestation { .. } => attestation_queue.push(work), + Work::GossipAttestationToConvert { .. } => { + attestation_to_convert_queue.push(work) + } // Attestation batches are formed internally within the // `BeaconProcessor`, they are not sent from external services. Work::GossipAttestationBatch { .. } => crit!( @@ -1353,10 +1383,10 @@ impl BeaconProcessor { sync_contribution_queue.push(work) } Work::GossipLightClientFinalityUpdate { .. } => { - finality_update_queue.push(work, work_id, &self.log) + lc_gossip_finality_update_queue.push(work, work_id, &self.log) } Work::GossipLightClientOptimisticUpdate { .. } => { - optimistic_update_queue.push(work, work_id, &self.log) + lc_gossip_optimistic_update_queue.push(work, work_id, &self.log) } Work::RpcBlock { .. } | Work::IgnoredRpcBlock { .. } => { rpc_block_queue.push(work, work_id, &self.log) @@ -1391,10 +1421,10 @@ impl BeaconProcessor { lc_bootstrap_queue.push(work, work_id, &self.log) } Work::LightClientOptimisticUpdateRequest { .. } => { - lc_optimistic_update_queue.push(work, work_id, &self.log) + lc_rpc_optimistic_update_queue.push(work, work_id, &self.log) } Work::LightClientFinalityUpdateRequest { .. } => { - lc_finality_update_queue.push(work, work_id, &self.log) + lc_rpc_finality_update_queue.push(work, work_id, &self.log) } Work::LightClientUpdatesByRangeRequest { .. } => { lc_update_range_queue.push(work, work_id, &self.log) @@ -1444,7 +1474,8 @@ impl BeaconProcessor { if let Some(modified_queue_id) = modified_queue_id { let queue_len = match modified_queue_id { - WorkType::GossipAttestation => aggregate_queue.len(), + WorkType::GossipAttestation => attestation_queue.len(), + WorkType::GossipAttestationToConvert => attestation_to_convert_queue.len(), WorkType::UnknownBlockAttestation => unknown_block_attestation_queue.len(), WorkType::GossipAttestationBatch => 0, // No queue WorkType::GossipAggregate => aggregate_queue.len(), @@ -1465,9 +1496,11 @@ impl BeaconProcessor { WorkType::GossipAttesterSlashing => gossip_attester_slashing_queue.len(), WorkType::GossipSyncSignature => sync_message_queue.len(), WorkType::GossipSyncContribution => sync_contribution_queue.len(), - WorkType::GossipLightClientFinalityUpdate => finality_update_queue.len(), + WorkType::GossipLightClientFinalityUpdate => { + lc_gossip_finality_update_queue.len() + } WorkType::GossipLightClientOptimisticUpdate => { - optimistic_update_queue.len() + lc_gossip_optimistic_update_queue.len() } WorkType::RpcBlock => rpc_block_queue.len(), WorkType::RpcBlobs | WorkType::IgnoredRpcBlock => rpc_blob_queue.len(), @@ -1488,10 +1521,10 @@ impl BeaconProcessor { } WorkType::LightClientBootstrapRequest => lc_bootstrap_queue.len(), WorkType::LightClientOptimisticUpdateRequest => { - lc_optimistic_update_queue.len() + lc_rpc_optimistic_update_queue.len() } WorkType::LightClientFinalityUpdateRequest => { - lc_finality_update_queue.len() + lc_rpc_finality_update_queue.len() } WorkType::GossipInclusionList => gossip_inclusion_list_queue.len(), WorkType::LightClientUpdatesByRangeRequest => lc_update_range_queue.len(), @@ -1578,6 +1611,12 @@ impl BeaconProcessor { } => task_spawner.spawn_blocking(move || { process_individual(*attestation); }), + Work::GossipAttestationToConvert { + attestation, + process_individual, + } => task_spawner.spawn_blocking(move || { + process_individual(*attestation); + }), Work::GossipAttestationBatch { attestations, process_batch, diff --git a/beacon_node/builder_client/Cargo.toml b/beacon_node/builder_client/Cargo.toml index 3531e81c84..1920bd0ebb 100644 --- a/beacon_node/builder_client/Cargo.toml +++ b/beacon_node/builder_client/Cargo.toml @@ -6,7 +6,9 @@ authors = ["Sean Anderson "] [dependencies] eth2 = { workspace = true } +ethereum_ssz = { workspace = true } lighthouse_version = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 91ee00a65f..5f64ac7e43 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -1,16 +1,24 @@ use eth2::types::builder_bid::SignedBuilderBid; +use eth2::types::fork_versioned_response::EmptyMetadata; use eth2::types::{ - EthSpec, ExecutionBlockHash, ForkVersionedResponse, PublicKeyBytes, - SignedValidatorRegistrationData, Slot, + ContentType, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, ForkVersionDeserialize, + ForkVersionedResponse, PublicKeyBytes, SignedValidatorRegistrationData, Slot, }; use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock}; pub use eth2::Error; -use eth2::{ok_or_error, StatusCode, CONSENSUS_VERSION_HEADER}; -use reqwest::header::{HeaderMap, HeaderValue}; +use eth2::{ + ok_or_error, StatusCode, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, + JSON_CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER, +}; +use reqwest::header::{HeaderMap, HeaderValue, ACCEPT}; use reqwest::{IntoUrl, Response}; use sensitive_url::SensitiveUrl; use serde::de::DeserializeOwned; use serde::Serialize; +use ssz::Encode; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; pub const DEFAULT_TIMEOUT_MILLIS: u64 = 15000; @@ -49,6 +57,7 @@ pub struct BuilderHttpClient { server: SensitiveUrl, timeouts: Timeouts, user_agent: String, + ssz_enabled: Arc, } impl BuilderHttpClient { @@ -64,6 +73,7 @@ impl BuilderHttpClient { server, timeouts: Timeouts::new(builder_header_timeout), user_agent, + ssz_enabled: Arc::new(false.into()), }) } @@ -71,6 +81,78 @@ impl BuilderHttpClient { &self.user_agent } + fn fork_name_from_header(&self, headers: &HeaderMap) -> Result, String> { + headers + .get(CONSENSUS_VERSION_HEADER) + .map(|fork_name| { + fork_name + .to_str() + .map_err(|e| e.to_string()) + .and_then(ForkName::from_str) + }) + .transpose() + } + + fn content_type_from_header(&self, headers: &HeaderMap) -> ContentType { + let Some(content_type) = headers.get(CONTENT_TYPE_HEADER).map(|content_type| { + let content_type = content_type.to_str(); + match content_type { + Ok(SSZ_CONTENT_TYPE_HEADER) => ContentType::Ssz, + _ => ContentType::Json, + } + }) else { + return ContentType::Json; + }; + content_type + } + + async fn get_with_header< + T: DeserializeOwned + ForkVersionDecode + ForkVersionDeserialize, + U: IntoUrl, + >( + &self, + url: U, + timeout: Duration, + headers: HeaderMap, + ) -> Result, Error> { + let response = self + .get_response_with_header(url, Some(timeout), headers) + .await?; + + let headers = response.headers().clone(); + let response_bytes = response.bytes().await?; + + let Ok(Some(fork_name)) = self.fork_name_from_header(&headers) else { + // if no fork version specified, attempt to fallback to JSON + self.ssz_enabled.store(false, Ordering::SeqCst); + return serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson); + }; + + let content_type = self.content_type_from_header(&headers); + + match content_type { + ContentType::Ssz => { + self.ssz_enabled.store(true, Ordering::SeqCst); + T::from_ssz_bytes_by_fork(&response_bytes, fork_name) + .map(|data| ForkVersionedResponse { + version: Some(fork_name), + metadata: EmptyMetadata {}, + data, + }) + .map_err(Error::InvalidSsz) + } + ContentType::Json => { + self.ssz_enabled.store(false, Ordering::SeqCst); + serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson) + } + } + } + + /// Return `true` if the most recently received response from the builder had SSZ Content-Type. + pub fn is_ssz_enabled(&self) -> bool { + self.ssz_enabled.load(Ordering::SeqCst) + } + async fn get_with_timeout( &self, url: U, @@ -83,6 +165,21 @@ impl BuilderHttpClient { .map_err(Into::into) } + /// Perform a HTTP GET request, returning the `Response` for further processing. + async fn get_response_with_header( + &self, + url: U, + timeout: Option, + headers: HeaderMap, + ) -> Result { + let mut builder = self.client.get(url); + if let Some(timeout) = timeout { + builder = builder.timeout(timeout); + } + let response = builder.headers(headers).send().await.map_err(Error::from)?; + ok_or_error(response).await + } + /// Perform a HTTP GET request, returning the `Response` for further processing. async fn get_response_with_timeout( &self, @@ -112,6 +209,32 @@ impl BuilderHttpClient { ok_or_error(response).await } + async fn post_ssz_with_raw_response( + &self, + url: U, + ssz_body: Vec, + mut headers: HeaderMap, + timeout: Option, + ) -> Result { + let mut builder = self.client.post(url); + if let Some(timeout) = timeout { + builder = builder.timeout(timeout); + } + + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_static(SSZ_CONTENT_TYPE_HEADER), + ); + + let response = builder + .headers(headers) + .body(ssz_body) + .send() + .await + .map_err(Error::from)?; + ok_or_error(response).await + } + async fn post_with_raw_response( &self, url: U, @@ -152,6 +275,42 @@ impl BuilderHttpClient { Ok(()) } + /// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body + pub async fn post_builder_blinded_blocks_ssz( + &self, + blinded_block: &SignedBlindedBeaconBlock, + ) -> Result, Error> { + let mut path = self.server.full.clone(); + + let body = blinded_block.as_ssz_bytes(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v1") + .push("builder") + .push("blinded_blocks"); + + let mut headers = HeaderMap::new(); + if let Ok(value) = HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) { + headers.insert(CONSENSUS_VERSION_HEADER, value); + } + + let result = self + .post_ssz_with_raw_response( + path, + body, + headers, + Some(self.timeouts.post_blinded_blocks), + ) + .await? + .bytes() + .await?; + + FullPayloadContents::from_ssz_bytes_by_fork(&result, blinded_block.fork_name_unchecked()) + .map_err(Error::InvalidSsz) + } + /// `POST /eth/v1/builder/blinded_blocks` pub async fn post_builder_blinded_blocks( &self, @@ -202,7 +361,17 @@ impl BuilderHttpClient { .push(format!("{parent_hash:?}").as_str()) .push(pubkey.as_hex_string().as_str()); - let resp = self.get_with_timeout(path, self.timeouts.get_header).await; + let mut headers = HeaderMap::new(); + if let Ok(ssz_content_type_header) = HeaderValue::from_str(&format!( + "{}; q=1.0,{}; q=0.9", + SSZ_CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER + )) { + headers.insert(ACCEPT, ssz_content_type_header); + }; + + let resp = self + .get_with_header(path, self.timeouts.get_header, headers) + .await; if matches!(resp, Err(Error::StatusCode(StatusCode::NO_CONTENT))) { Ok(None) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 8ff677dda4..b7d27ddf0f 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -158,6 +158,7 @@ pub enum Error { }, ZeroLengthTransaction, PayloadBodiesByRangeNotSupported, + GetBlobsNotSupported, InvalidJWTSecret(String), InvalidForkForPayload, InvalidPayloadBody(String), @@ -1886,7 +1887,7 @@ impl ExecutionLayer { .map_err(Box::new) .map_err(Error::EngineError) } else { - Ok(vec![None; query.len()]) + Err(Error::GetBlobsNotSupported) } } @@ -1915,11 +1916,18 @@ impl ExecutionLayer { if let Some(builder) = self.builder() { let (payload_result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { - builder - .post_builder_blinded_blocks(block) - .await - .map_err(Error::Builder) - .map(|d| d.data) + if builder.is_ssz_enabled() { + builder + .post_builder_blinded_blocks_ssz(block) + .await + .map_err(Error::Builder) + } else { + builder + .post_builder_blinded_blocks(block) + .await + .map_err(Error::Builder) + .map(|d| d.data) + } }) .await; diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 3540909fe4..f07ee7ac6f 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -1,15 +1,20 @@ use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET}; use crate::{Config, ExecutionLayer, PayloadAttributes, PayloadParameters}; +use bytes::Bytes; use eth2::types::PublishBlockRequest; use eth2::types::{ BlobsBundle, BlockId, BroadcastValidation, EventKind, EventTopic, FullPayloadContents, ProposerData, StateId, ValidatorId, }; -use eth2::{BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER}; +use eth2::{ + BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, + SSZ_CONTENT_TYPE_HEADER, +}; use fork_choice::ForkchoiceUpdateParameters; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; use slog::{debug, error, info, warn, Logger}; +use ssz::Encode; use std::collections::HashMap; use std::fmt::Debug; use std::future::Future; @@ -26,11 +31,12 @@ use types::builder_bid::{ }; use types::{ Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionPayload, - ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionedResponse, Hash256, - PublicKeyBytes, Signature, SignedBlindedBeaconBlock, SignedRoot, - SignedValidatorRegistrationData, Slot, Uint256, + ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionDecode, + ForkVersionedResponse, Hash256, PublicKeyBytes, Signature, SignedBlindedBeaconBlock, + SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, }; use types::{ExecutionBlockHash, SecretKey}; +use warp::reply::{self, Reply}; use warp::{Filter, Rejection}; pub const DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -955,6 +961,33 @@ pub fn serve( ) .boxed(); + let blinded_block_ssz = prefix + .and(warp::path("blinded_blocks")) + .and(warp::body::bytes()) + .and(warp::header::header::(CONSENSUS_VERSION_HEADER)) + .and(warp::path::end()) + .and(ctx_filter.clone()) + .and_then( + |block_bytes: Bytes, fork_name: ForkName, builder: MockBuilder| async move { + let block = + SignedBlindedBeaconBlock::::from_ssz_bytes_by_fork(&block_bytes, fork_name) + .map_err(|e| warp::reject::custom(Custom(format!("{:?}", e))))?; + let payload = builder + .submit_blinded_block(block) + .await + .map_err(|e| warp::reject::custom(Custom(e)))?; + + Ok::<_, warp::reject::Rejection>( + warp::http::Response::builder() + .status(200) + .body(payload.as_ssz_bytes()) + .map(add_ssz_content_type_header) + .map(|res| add_consensus_version_header(res, fork_name)) + .unwrap(), + ) + }, + ); + let blinded_block = prefix .and(warp::path("blinded_blocks")) @@ -1007,35 +1040,47 @@ pub fn serve( ) .and(warp::path::end()) .and(ctx_filter.clone()) + .and(warp::header::optional::("accept")) .and_then( |slot: Slot, parent_hash: ExecutionBlockHash, pubkey: PublicKeyBytes, - builder: MockBuilder| async move { + builder: MockBuilder, + accept_header: Option| async move { let fork_name = builder.fork_name_at_slot(slot); let signed_bid = builder .get_header(slot, parent_hash, pubkey) .await .map_err(|e| warp::reject::custom(Custom(e)))?; - - let resp: ForkVersionedResponse<_> = ForkVersionedResponse { - version: Some(fork_name), - metadata: Default::default(), - data: signed_bid, - }; - let json_bid = serde_json::to_string(&resp) - .map_err(|_| reject("coudn't serialize signed bid"))?; - Ok::<_, Rejection>( - warp::http::Response::builder() - .status(200) - .body(json_bid) - .unwrap(), - ) + let accept_header = accept_header.unwrap_or(eth2::types::Accept::Any); + match accept_header { + eth2::types::Accept::Ssz => Ok::<_, Rejection>( + warp::http::Response::builder() + .status(200) + .body(signed_bid.as_ssz_bytes()) + .map(add_ssz_content_type_header) + .map(|res| add_consensus_version_header(res, fork_name)) + .unwrap(), + ), + eth2::types::Accept::Json | eth2::types::Accept::Any => { + let resp: ForkVersionedResponse<_> = ForkVersionedResponse { + version: Some(fork_name), + metadata: Default::default(), + data: signed_bid, + }; + Ok::<_, Rejection>(warp::reply::json(&resp).into_response()) + } + } }, ); let routes = warp::post() - .and(validators.or(blinded_block)) + // Routes which expect `application/octet-stream` go within this `and`. + .and( + warp::header::exact(CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER) + .and(blinded_block_ssz), + ) + .or(validators.or(blinded_block)) .or(warp::get().and(status).or(header)) .map(|reply| warp::reply::with_header(reply, "Server", "lighthouse-mock-builder-server")); @@ -1048,3 +1093,13 @@ pub fn serve( fn reject(msg: &'static str) -> Rejection { warp::reject::custom(Custom(msg.to_string())) } + +/// Add the 'Content-Type application/octet-stream` header to a response. +fn add_ssz_content_type_header(reply: T) -> warp::reply::Response { + reply::with_header(reply, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER).into_response() +} + +/// Add the `Eth-Consensus-Version` header to a response. +fn add_consensus_version_header(reply: T, fork_name: ForkName) -> warp::reply::Response { + reply::with_header(reply, CONSENSUS_VERSION_HEADER, fork_name.to_string()).into_response() +} diff --git a/beacon_node/genesis/src/interop.rs b/beacon_node/genesis/src/interop.rs index 90c4ad6e66..4fccc0393b 100644 --- a/beacon_node/genesis/src/interop.rs +++ b/beacon_node/genesis/src/interop.rs @@ -24,10 +24,134 @@ fn eth1_withdrawal_credentials(pubkey: &PublicKey, spec: &ChainSpec) -> Hash256 Hash256::from_slice(&credentials) } +pub type WithdrawalCredentialsFn = + Box Fn(usize, &'a PublicKey, &'a ChainSpec) -> Hash256>; + /// Builds a genesis state as defined by the Eth2 interop procedure (see below). /// /// Reference: /// https://github.com/ethereum/eth2.0-pm/tree/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start +#[derive(Default)] +pub struct InteropGenesisBuilder { + /// Mapping from validator index to initial balance for each validator. + /// + /// If `None`, then the default balance of 32 ETH will be used. + initial_balance_fn: Option u64>>, + + /// Mapping from validator index and pubkey to withdrawal credentials for each validator. + /// + /// If `None`, then default BLS withdrawal credentials will be used. + withdrawal_credentials_fn: Option, + + /// The execution payload header to embed in the genesis state. + execution_payload_header: Option>, +} + +impl InteropGenesisBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn set_initial_balance_fn(mut self, initial_balance_fn: Box u64>) -> Self { + self.initial_balance_fn = Some(initial_balance_fn); + self + } + + pub fn set_withdrawal_credentials_fn( + mut self, + withdrawal_credentials_fn: WithdrawalCredentialsFn, + ) -> Self { + self.withdrawal_credentials_fn = Some(withdrawal_credentials_fn); + self + } + + pub fn set_alternating_eth1_withdrawal_credentials(self) -> Self { + self.set_withdrawal_credentials_fn(Box::new(alternating_eth1_withdrawal_credentials_fn)) + } + + pub fn set_execution_payload_header( + self, + execution_payload_header: ExecutionPayloadHeader, + ) -> Self { + self.set_opt_execution_payload_header(Some(execution_payload_header)) + } + + pub fn set_opt_execution_payload_header( + mut self, + execution_payload_header: Option>, + ) -> Self { + self.execution_payload_header = execution_payload_header; + self + } + + pub fn build_genesis_state( + self, + keypairs: &[Keypair], + genesis_time: u64, + eth1_block_hash: Hash256, + spec: &ChainSpec, + ) -> Result, String> { + // Generate withdrawal credentials using provided function, or default BLS. + let withdrawal_credentials_fn = self.withdrawal_credentials_fn.unwrap_or_else(|| { + Box::new(|_, pubkey, spec| bls_withdrawal_credentials(pubkey, spec)) + }); + + let withdrawal_credentials = keypairs + .iter() + .map(|key| &key.pk) + .enumerate() + .map(|(i, pubkey)| withdrawal_credentials_fn(i, pubkey, spec)) + .collect::>(); + + // Generate initial balances. + let initial_balance_fn = self + .initial_balance_fn + .unwrap_or_else(|| Box::new(|_| spec.max_effective_balance)); + + let eth1_timestamp = 2_u64.pow(40); + + let initial_balances = (0..keypairs.len()) + .map(initial_balance_fn) + .collect::>(); + + let datas = keypairs + .into_par_iter() + .zip(withdrawal_credentials.into_par_iter()) + .zip(initial_balances.into_par_iter()) + .map(|((keypair, withdrawal_credentials), amount)| { + let mut data = DepositData { + withdrawal_credentials, + pubkey: keypair.pk.clone().into(), + amount, + signature: Signature::empty().into(), + }; + + data.signature = data.create_signature(&keypair.sk, spec); + + data + }) + .collect::>(); + + let mut state = initialize_beacon_state_from_eth1( + eth1_block_hash, + eth1_timestamp, + genesis_deposits(datas, spec)?, + self.execution_payload_header, + spec, + ) + .map_err(|e| format!("Unable to initialize genesis state: {:?}", e))?; + + *state.genesis_time_mut() = genesis_time; + + // Invalidate all the caches after all the manual state surgery. + state + .drop_all_caches() + .map_err(|e| format!("Unable to drop caches: {:?}", e))?; + + Ok(state) + } +} + pub fn interop_genesis_state( keypairs: &[Keypair], genesis_time: u64, @@ -35,18 +159,21 @@ pub fn interop_genesis_state( execution_payload_header: Option>, spec: &ChainSpec, ) -> Result, String> { - let withdrawal_credentials = keypairs - .iter() - .map(|keypair| bls_withdrawal_credentials(&keypair.pk, spec)) - .collect::>(); - interop_genesis_state_with_withdrawal_credentials::( - keypairs, - &withdrawal_credentials, - genesis_time, - eth1_block_hash, - execution_payload_header, - spec, - ) + InteropGenesisBuilder::new() + .set_opt_execution_payload_header(execution_payload_header) + .build_genesis_state(keypairs, genesis_time, eth1_block_hash, spec) +} + +fn alternating_eth1_withdrawal_credentials_fn<'a>( + index: usize, + pubkey: &'a PublicKey, + spec: &'a ChainSpec, +) -> Hash256 { + if index % 2usize == 0usize { + bls_withdrawal_credentials(pubkey, spec) + } else { + eth1_withdrawal_credentials(pubkey, spec) + } } // returns an interop genesis state except every other @@ -58,80 +185,10 @@ pub fn interop_genesis_state_with_eth1( execution_payload_header: Option>, spec: &ChainSpec, ) -> Result, String> { - let withdrawal_credentials = keypairs - .iter() - .enumerate() - .map(|(index, keypair)| { - if index % 2 == 0 { - bls_withdrawal_credentials(&keypair.pk, spec) - } else { - eth1_withdrawal_credentials(&keypair.pk, spec) - } - }) - .collect::>(); - interop_genesis_state_with_withdrawal_credentials::( - keypairs, - &withdrawal_credentials, - genesis_time, - eth1_block_hash, - execution_payload_header, - spec, - ) -} - -pub fn interop_genesis_state_with_withdrawal_credentials( - keypairs: &[Keypair], - withdrawal_credentials: &[Hash256], - genesis_time: u64, - eth1_block_hash: Hash256, - execution_payload_header: Option>, - spec: &ChainSpec, -) -> Result, String> { - if keypairs.len() != withdrawal_credentials.len() { - return Err(format!( - "wrong number of withdrawal credentials, expected: {}, got: {}", - keypairs.len(), - withdrawal_credentials.len() - )); - } - - let eth1_timestamp = 2_u64.pow(40); - let amount = spec.max_effective_balance; - - let datas = keypairs - .into_par_iter() - .zip(withdrawal_credentials.into_par_iter()) - .map(|(keypair, &withdrawal_credentials)| { - let mut data = DepositData { - withdrawal_credentials, - pubkey: keypair.pk.clone().into(), - amount, - signature: Signature::empty().into(), - }; - - data.signature = data.create_signature(&keypair.sk, spec); - - data - }) - .collect::>(); - - let mut state = initialize_beacon_state_from_eth1( - eth1_block_hash, - eth1_timestamp, - genesis_deposits(datas, spec)?, - execution_payload_header, - spec, - ) - .map_err(|e| format!("Unable to initialize genesis state: {:?}", e))?; - - *state.genesis_time_mut() = genesis_time; - - // Invalidate all the caches after all the manual state surgery. - state - .drop_all_caches() - .map_err(|e| format!("Unable to drop caches: {:?}", e))?; - - Ok(state) + InteropGenesisBuilder::new() + .set_alternating_eth1_withdrawal_credentials() + .set_opt_execution_payload_header(execution_payload_header) + .build_genesis_state(keypairs, genesis_time, eth1_block_hash, spec) } #[cfg(test)] diff --git a/beacon_node/genesis/src/lib.rs b/beacon_node/genesis/src/lib.rs index 3fb053bf88..1fba64aafb 100644 --- a/beacon_node/genesis/src/lib.rs +++ b/beacon_node/genesis/src/lib.rs @@ -7,6 +7,6 @@ pub use eth1::Eth1Endpoint; pub use eth1_genesis_service::{Eth1GenesisService, Statistics}; pub use interop::{ bls_withdrawal_credentials, interop_genesis_state, interop_genesis_state_with_eth1, - interop_genesis_state_with_withdrawal_credentials, DEFAULT_ETH1_BLOCK_HASH, + InteropGenesisBuilder, DEFAULT_ETH1_BLOCK_HASH, }; pub use types::test_utils::generate_deterministic_keypairs; diff --git a/beacon_node/http_api/src/aggregate_attestation.rs b/beacon_node/http_api/src/aggregate_attestation.rs new file mode 100644 index 0000000000..23af5b0cb5 --- /dev/null +++ b/beacon_node/http_api/src/aggregate_attestation.rs @@ -0,0 +1,68 @@ +use crate::api_types::GenericResponse; +use crate::unsupported_version_rejection; +use crate::version::{add_consensus_version_header, V1, V2}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::types::{self, EndpointVersion, Hash256, Slot}; +use std::sync::Arc; +use types::fork_versioned_response::EmptyMetadata; +use types::{CommitteeIndex, ForkVersionedResponse}; +use warp::{ + hyper::{Body, Response}, + reply::Reply, +}; + +pub fn get_aggregate_attestation( + slot: Slot, + attestation_data_root: &Hash256, + committee_index: Option, + endpoint_version: EndpointVersion, + chain: Arc>, +) -> Result, warp::reject::Rejection> { + let fork_name = chain.spec.fork_name_at_slot::(slot); + let aggregate_attestation = if fork_name.electra_enabled() { + let Some(committee_index) = committee_index else { + return Err(warp_utils::reject::custom_bad_request( + "missing committee index".to_string(), + )); + }; + chain + .get_aggregated_attestation_electra(slot, attestation_data_root, committee_index) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "unable to fetch aggregate: {:?}", + e + )) + })? + .ok_or_else(|| { + warp_utils::reject::custom_not_found("no matching aggregate found".to_string()) + })? + } else { + chain + .get_pre_electra_aggregated_attestation_by_slot_and_root(slot, attestation_data_root) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "unable to fetch aggregate: {:?}", + e + )) + })? + .ok_or_else(|| { + warp_utils::reject::custom_not_found("no matching aggregate found".to_string()) + })? + }; + + if endpoint_version == V2 { + let fork_versioned_response = ForkVersionedResponse { + version: Some(fork_name), + metadata: EmptyMetadata {}, + data: aggregate_attestation, + }; + Ok(add_consensus_version_header( + warp::reply::json(&fork_versioned_response).into_response(), + fork_name, + )) + } else if endpoint_version == V1 { + Ok(warp::reply::json(&GenericResponse::from(aggregate_attestation)).into_response()) + } else { + return Err(unsupported_version_rejection(endpoint_version)); + } +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index ae39353586..88231c1325 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -5,6 +5,7 @@ //! There are also some additional, non-standard endpoints behind the `/lighthouse/` path which are //! used for development. +mod aggregate_attestation; mod attestation_performance; mod attester_duties; mod block_id; @@ -172,7 +173,7 @@ impl Default for Config { sse_capacity_multiplier: 1, enable_beacon_processor: true, duplicate_block_status_code: StatusCode::ACCEPTED, - enable_light_client_server: false, + enable_light_client_server: true, target_peers: 100, } } @@ -3419,40 +3420,15 @@ pub fn serve( not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { + task_spawner.blocking_response_task(Priority::P0, move || { not_synced_filter?; - let res = if endpoint_version == V2 { - let Some(committee_index) = query.committee_index else { - return Err(warp_utils::reject::custom_bad_request( - "missing committee index".to_string(), - )); - }; - chain.get_aggregated_attestation_electra( - query.slot, - &query.attestation_data_root, - committee_index, - ) - } else if endpoint_version == V1 { - // Do nothing - chain.get_pre_electra_aggregated_attestation_by_slot_and_root( - query.slot, - &query.attestation_data_root, - ) - } else { - return Err(unsupported_version_rejection(endpoint_version)); - }; - res.map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "unable to fetch aggregate: {:?}", - e - )) - })? - .map(api_types::GenericResponse::from) - .ok_or_else(|| { - warp_utils::reject::custom_not_found( - "no matching aggregate found".to_string(), - ) - }) + crate::aggregate_attestation::get_aggregate_attestation( + query.slot, + &query.attestation_data_root, + query.committee_index, + endpoint_version, + chain, + ) }) }, ); diff --git a/beacon_node/http_api/src/publish_attestations.rs b/beacon_node/http_api/src/publish_attestations.rs index 1b9949d4d5..10d13e09a5 100644 --- a/beacon_node/http_api/src/publish_attestations.rs +++ b/beacon_node/http_api/src/publish_attestations.rs @@ -36,8 +36,8 @@ //! attestations and there's no immediate cause for concern. use crate::task_spawner::{Priority, TaskSpawner}; use beacon_chain::{ - validator_monitor::timestamp_now, AttestationError, BeaconChain, BeaconChainError, - BeaconChainTypes, + single_attestation::single_attestation_to_attestation, validator_monitor::timestamp_now, + AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes, }; use beacon_processor::work_reprocessing_queue::{QueuedUnaggregate, ReprocessQueueMessage}; use either::Either; @@ -183,10 +183,10 @@ fn convert_to_attestation<'a, T: BeaconChainTypes>( chain: &Arc>, attestation: &'a Either, SingleAttestation>, ) -> Result>, Error> { - let a = match attestation { - Either::Left(a) => Cow::Borrowed(a), - Either::Right(single_attestation) => chain - .with_committee_cache( + match attestation { + Either::Left(a) => Ok(Cow::Borrowed(a)), + Either::Right(single_attestation) => { + let conversion_result = chain.with_committee_cache( single_attestation.data.target.root, single_attestation .data @@ -197,24 +197,33 @@ fn convert_to_attestation<'a, T: BeaconChainTypes>( single_attestation.data.slot, single_attestation.committee_index, ) else { - return Err(BeaconChainError::AttestationError( - types::AttestationError::NoCommitteeForSlotAndIndex { - slot: single_attestation.data.slot, - index: single_attestation.committee_index, - }, - )); + return Ok(Err(AttestationError::NoCommitteeForSlotAndIndex { + slot: single_attestation.data.slot, + index: single_attestation.committee_index, + })); }; - let attestation = - single_attestation.to_attestation::(committee.committee)?; - - Ok(Cow::Owned(attestation)) + Ok(single_attestation_to_attestation::( + single_attestation, + committee.committee, + ) + .map(Cow::Owned)) }, - ) - .map_err(Error::FailedConversion)?, - }; - - Ok(a) + ); + match conversion_result { + Ok(Ok(attestation)) => Ok(attestation), + Ok(Err(e)) => Err(Error::Validation(e)), + // Map the error returned by `with_committee_cache` for unknown blocks into the + // `UnknownHeadBlock` error that is gracefully handled. + Err(BeaconChainError::MissingBeaconBlock(beacon_block_root)) => { + Err(Error::Validation(AttestationError::UnknownHeadBlock { + beacon_block_root, + })) + } + Err(e) => Err(Error::FailedConversion(e)), + } + } + } } pub async fn publish_attestations( diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index d6b8df33b3..10e1d01536 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -5,7 +5,7 @@ use beacon_chain::{ }; use eth2::types::{IndexedErrorMessage, StateId, SyncSubcommittee}; use execution_layer::test_utils::generate_genesis_header; -use genesis::{bls_withdrawal_credentials, interop_genesis_state_with_withdrawal_credentials}; +use genesis::{bls_withdrawal_credentials, InteropGenesisBuilder}; use http_api::test_utils::*; use std::collections::HashSet; use types::{ @@ -346,35 +346,46 @@ fn assert_server_indexed_error(error: eth2::Error, status_code: u16, indices: Ve #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn bls_to_execution_changes_update_all_around_capella_fork() { - let validator_count = 128; + const VALIDATOR_COUNT: usize = 128; let fork_epoch = Epoch::new(2); let spec = capella_spec(fork_epoch); let max_bls_to_execution_changes = E::max_bls_to_execution_changes(); // Use a genesis state with entirely BLS withdrawal credentials. - // Offset keypairs by `validator_count` to create keys distinct from the signing keys. - let validator_keypairs = generate_deterministic_keypairs(validator_count); - let withdrawal_keypairs = (0..validator_count) - .map(|i| Some(generate_deterministic_keypair(i + validator_count))) - .collect::>(); - let withdrawal_credentials = withdrawal_keypairs - .iter() - .map(|keypair| bls_withdrawal_credentials(&keypair.as_ref().unwrap().pk, &spec)) + // Offset keypairs by `VALIDATOR_COUNT` to create keys distinct from the signing keys. + let validator_keypairs = generate_deterministic_keypairs(VALIDATOR_COUNT); + let withdrawal_keypairs = (0..VALIDATOR_COUNT) + .map(|i| Some(generate_deterministic_keypair(i + VALIDATOR_COUNT))) .collect::>(); + + fn withdrawal_credentials_fn<'a>( + index: usize, + _: &'a types::PublicKey, + spec: &'a ChainSpec, + ) -> Hash256 { + // It is a bit inefficient to regenerate the whole keypair here, but this is a workaround. + // `InteropGenesisBuilder` requires the `withdrawal_credentials_fn` to have + // a `'static` lifetime. + let keypair = generate_deterministic_keypair(index + VALIDATOR_COUNT); + bls_withdrawal_credentials(&keypair.pk, spec) + } + let header = generate_genesis_header(&spec, true); - let genesis_state = interop_genesis_state_with_withdrawal_credentials( - &validator_keypairs, - &withdrawal_credentials, - HARNESS_GENESIS_TIME, - Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), - header, - &spec, - ) - .unwrap(); + + let genesis_state = InteropGenesisBuilder::new() + .set_opt_execution_payload_header(header) + .set_withdrawal_credentials_fn(Box::new(withdrawal_credentials_fn)) + .build_genesis_state( + &validator_keypairs, + HARNESS_GENESIS_TIME, + Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), + &spec, + ) + .unwrap(); let tester = InteractiveTester::::new_with_initializer_and_mutator( Some(spec.clone()), - validator_count, + VALIDATOR_COUNT, Some(Box::new(|harness_builder| { harness_builder .keypairs(validator_keypairs) @@ -421,7 +432,7 @@ async fn bls_to_execution_changes_update_all_around_capella_fork() { let pubkey = &harness.get_withdrawal_keypair(validator_index).pk; // And the wrong secret key. let secret_key = &harness - .get_withdrawal_keypair((validator_index + 1) % validator_count as u64) + .get_withdrawal_keypair((validator_index + 1) % VALIDATOR_COUNT as u64) .sk; harness.make_bls_to_execution_change_with_keys( validator_index, @@ -433,7 +444,7 @@ async fn bls_to_execution_changes_update_all_around_capella_fork() { .collect::>(); // Submit some changes before Capella. Just enough to fill two blocks. - let num_pre_capella = validator_count / 4; + let num_pre_capella = VALIDATOR_COUNT / 4; let blocks_filled_pre_capella = 2; assert_eq!( num_pre_capella, @@ -488,7 +499,7 @@ async fn bls_to_execution_changes_update_all_around_capella_fork() { ); // Add Capella blocks which should be full of BLS to execution changes. - for i in 0..validator_count / max_bls_to_execution_changes { + for i in 0..VALIDATOR_COUNT / max_bls_to_execution_changes { let head_block_root = harness.extend_slots(1).await; let head_block = harness .chain @@ -534,7 +545,7 @@ async fn bls_to_execution_changes_update_all_around_capella_fork() { assert_server_indexed_error( error, 400, - (validator_count..3 * validator_count).collect(), + (VALIDATOR_COUNT..3 * VALIDATOR_COUNT).collect(), ); } } diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index ebc28123a9..50796e03a3 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3570,44 +3570,48 @@ impl ApiTester { } #[allow(clippy::await_holding_lock)] // This is a test, so it should be fine. - pub async fn test_get_validator_aggregate_attestation(self) -> Self { - if self + pub async fn test_get_validator_aggregate_attestation_v1(self) -> Self { + let attestation = self .chain - .spec - .fork_name_at_slot::(self.chain.slot().unwrap()) - .electra_enabled() - { - for attestation in self.chain.naive_aggregation_pool.read().iter() { - let result = self - .client - .get_validator_aggregate_attestation_v2( - attestation.data().slot, - attestation.data().tree_hash_root(), - attestation.committee_index().expect("committee index"), - ) - .await - .unwrap() - .unwrap() - .data; - let expected = attestation; + .head_beacon_block() + .message() + .body() + .attestations() + .next() + .unwrap() + .clone_as_attestation(); + let result = self + .client + .get_validator_aggregate_attestation_v1( + attestation.data().slot, + attestation.data().tree_hash_root(), + ) + .await + .unwrap() + .unwrap() + .data; + let expected = attestation; - assert_eq!(&result, expected); - } - } else { - let attestation = self - .chain - .head_beacon_block() - .message() - .body() - .attestations() - .next() - .unwrap() - .clone_as_attestation(); + assert_eq!(result, expected); + + self + } + + pub async fn test_get_validator_aggregate_attestation_v2(self) -> Self { + let attestations = self + .chain + .naive_aggregation_pool + .read() + .iter() + .cloned() + .collect::>(); + for attestation in attestations { let result = self .client - .get_validator_aggregate_attestation_v1( + .get_validator_aggregate_attestation_v2( attestation.data().slot, attestation.data().tree_hash_root(), + attestation.committee_index().expect("committee index"), ) .await .unwrap() @@ -3617,7 +3621,6 @@ impl ApiTester { assert_eq!(result, expected); } - self } @@ -6805,19 +6808,36 @@ async fn get_validator_attestation_data_with_skip_slots() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn get_validator_aggregate_attestation() { +async fn get_validator_aggregate_attestation_v1() { ApiTester::new() .await - .test_get_validator_aggregate_attestation() + .test_get_validator_aggregate_attestation_v1() .await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn get_validator_aggregate_attestation_with_skip_slots() { +async fn get_validator_aggregate_attestation_v2() { + ApiTester::new() + .await + .test_get_validator_aggregate_attestation_v2() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_validator_aggregate_attestation_with_skip_slots_v1() { ApiTester::new() .await .skip_slots(E::slots_per_epoch() * 2) - .test_get_validator_aggregate_attestation() + .test_get_validator_aggregate_attestation_v1() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_validator_aggregate_attestation_with_skip_slots_v2() { + ApiTester::new() + .await + .skip_slots(E::slots_per_epoch() * 2) + .test_get_validator_aggregate_attestation_v2() .await; } diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 3c89ece442..b16ccc2a8c 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -22,6 +22,7 @@ hex = { workspace = true } itertools = { workspace = true } libp2p-mplex = "0.43" lighthouse_version = { workspace = true } +local-ip-address = "0.6" lru = { workspace = true } lru_cache = { workspace = true } metrics = { workspace = true } diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 55c1dbf491..5a6628439e 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -6,6 +6,7 @@ use directory::{ DEFAULT_BEACON_NODE_DIR, DEFAULT_HARDCODED_NETWORK, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR, }; use libp2p::Multiaddr; +use local_ip_address::local_ipv6; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::net::{Ipv4Addr, Ipv6Addr}; @@ -266,6 +267,18 @@ impl Config { } } + /// A helper function to check if the local host has a globally routeable IPv6 address. If so, + /// returns true. + pub fn is_ipv6_supported() -> bool { + // If IPv6 is supported + let Ok(std::net::IpAddr::V6(local_ip)) = local_ipv6() else { + return false; + }; + + // If its globally routable, return true + is_global_ipv6(&local_ip) + } + pub fn listen_addrs(&self) -> &ListenAddress { &self.listen_addresses } @@ -354,7 +367,7 @@ impl Default for Config { topics: Vec::new(), proposer_only: false, metrics_enabled: false, - enable_light_client_server: false, + enable_light_client_server: true, outbound_rate_limiter_config: None, invalid_block_storage: None, inbound_rate_limiter_config: None, diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 37cb5df6ea..8e5d6121e0 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -689,8 +689,8 @@ impl PeerDB { &mut self, supernode: bool, spec: &ChainSpec, + enr_key: CombinedKey, ) -> PeerId { - let enr_key = CombinedKey::generate_secp256k1(); let mut enr = Enr::builder().build(&enr_key).unwrap(); let peer_id = enr.peer_id(); diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index 2e8f462565..4cbff59ce2 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -234,6 +234,11 @@ impl PeerInfo { self.custody_subnets.contains(subnet) } + /// Returns an iterator on this peer's custody subnets + pub fn custody_subnets_iter(&self) -> impl Iterator { + self.custody_subnets.iter() + } + /// Returns true if the peer is connected to a long-lived subnet. pub fn has_long_lived_subnet(&self) -> bool { // Check the meta_data diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index ad6bea455e..2f6200a836 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -411,6 +411,27 @@ impl OldBlocksByRangeRequest { } } +impl From for OldBlocksByRangeRequest { + fn from(req: BlocksByRangeRequest) -> Self { + match req { + BlocksByRangeRequest::V1(ref req) => { + OldBlocksByRangeRequest::V1(OldBlocksByRangeRequestV1 { + start_slot: req.start_slot, + count: req.count, + step: 1, + }) + } + BlocksByRangeRequest::V2(ref req) => { + OldBlocksByRangeRequest::V2(OldBlocksByRangeRequestV2 { + start_slot: req.start_slot, + count: req.count, + step: 1, + }) + } + } + } +} + /// Request a number of beacon block bodies from a peer. #[superstruct(variants(V1, V2), variant_attributes(derive(Clone, Debug, PartialEq)))] #[derive(Clone, Debug, PartialEq)] diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 03f1395b8b..a91d2d1042 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -232,14 +232,14 @@ impl RPC { } } } else { - ToSwarm::NotifyHandler { - peer_id, - handler: NotifyHandler::Any, - event: RPCSend::Request(request_id, req), - } + RPCSend::Request(request_id, req) }; - self.events.push(event); + self.events.push(BehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event, + }); } /// Lighthouse wishes to disconnect from this peer by sending a Goodbye message. This diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index e0c8593f29..515e7d7244 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -35,7 +35,7 @@ pub(crate) struct SelfRateLimiter { /// Rate limiter for our own requests. limiter: RateLimiter, /// Requests that are ready to be sent. - ready_requests: SmallVec<[BehaviourAction; 3]>, + ready_requests: SmallVec<[(PeerId, RPCSend); 3]>, /// Slog logger. log: Logger, } @@ -76,7 +76,7 @@ impl SelfRateLimiter { peer_id: PeerId, request_id: Id, req: RequestType, - ) -> Result, Error> { + ) -> Result, Error> { let protocol = req.versioned_protocol().protocol(); // First check that there are not already other requests waiting to be sent. if let Some(queued_requests) = self.delayed_requests.get_mut(&(peer_id, protocol)) { @@ -108,13 +108,9 @@ impl SelfRateLimiter { request_id: Id, req: RequestType, log: &Logger, - ) -> Result, (QueuedRequest, Duration)> { + ) -> Result, (QueuedRequest, Duration)> { match limiter.allows(&peer_id, &req) { - Ok(()) => Ok(BehaviourAction::NotifyHandler { - peer_id, - handler: NotifyHandler::Any, - event: RPCSend::Request(request_id, req), - }), + Ok(()) => Ok(RPCSend::Request(request_id, req)), Err(e) => { let protocol = req.versioned_protocol(); match e { @@ -126,11 +122,7 @@ impl SelfRateLimiter { "Self rate limiting error for a batch that will never fit. Sending request anyway. Check configuration parameters."; "protocol" => %req.versioned_protocol().protocol() ); - Ok(BehaviourAction::NotifyHandler { - peer_id, - handler: NotifyHandler::Any, - event: RPCSend::Request(request_id, req), - }) + Ok(RPCSend::Request(request_id, req)) } RateLimitedErr::TooSoon(wait_time) => { debug!(log, "Self rate limiting"; "protocol" => %protocol.protocol(), "wait_time_ms" => wait_time.as_millis(), "peer_id" => %peer_id); @@ -156,7 +148,7 @@ impl SelfRateLimiter { // If one fails just wait for the next window that allows sending requests. return; } - Ok(event) => self.ready_requests.push(event), + Ok(event) => self.ready_requests.push((peer_id, event)), } } if queued_requests.is_empty() { @@ -203,8 +195,12 @@ impl SelfRateLimiter { let _ = self.limiter.poll_unpin(cx); // Finally return any queued events. - if !self.ready_requests.is_empty() { - return Poll::Ready(self.ready_requests.remove(0)); + if let Some((peer_id, event)) = self.ready_requests.pop() { + return Poll::Ready(BehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event, + }); } Poll::Pending @@ -217,7 +213,7 @@ mod tests { use crate::rpc::rate_limiter::Quota; use crate::rpc::self_limiter::SelfRateLimiter; use crate::rpc::{Ping, Protocol, RequestType}; - use crate::service::api_types::{AppRequestId, RequestId, SyncRequestId}; + use crate::service::api_types::{AppRequestId, RequestId, SingleLookupReqId, SyncRequestId}; use libp2p::PeerId; use std::time::Duration; use types::{EthSpec, ForkContext, Hash256, MainnetEthSpec, Slot}; @@ -238,12 +234,16 @@ mod tests { let mut limiter: SelfRateLimiter = SelfRateLimiter::new(config, fork_context, log).unwrap(); let peer_id = PeerId::random(); + let lookup_id = 0; for i in 1..=5u32 { let _ = limiter.allows( peer_id, - RequestId::Application(AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { - id: i, + RequestId::Application(AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { + lookup_id, + req_id: i, + }, })), RequestType::Ping(Ping { data: i as u64 }), ); @@ -261,9 +261,9 @@ mod tests { for i in 2..=5u32 { assert!(matches!( iter.next().unwrap().request_id, - RequestId::Application(AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { - id, - })) if id == i + RequestId::Application(AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { req_id, .. }, + })) if req_id == i, )); } @@ -286,9 +286,9 @@ mod tests { for i in 3..=5 { assert!(matches!( iter.next().unwrap().request_id, - RequestId::Application(AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { - id - })) if id == i + RequestId::Application(AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { req_id, .. }, + })) if req_id == i, )); } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 85fabbb0c3..e69c7aa5f7 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -1,15 +1,14 @@ -use std::sync::Arc; - -use libp2p::swarm::ConnectionId; -use types::{ - BlobSidecar, DataColumnSidecar, EthSpec, Hash256, LightClientBootstrap, - LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, SignedBeaconBlock, -}; - use crate::rpc::{ methods::{ResponseTermination, RpcResponse, RpcSuccessResponse, StatusMessage}, SubstreamId, }; +use libp2p::swarm::ConnectionId; +use std::fmt::{Display, Formatter}; +use std::sync::Arc; +use types::{ + BlobSidecar, DataColumnSidecar, Epoch, EthSpec, Hash256, LightClientBootstrap, + LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, SignedBeaconBlock, +}; /// Identifier of requests sent by a peer. pub type PeerRequestId = (ConnectionId, SubstreamId); @@ -31,8 +30,12 @@ pub enum SyncRequestId { SingleBlob { id: SingleLookupReqId }, /// Request searching for a set of data columns given a hash and list of column indices. DataColumnsByRoot(DataColumnsByRootRequestId), - /// Range request that is composed by both a block range request and a blob range request. - RangeBlockAndBlobs { id: Id }, + /// Blocks by range request + BlocksByRange(BlocksByRangeRequestId), + /// Blobs by range request + BlobsByRange(BlobsByRangeRequestId), + /// Data columns by range request + DataColumnsByRange(DataColumnsByRangeRequestId), } /// Request ID for data_columns_by_root requests. Block lookups do not issue this request directly. @@ -43,12 +46,60 @@ pub struct DataColumnsByRootRequestId { pub requester: DataColumnsByRootRequester, } +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct BlocksByRangeRequestId { + /// Id to identify this attempt at a blocks_by_range request for `parent_request_id` + pub id: Id, + /// The Id of the overall By Range request for block components. + pub parent_request_id: ComponentsByRangeRequestId, +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct BlobsByRangeRequestId { + /// Id to identify this attempt at a blobs_by_range request for `parent_request_id` + pub id: Id, + /// The Id of the overall By Range request for block components. + pub parent_request_id: ComponentsByRangeRequestId, +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct DataColumnsByRangeRequestId { + /// Id to identify this attempt at a data_columns_by_range request for `parent_request_id` + pub id: Id, + /// The Id of the overall By Range request for block components. + pub parent_request_id: ComponentsByRangeRequestId, +} + +/// Block components by range request for range sync. Includes an ID for downstream consumers to +/// handle retries and tie all their sub requests together. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct ComponentsByRangeRequestId { + /// Each `RangeRequestId` may request the same data in a later retry. This Id identifies the + /// current attempt. + pub id: Id, + /// What sync component is issuing a components by range request and expecting data back + pub requester: RangeRequestId, +} + +/// Range sync chain or backfill batch +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum RangeRequestId { + RangeSync { chain_id: Id, batch_id: Epoch }, + BackfillSync { batch_id: Epoch }, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum DataColumnsByRootRequester { Sampling(SamplingId), Custody(CustodyId), } +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum RangeRequester { + RangeSync { chain_id: u64, batch_id: Epoch }, + BackfillSync { batch_id: Epoch }, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct SamplingId { pub id: SamplingRequester, @@ -183,9 +234,108 @@ impl slog::Value for RequestId { } } -// This custom impl reduces log boilerplate not printing `DataColumnsByRootRequestId` on each id log -impl std::fmt::Display for DataColumnsByRootRequestId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} {:?}", self.id, self.requester) +macro_rules! impl_display { + ($structname: ty, $format: literal, $($field:ident),*) => { + impl Display for $structname { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, $format, $(self.$field,)*) + } + } + }; +} + +// Since each request Id is deeply nested with various types, if rendered with Debug on logs they +// take too much visual space. This custom Display implementations make the overall Id short while +// not losing information +impl_display!(BlocksByRangeRequestId, "{}/{}", id, parent_request_id); +impl_display!(BlobsByRangeRequestId, "{}/{}", id, parent_request_id); +impl_display!(DataColumnsByRangeRequestId, "{}/{}", id, parent_request_id); +impl_display!(ComponentsByRangeRequestId, "{}/{}", id, requester); +impl_display!(DataColumnsByRootRequestId, "{}/{}", id, requester); +impl_display!(SingleLookupReqId, "{}/Lookup/{}", req_id, lookup_id); +impl_display!(CustodyId, "{}", requester); +impl_display!(SamplingId, "{}/{}", sampling_request_id, id); + +impl Display for DataColumnsByRootRequester { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Custody(id) => write!(f, "Custody/{id}"), + Self::Sampling(id) => write!(f, "Sampling/{id}"), + } + } +} + +impl Display for CustodyRequester { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Display for RangeRequestId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::RangeSync { chain_id, batch_id } => write!(f, "RangeSync/{batch_id}/{chain_id}"), + Self::BackfillSync { batch_id } => write!(f, "BackfillSync/{batch_id}"), + } + } +} + +impl Display for SamplingRequestId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Display for SamplingRequester { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::ImportedBlock(block) => write!(f, "ImportedBlock/{block}"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_id_data_columns_by_root_custody() { + let id = DataColumnsByRootRequestId { + id: 123, + requester: DataColumnsByRootRequester::Custody(CustodyId { + requester: CustodyRequester(SingleLookupReqId { + req_id: 121, + lookup_id: 101, + }), + }), + }; + assert_eq!(format!("{id}"), "123/Custody/121/Lookup/101"); + } + + #[test] + fn display_id_data_columns_by_root_sampling() { + let id = DataColumnsByRootRequestId { + id: 123, + requester: DataColumnsByRootRequester::Sampling(SamplingId { + id: SamplingRequester::ImportedBlock(Hash256::ZERO), + sampling_request_id: SamplingRequestId(101), + }), + }; + assert_eq!(format!("{id}"), "123/Sampling/101/ImportedBlock/0x0000000000000000000000000000000000000000000000000000000000000000"); + } + + #[test] + fn display_id_data_columns_by_range() { + let id = DataColumnsByRangeRequestId { + id: 123, + parent_request_id: ComponentsByRangeRequestId { + id: 122, + requester: RangeRequestId::RangeSync { + chain_id: 54, + batch_id: Epoch::new(0), + }, + }, + }; + assert_eq!(format!("{id}"), "123/122/RangeSync/0/54"); } } diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 354def79b0..48481c2e1d 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -14,9 +14,8 @@ use crate::rpc::{ RequestType, ResponseTermination, RpcErrorResponse, RpcResponse, RpcSuccessResponse, RPC, }; use crate::types::{ - attestation_sync_committee_topics, fork_core_topics, subnet_from_topic_hash, GossipEncoding, - GossipKind, GossipTopic, SnappyTransform, Subnet, SubnetDiscovery, ALTAIR_CORE_TOPICS, - BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, + all_topics_at_fork, core_topics_to_subscribe, is_fork_non_core_topic, subnet_from_topic_hash, + GossipEncoding, GossipKind, GossipTopic, SnappyTransform, Subnet, SubnetDiscovery, }; use crate::EnrExt; use crate::Eth2Enr; @@ -280,14 +279,39 @@ impl Network { // Set up a scoring update interval let update_gossipsub_scores = tokio::time::interval(params.decay_interval); - let max_topics = ctx.chain_spec.attestation_subnet_count as usize - + SYNC_COMMITTEE_SUBNET_COUNT as usize - + ctx.chain_spec.blob_sidecar_subnet_count_max() as usize - + ctx.chain_spec.data_column_sidecar_subnet_count as usize - + BASE_CORE_TOPICS.len() - + ALTAIR_CORE_TOPICS.len() - + CAPELLA_CORE_TOPICS.len() // 0 core deneb and electra topics - + LIGHT_CLIENT_GOSSIP_TOPICS.len(); + let current_and_future_forks = ForkName::list_all().into_iter().filter_map(|fork| { + if fork >= ctx.fork_context.current_fork() { + ctx.fork_context + .to_context_bytes(fork) + .map(|fork_digest| (fork, fork_digest)) + } else { + None + } + }); + + let all_topics_for_forks = current_and_future_forks + .map(|(fork, fork_digest)| { + all_topics_at_fork::(fork, &ctx.chain_spec) + .into_iter() + .map(|topic| { + Topic::new(GossipTopic::new( + topic, + GossipEncoding::default(), + fork_digest, + )) + .into() + }) + .collect::>() + }) + .collect::>(); + + // For simplicity find the fork with the most individual topics and assume all forks + // have the same topic count + let max_topics_at_any_fork = all_topics_for_forks + .iter() + .map(|topics| topics.len()) + .max() + .expect("each fork has at least 5 hardcoded core topics"); let possible_fork_digests = ctx.fork_context.all_fork_digests(); let filter = gossipsub::MaxCountSubscriptionFilter { @@ -297,9 +321,9 @@ impl Network { SYNC_COMMITTEE_SUBNET_COUNT, ), // during a fork we subscribe to both the old and new topics - max_subscribed_topics: max_topics * 4, + max_subscribed_topics: max_topics_at_any_fork * 4, // 424 in theory = (64 attestation + 4 sync committee + 7 core topics + 9 blob topics + 128 column topics) * 2 - max_subscriptions_per_request: max_topics * 2, + max_subscriptions_per_request: max_topics_at_any_fork * 2, }; // If metrics are enabled for libp2p build the configuration @@ -332,17 +356,9 @@ impl Network { // If we are using metrics, then register which topics we want to make sure to keep // track of if ctx.libp2p_registry.is_some() { - let topics_to_keep_metrics_for = attestation_sync_committee_topics::() - .map(|gossip_kind| { - Topic::from(GossipTopic::new( - gossip_kind, - GossipEncoding::default(), - enr_fork_id.fork_digest, - )) - .into() - }) - .collect::>(); - gossipsub.register_topics_for_metrics(topics_to_keep_metrics_for); + for topics in all_topics_for_forks { + gossipsub.register_topics_for_metrics(topics); + } } (gossipsub, update_gossipsub_scores) @@ -700,32 +716,26 @@ impl Network { /// Subscribe to all required topics for the `new_fork` with the given `new_fork_digest`. pub fn subscribe_new_fork_topics(&mut self, new_fork: ForkName, new_fork_digest: [u8; 4]) { - // Subscribe to existing topics with new fork digest + // Re-subscribe to non-core topics with the new fork digest let subscriptions = self.network_globals.gossipsub_subscriptions.read().clone(); for mut topic in subscriptions.into_iter() { - topic.fork_digest = new_fork_digest; - self.subscribe(topic); + if is_fork_non_core_topic(&topic, new_fork) { + topic.fork_digest = new_fork_digest; + self.subscribe(topic); + } } // Subscribe to core topics for the new fork - for kind in fork_core_topics::(&new_fork, &self.fork_context.spec) { + for kind in core_topics_to_subscribe::( + new_fork, + &self.network_globals.as_topic_config(), + &self.fork_context.spec, + ) { let topic = GossipTopic::new(kind, GossipEncoding::default(), new_fork_digest); self.subscribe(topic); } - // Register the new topics for metrics - let topics_to_keep_metrics_for = attestation_sync_committee_topics::() - .map(|gossip_kind| { - Topic::from(GossipTopic::new( - gossip_kind, - GossipEncoding::default(), - new_fork_digest, - )) - .into() - }) - .collect::>(); - self.gossipsub_mut() - .register_topics_for_metrics(topics_to_keep_metrics_for); + // Already registered all possible gossipsub topics for metrics } /// Unsubscribe from all topics that doesn't have the given fork_digest diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index c9e84e2dd1..d243c68c0f 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -1,4 +1,5 @@ //! A collection of variables that are accessible outside of the network thread itself. +use super::TopicConfig; use crate::peer_manager::peerdb::PeerDB; use crate::rpc::{MetaData, MetaDataV3}; use crate::types::{BackFillState, SyncState}; @@ -183,6 +184,16 @@ impl NetworkGlobals { .collect::>() } + /// Returns the TopicConfig to compute the set of Gossip topics for a given fork + pub fn as_topic_config(&self) -> TopicConfig { + TopicConfig { + enable_light_client_server: self.config.enable_light_client_server, + subscribe_all_subnets: self.config.subscribe_all_subnets, + subscribe_all_data_column_subnets: self.config.subscribe_all_data_column_subnets, + sampling_subnets: &self.sampling_subnets, + } + } + /// TESTING ONLY. Build a dummy NetworkGlobals instance. pub fn new_test_globals( trusted_peers: Vec, diff --git a/beacon_node/lighthouse_network/src/types/mod.rs b/beacon_node/lighthouse_network/src/types/mod.rs index a1eedaef74..db92f05b8f 100644 --- a/beacon_node/lighthouse_network/src/types/mod.rs +++ b/beacon_node/lighthouse_network/src/types/mod.rs @@ -16,7 +16,6 @@ pub use pubsub::{PubsubMessage, SnappyTransform}; pub use subnet::{Subnet, SubnetDiscovery}; pub use sync_state::{BackFillState, SyncState}; pub use topics::{ - attestation_sync_committee_topics, core_topics_to_subscribe, fork_core_topics, - subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, ALTAIR_CORE_TOPICS, - BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, + all_topics_at_fork, core_topics_to_subscribe, is_fork_non_core_topic, subnet_from_topic_hash, + GossipEncoding, GossipKind, GossipTopic, TopicConfig, }; diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index cd956e3bf1..31158fb1c5 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -1,5 +1,6 @@ use gossipsub::{IdentTopic as Topic, TopicHash}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use strum::AsRefStr; use types::{ChainSpec, DataColumnSubnetId, EthSpec, ForkName, SubnetId, SyncSubnetId, Unsigned}; @@ -25,81 +26,115 @@ pub const LIGHT_CLIENT_FINALITY_UPDATE: &str = "light_client_finality_update"; pub const LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = "light_client_optimistic_update"; pub const INCLUSION_LIST_TOPIC: &str = "inclusion_list"; -pub const BASE_CORE_TOPICS: [GossipKind; 5] = [ - GossipKind::BeaconBlock, - GossipKind::BeaconAggregateAndProof, - GossipKind::VoluntaryExit, - GossipKind::ProposerSlashing, - GossipKind::AttesterSlashing, -]; - -pub const ALTAIR_CORE_TOPICS: [GossipKind; 1] = [GossipKind::SignedContributionAndProof]; - -pub const CAPELLA_CORE_TOPICS: [GossipKind; 1] = [GossipKind::BlsToExecutionChange]; - -pub const LIGHT_CLIENT_GOSSIP_TOPICS: [GossipKind; 2] = [ - GossipKind::LightClientFinalityUpdate, - GossipKind::LightClientOptimisticUpdate, -]; - -pub const FULU_CORE_TOPICS: [GossipKind; 1] = [GossipKind::InclusionList]; - -/// Returns the core topics associated with each fork that are new to the previous fork -pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> Vec { - match fork_name { - ForkName::Base => BASE_CORE_TOPICS.to_vec(), - ForkName::Altair => ALTAIR_CORE_TOPICS.to_vec(), - ForkName::Bellatrix => vec![], - ForkName::Capella => CAPELLA_CORE_TOPICS.to_vec(), - ForkName::Deneb => { - // All of deneb blob topics are core topics - let mut deneb_blob_topics = Vec::new(); - for i in 0..spec.blob_sidecar_subnet_count(ForkName::Deneb) { - deneb_blob_topics.push(GossipKind::BlobSidecar(i)); - } - deneb_blob_topics - } - ForkName::Electra => { - // All of electra blob topics are core topics - let mut electra_blob_topics = Vec::new(); - for i in 0..spec.blob_sidecar_subnet_count(ForkName::Electra) { - electra_blob_topics.push(GossipKind::BlobSidecar(i)); - } - electra_blob_topics - } - ForkName::Fulu => FULU_CORE_TOPICS.to_vec(), - } +#[derive(Debug)] +pub struct TopicConfig<'a> { + pub enable_light_client_server: bool, + pub subscribe_all_subnets: bool, + pub subscribe_all_data_column_subnets: bool, + pub sampling_subnets: &'a HashSet, } -/// Returns all the attestation and sync committee topics, for a given fork. -pub fn attestation_sync_committee_topics() -> impl Iterator { - (0..E::SubnetBitfieldLength::to_usize()) - .map(|subnet_id| GossipKind::Attestation(SubnetId::new(subnet_id as u64))) - .chain( - (0..E::SyncCommitteeSubnetCount::to_usize()).map(|sync_committee_id| { - GossipKind::SyncCommitteeMessage(SyncSubnetId::new(sync_committee_id as u64)) - }), - ) -} - -/// Returns all the topics that we need to subscribe to for a given fork -/// including topics from older forks and new topics for the current fork. +/// Returns all the topics the node should subscribe at `fork_name` pub fn core_topics_to_subscribe( - mut current_fork: ForkName, + fork_name: ForkName, + opts: &TopicConfig, spec: &ChainSpec, ) -> Vec { - let mut topics = fork_core_topics::(¤t_fork, spec); - while let Some(previous_fork) = current_fork.previous_fork() { - let previous_fork_topics = fork_core_topics::(&previous_fork, spec); - topics.extend(previous_fork_topics); - current_fork = previous_fork; + let mut topics = vec![ + GossipKind::BeaconBlock, + GossipKind::BeaconAggregateAndProof, + GossipKind::VoluntaryExit, + GossipKind::ProposerSlashing, + GossipKind::AttesterSlashing, + ]; + + if opts.subscribe_all_subnets { + for i in 0..spec.attestation_subnet_count { + topics.push(GossipKind::Attestation(i.into())); + } } - // Remove duplicates + + if fork_name.altair_enabled() { + topics.push(GossipKind::SignedContributionAndProof); + + if opts.subscribe_all_subnets { + for i in 0..E::SyncCommitteeSubnetCount::to_u64() { + topics.push(GossipKind::SyncCommitteeMessage(i.into())); + } + } + + if opts.enable_light_client_server { + topics.push(GossipKind::LightClientFinalityUpdate); + topics.push(GossipKind::LightClientOptimisticUpdate); + } + } + + if fork_name.capella_enabled() { + topics.push(GossipKind::BlsToExecutionChange); + } + + if fork_name.deneb_enabled() && !fork_name.fulu_enabled() { + // All of deneb blob topics are core topics + for i in 0..spec.blob_sidecar_subnet_count(fork_name) { + topics.push(GossipKind::BlobSidecar(i)); + } + } + + if fork_name.electra_enabled() { + topics.push(GossipKind::InclusionList); + } + + if fork_name.fulu_enabled() { + if opts.subscribe_all_data_column_subnets { + for i in 0..spec.data_column_sidecar_subnet_count { + topics.push(GossipKind::DataColumnSidecar(i.into())); + } + } else { + for subnet in opts.sampling_subnets { + topics.push(GossipKind::DataColumnSidecar(*subnet)); + } + } + } + topics - .into_iter() - .collect::>() - .into_iter() - .collect() +} + +/// Returns true if a given non-core `GossipTopic` MAY be subscribe at this fork. +/// +/// For example: the `Attestation` topic is not subscribed as a core topic if +/// subscribe_all_subnets = false` but we may subscribe to it outside of a fork +/// boundary if the node is an aggregator. +pub fn is_fork_non_core_topic(topic: &GossipTopic, _fork_name: ForkName) -> bool { + match topic.kind() { + // Node may be aggregator of attestation and sync_committee_message topics for all known + // forks + GossipKind::Attestation(_) | GossipKind::SyncCommitteeMessage(_) => true, + // All these topics are core-only + GossipKind::BeaconBlock + | GossipKind::BeaconAggregateAndProof + | GossipKind::BlobSidecar(_) + | GossipKind::DataColumnSidecar(_) + | GossipKind::VoluntaryExit + | GossipKind::ProposerSlashing + | GossipKind::AttesterSlashing + | GossipKind::SignedContributionAndProof + | GossipKind::BlsToExecutionChange + | GossipKind::LightClientFinalityUpdate + | GossipKind::LightClientOptimisticUpdate + | GossipKind::InclusionList => false, + } +} + +pub fn all_topics_at_fork(fork: ForkName, spec: &ChainSpec) -> Vec { + // Compute the worst case of all forks + let sampling_subnets = HashSet::from_iter(spec.all_data_column_sidecar_subnets()); + let opts = TopicConfig { + enable_light_client_server: true, + subscribe_all_subnets: true, + subscribe_all_data_column_subnets: true, + sampling_subnets: &sampling_subnets, + }; + core_topics_to_subscribe::(fork, &opts, spec) } /// A gossipsub topic which encapsulates the type of messages that should be sent and received over @@ -349,10 +384,9 @@ fn subnet_topic_index(topic: &str) -> Option { #[cfg(test)] mod tests { - use types::MainnetEthSpec; - use super::GossipKind::*; use super::*; + use types::{Epoch, MainnetEthSpec as E}; const GOOD_FORK_DIGEST: &str = "e1925f3b"; const BAD_PREFIX: &str = "tezos"; @@ -477,24 +511,94 @@ mod tests { assert_eq!("attester_slashing", AttesterSlashing.as_ref()); } + fn get_spec() -> ChainSpec { + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(1)); + spec.bellatrix_fork_epoch = Some(Epoch::new(2)); + spec.capella_fork_epoch = Some(Epoch::new(3)); + spec.deneb_fork_epoch = Some(Epoch::new(4)); + spec.electra_fork_epoch = Some(Epoch::new(5)); + spec.fulu_fork_epoch = Some(Epoch::new(6)); + spec + } + + fn get_sampling_subnets() -> HashSet { + HashSet::new() + } + + fn get_topic_config(sampling_subnets: &HashSet) -> TopicConfig { + TopicConfig { + enable_light_client_server: false, + subscribe_all_subnets: false, + subscribe_all_data_column_subnets: false, + sampling_subnets, + } + } + + #[test] + fn base_topics_are_always_active() { + let spec = get_spec(); + let s = get_sampling_subnets(); + let topic_config = get_topic_config(&s); + for fork in ForkName::list_all() { + assert!(core_topics_to_subscribe::(fork, &topic_config, &spec,) + .contains(&GossipKind::BeaconBlock)); + } + } + + #[test] + fn blobs_are_not_subscribed_in_peerdas() { + let spec = get_spec(); + let s = get_sampling_subnets(); + let topic_config = get_topic_config(&s); + assert!( + !core_topics_to_subscribe::(ForkName::Fulu, &topic_config, &spec,) + .contains(&GossipKind::BlobSidecar(0)) + ); + } + + #[test] + fn columns_are_subscribed_in_peerdas() { + let spec = get_spec(); + let s = get_sampling_subnets(); + let mut topic_config = get_topic_config(&s); + topic_config.subscribe_all_data_column_subnets = true; + assert!( + core_topics_to_subscribe::(ForkName::Fulu, &topic_config, &spec) + .contains(&GossipKind::DataColumnSidecar(0.into())) + ); + } + #[test] fn test_core_topics_to_subscribe() { - type E = MainnetEthSpec; - let spec = E::default_spec(); - let mut all_topics = Vec::new(); - let mut electra_core_topics = fork_core_topics::(&ForkName::Electra, &spec); - let mut deneb_core_topics = fork_core_topics::(&ForkName::Deneb, &spec); - all_topics.append(&mut electra_core_topics); - all_topics.append(&mut deneb_core_topics); - all_topics.extend(CAPELLA_CORE_TOPICS); - all_topics.extend(ALTAIR_CORE_TOPICS); - all_topics.extend(BASE_CORE_TOPICS); - + let spec = get_spec(); + let s = HashSet::from_iter([1, 2].map(DataColumnSubnetId::new)); + let mut topic_config = get_topic_config(&s); + topic_config.enable_light_client_server = true; let latest_fork = *ForkName::list_all().last().unwrap(); - let core_topics = core_topics_to_subscribe::(latest_fork, &spec); + let topics = core_topics_to_subscribe::(latest_fork, &topic_config, &spec); + + let mut expected_topics = vec![ + GossipKind::BeaconBlock, + GossipKind::BeaconAggregateAndProof, + GossipKind::VoluntaryExit, + GossipKind::ProposerSlashing, + GossipKind::AttesterSlashing, + GossipKind::SignedContributionAndProof, + GossipKind::LightClientFinalityUpdate, + GossipKind::LightClientOptimisticUpdate, + GossipKind::BlsToExecutionChange, + ]; + for subnet in s { + expected_topics.push(GossipKind::DataColumnSidecar(subnet)); + } // Need to check all the topics exist in an order independent manner - for topic in all_topics { - assert!(core_topics.contains(&topic)); + for expected_topic in expected_topics { + assert!( + topics.contains(&expected_topic), + "Should contain {:?}", + expected_topic + ); } } } diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 09179c4a51..5071e247a3 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -10,8 +10,10 @@ eth2 = { workspace = true } eth2_network_config = { workspace = true } genesis = { workspace = true } gossipsub = { workspace = true } +k256 = "0.13.4" kzg = { workspace = true } matches = "0.1.8" +rand_chacha = "0.3.1" serde_json = { workspace = true } slog-async = { workspace = true } slog-term = { workspace = true } diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 154a59eade..7c38ae9d75 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -14,6 +14,7 @@ pub use metrics::*; use std::sync::{Arc, LazyLock}; use strum::AsRefStr; use strum::IntoEnumIterator; +use types::DataColumnSubnetId; use types::EthSpec; pub const SUCCESS: &str = "SUCCESS"; @@ -374,11 +375,18 @@ pub static PEERS_PER_SYNC_TYPE: LazyLock> = LazyLock::new(|| }); pub static PEERS_PER_COLUMN_SUBNET: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( - "peers_per_column_subnet", + "sync_peers_per_column_subnet", "Number of connected peers per column subnet", &["subnet_id"], ) }); +pub static PEERS_PER_CUSTODY_COLUMN_SUBNET: LazyLock> = LazyLock::new(|| { + try_create_int_gauge_vec( + "sync_peers_per_custody_column_subnet", + "Number of connected peers per custody column subnet", + &["subnet_id"], + ) +}); pub static SYNCING_CHAINS_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( "sync_range_chains", @@ -746,16 +754,42 @@ pub fn update_sync_metrics(network_globals: &Arc>) // count per sync status, the number of connected peers let mut peers_per_sync_type = FnvHashMap::default(); - for sync_type in network_globals - .peers - .read() - .connected_peers() - .map(|(_peer_id, info)| info.sync_status().as_str()) - { + let mut peers_per_column_subnet = FnvHashMap::default(); + + for (_, info) in network_globals.peers.read().connected_peers() { + let sync_type = info.sync_status().as_str(); *peers_per_sync_type.entry(sync_type).or_default() += 1; + + for subnet in info.custody_subnets_iter() { + *peers_per_column_subnet.entry(*subnet).or_default() += 1; + } } for (sync_type, peer_count) in peers_per_sync_type { set_gauge_entry(&PEERS_PER_SYNC_TYPE, &[sync_type], peer_count); } + + let all_column_subnets = + (0..network_globals.spec.data_column_sidecar_subnet_count).map(DataColumnSubnetId::new); + let custody_column_subnets = network_globals.sampling_subnets.iter(); + + // Iterate all subnet values to set to zero the empty entries in peers_per_column_subnet + for subnet in all_column_subnets { + set_gauge_entry( + &PEERS_PER_COLUMN_SUBNET, + &[&format!("{subnet}")], + peers_per_column_subnet.get(&subnet).copied().unwrap_or(0), + ); + } + + // Registering this metric is a duplicate for supernodes but helpful for fullnodes. This way + // operators can monitor the health of only the subnets of their interest without complex + // Grafana queries. + for subnet in custody_column_subnets { + set_gauge_entry( + &PEERS_PER_CUSTODY_COLUMN_SUBNET, + &[&format!("{subnet}")], + peers_per_column_subnet.get(subnet).copied().unwrap_or(0), + ); + } } diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 08b39b9f4a..3a12ef1d43 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -17,6 +17,7 @@ use beacon_chain::{ light_client_finality_update_verification::Error as LightClientFinalityUpdateError, light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError, observed_operations::ObservationOutcome, + single_attestation::single_attestation_to_attestation, sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::{get_block_delay_ms, get_slot_delay_ms}, AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, @@ -35,12 +36,12 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; use tokio::sync::mpsc; use types::{ - beacon_block::BlockImportSource, Attestation, AttestationRef, AttesterSlashing, BlobSidecar, - DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation, - LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, + beacon_block::BlockImportSource, Attestation, AttestationData, AttestationRef, + AttesterSlashing, BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, + IndexedAttestation, LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedInclusionList, SignedVoluntaryExit, Slot, SubnetId, - SyncCommitteeMessage, SyncSubnetId, + SignedContributionAndProof, SignedInclusionList, SignedVoluntaryExit, SingleAttestation, Slot, + SubnetId, SyncCommitteeMessage, SyncSubnetId, }; use beacon_processor::{ @@ -48,7 +49,7 @@ use beacon_processor::{ QueuedAggregate, QueuedGossipBlock, QueuedLightClientUpdate, QueuedUnaggregate, ReprocessQueueMessage, }, - DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, + DuplicateCache, GossipAggregatePackage, GossipAttestationBatch, }; /// Set to `true` to introduce stricter penalties for peers who send some types of late consensus @@ -130,6 +131,11 @@ enum FailedAtt { should_import: bool, seen_timestamp: Duration, }, + // This variant is just a dummy variant for now, as SingleAttestation reprocessing is handled + // separately. + SingleUnaggregate { + attestation: Box, + }, Aggregate { attestation: Box>, seen_timestamp: Duration, @@ -138,20 +144,22 @@ enum FailedAtt { impl FailedAtt { pub fn beacon_block_root(&self) -> &Hash256 { - &self.attestation().data().beacon_block_root + &self.attestation_data().beacon_block_root } pub fn kind(&self) -> &'static str { match self { FailedAtt::Unaggregate { .. } => "unaggregated", + FailedAtt::SingleUnaggregate { .. } => "unaggregated", FailedAtt::Aggregate { .. } => "aggregated", } } - pub fn attestation(&self) -> AttestationRef { + pub fn attestation_data(&self) -> &AttestationData { match self { - FailedAtt::Unaggregate { attestation, .. } => attestation.to_ref(), - FailedAtt::Aggregate { attestation, .. } => attestation.message().aggregate(), + FailedAtt::Unaggregate { attestation, .. } => attestation.data(), + FailedAtt::SingleUnaggregate { attestation, .. } => &attestation.data, + FailedAtt::Aggregate { attestation, .. } => attestation.message().aggregate().data(), } } } @@ -232,7 +240,7 @@ impl NetworkBeaconProcessor { pub fn process_gossip_attestation_batch( self: Arc, - packages: Vec>, + packages: GossipAttestationBatch, reprocess_tx: Option>, ) { let attestations_and_subnets = packages @@ -402,6 +410,155 @@ impl NetworkBeaconProcessor { } } + /// Process an unaggregated attestation requiring conversion. + /// + /// This function performs the conversion, and if successfull queues a new message to be + /// processed by `process_gossip_attestation`. If unsuccessful due to block unavailability, + /// a retry message will be pushed to the `reprocess_tx` if it is `Some`. + #[allow(clippy::too_many_arguments)] + pub fn process_gossip_attestation_to_convert( + self: Arc, + message_id: MessageId, + peer_id: PeerId, + single_attestation: Box, + subnet_id: SubnetId, + should_import: bool, + reprocess_tx: Option>, + seen_timestamp: Duration, + ) { + let conversion_result = self.chain.with_committee_cache( + single_attestation.data.target.root, + single_attestation + .data + .slot + .epoch(T::EthSpec::slots_per_epoch()), + |committee_cache, _| { + let slot = single_attestation.data.slot; + let committee_index = single_attestation.committee_index; + let Some(committee) = committee_cache.get_beacon_committee(slot, committee_index) + else { + return Ok(Err(AttnError::NoCommitteeForSlotAndIndex { + slot, + index: committee_index, + })); + }; + + Ok(single_attestation_to_attestation( + &single_attestation, + committee.committee, + )) + }, + ); + + match conversion_result { + Ok(Ok(attestation)) => { + let slot = attestation.data().slot; + if let Err(e) = self.send_unaggregated_attestation( + message_id.clone(), + peer_id, + attestation, + subnet_id, + should_import, + seen_timestamp, + ) { + error!( + &self.log, + "Unable to queue converted SingleAttestation"; + "error" => %e, + "slot" => slot, + ); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); + } + } + // Outermost error (from `with_committee_cache`) indicating that the block is not known + // and that this conversion should be retried. + Err(BeaconChainError::MissingBeaconBlock(beacon_block_root)) => { + if let Some(sender) = reprocess_tx { + metrics::inc_counter( + &metrics::BEACON_PROCESSOR_UNAGGREGATED_ATTESTATION_REQUEUED_TOTAL, + ); + // We don't know the block, get the sync manager to handle the block lookup, and + // send the attestation to be scheduled for re-processing. + self.sync_tx + .send(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + beacon_block_root, + )) + .unwrap_or_else(|_| { + warn!( + self.log, + "Failed to send to sync service"; + "msg" => "UnknownBlockHash" + ) + }); + let processor = self.clone(); + // Do not allow this attestation to be re-processed beyond this point. + let reprocess_msg = + ReprocessQueueMessage::UnknownBlockUnaggregate(QueuedUnaggregate { + beacon_block_root, + process_fn: Box::new(move || { + processor.process_gossip_attestation_to_convert( + message_id, + peer_id, + single_attestation, + subnet_id, + should_import, + None, + seen_timestamp, + ) + }), + }); + if sender.try_send(reprocess_msg).is_err() { + error!( + self.log, + "Failed to send attestation for re-processing"; + ) + } + } else { + // We shouldn't make any further attempts to process this attestation. + // + // Don't downscore the peer since it's not clear if we requested this head + // block from them or not. + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); + } + } + Ok(Err(error)) => { + // We already handled reprocessing above so do not attempt it in the error handler. + self.handle_attestation_verification_failure( + peer_id, + message_id, + FailedAtt::SingleUnaggregate { + attestation: single_attestation, + }, + None, + error, + seen_timestamp, + ); + } + Err(error) => { + // We already handled reprocessing above so do not attempt it in the error handler. + self.handle_attestation_verification_failure( + peer_id, + message_id, + FailedAtt::SingleUnaggregate { + attestation: single_attestation, + }, + None, + AttnError::BeaconChainError(error), + seen_timestamp, + ); + } + } + } + /// Process the aggregated attestation received from the gossip network and: /// /// - If it passes gossip propagation criteria, tell the network thread to forward it. @@ -1323,22 +1480,13 @@ impl NetworkBeaconProcessor { ); return None; } - Err(e @ BlockError::InternalError(_)) => { + // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` + Err(e @ BlockError::InternalError(_)) | Err(e @ BlockError::BlobNotRequired(_)) => { error!(self.log, "Internal block gossip validation error"; "error" => %e ); return None; } - Err(e @ BlockError::BlobNotRequired(_)) => { - // TODO(das): penalty not implemented yet as other clients may still send us blobs - // during early stage of implementation. - debug!(self.log, "Received blobs for slot after PeerDAS epoch from peer"; - "error" => %e, - "peer_id" => %peer_id, - ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - return None; - } }; metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_VERIFIED_TOTAL); @@ -1449,9 +1597,10 @@ impl NetworkBeaconProcessor { let block = verified_block.block.block_cloned(); let block_root = verified_block.block_root; - // TODO(das) Might be too early to issue a request here. We haven't checked that the block - // actually includes blob transactions and thus has data. A peer could send a block is - // garbage commitments, and make us trigger sampling for a block that does not have data. + // Note: okay to issue sampling request before the block is execution verified. If the + // proposer sends us a block with invalid blob transactions it can trigger us to issue + // sampling queries that will never resolve. This attack is equivalent to withholding data. + // Dismissed proposal to move this block to post-execution: https://github.com/sigp/lighthouse/pull/6492 if block.num_expected_blobs() > 0 { // Trigger sampling for block not yet execution valid. At this point column custodials are // unlikely to have received their columns. Triggering sampling so early is only viable with @@ -2244,9 +2393,9 @@ impl NetworkBeaconProcessor { // network. let seen_clock = &self.chain.slot_clock.freeze_at(seen_timestamp); let hindsight_verification = - attestation_verification::verify_propagation_slot_range( + attestation_verification::verify_propagation_slot_range::<_, T::EthSpec>( seen_clock, - failed_att.attestation(), + failed_att.attestation_data(), &self.chain.spec, ); @@ -2331,6 +2480,19 @@ impl NetworkBeaconProcessor { "attn_agg_not_in_committee", ); } + AttnError::AttesterNotInCommittee { .. } => { + /* + * `SingleAttestation` from a validator is invalid because the `attester_index` is + * not in the claimed committee. There is no reason a non-faulty validator would + * send this message. + */ + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "attn_single_not_in_committee", + ); + } AttnError::AttestationSupersetKnown { .. } => { /* * The aggregate attestation has already been observed on the network or in @@ -2476,6 +2638,17 @@ impl NetworkBeaconProcessor { }), }) } + FailedAtt::SingleUnaggregate { .. } => { + // This should never happen, as we handle the unknown head block case + // for `SingleAttestation`s separately and should not be able to hit + // an `UnknownHeadBlock` error. + error!( + self.log, + "Dropping SingleAttestation instead of requeueing"; + "block_root" => ?beacon_block_root, + ); + return; + } FailedAtt::Unaggregate { attestation, subnet_id, @@ -2698,7 +2871,7 @@ impl NetworkBeaconProcessor { self.log, "Ignored attestation to finalized block"; "block_root" => ?beacon_block_root, - "attestation_slot" => failed_att.attestation().data().slot, + "attestation_slot" => failed_att.attestation_data().slot, ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); @@ -2721,9 +2894,9 @@ impl NetworkBeaconProcessor { debug!( self.log, "Dropping attestation"; - "target_root" => ?failed_att.attestation().data().target.root, + "target_root" => ?failed_att.attestation_data().target.root, "beacon_block_root" => ?beacon_block_root, - "slot" => ?failed_att.attestation().data().slot, + "slot" => ?failed_att.attestation_data().slot, "type" => ?attestation_type, "error" => ?e, "peer_id" => % peer_id @@ -2742,7 +2915,7 @@ impl NetworkBeaconProcessor { self.log, "Unable to validate attestation"; "beacon_block_root" => ?beacon_block_root, - "slot" => ?failed_att.attestation().data().slot, + "slot" => ?failed_att.attestation_data().slot, "type" => ?attestation_type, "peer_id" => %peer_id, "error" => ?e, @@ -3143,9 +3316,9 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, ) { - let is_timely = attestation_verification::verify_propagation_slot_range( + let is_timely = attestation_verification::verify_propagation_slot_range::<_, T::EthSpec>( &self.chain.slot_clock, - attestation, + attestation.data(), &self.chain.spec, ) .is_ok(); diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 12d3f41465..eb792029d0 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -94,46 +94,34 @@ impl NetworkBeaconProcessor { should_import: bool, seen_timestamp: Duration, ) -> Result<(), Error> { - let result = self.chain.with_committee_cache( - single_attestation.data.target.root, - single_attestation - .data - .slot - .epoch(T::EthSpec::slots_per_epoch()), - |committee_cache, _| { - let Some(committee) = committee_cache.get_beacon_committee( - single_attestation.data.slot, - single_attestation.committee_index, - ) else { - warn!( - self.log, - "No beacon committee for slot and index"; - "slot" => single_attestation.data.slot, - "index" => single_attestation.committee_index - ); - return Ok(Ok(())); - }; + let processor = self.clone(); + let process_individual = move |package: GossipAttestationPackage| { + let reprocess_tx = processor.reprocess_tx.clone(); + processor.process_gossip_attestation_to_convert( + package.message_id, + package.peer_id, + package.attestation, + package.subnet_id, + package.should_import, + Some(reprocess_tx), + package.seen_timestamp, + ) + }; - let attestation = single_attestation.to_attestation(committee.committee)?; - - Ok(self.send_unaggregated_attestation( - message_id.clone(), + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::GossipAttestationToConvert { + attestation: Box::new(GossipAttestationPackage { + message_id, peer_id, - attestation, + attestation: Box::new(single_attestation), subnet_id, should_import, seen_timestamp, - )) + }), + process_individual: Box::new(process_individual), }, - ); - - match result { - Ok(result) => result, - Err(e) => { - warn!(self.log, "Failed to send SingleAttestation"; "error" => ?e); - Ok(()) - } - } + }) } /// Create a new `Work` event for some unaggregated attestation. @@ -148,18 +136,19 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { // Define a closure for processing individual attestations. let processor = self.clone(); - let process_individual = move |package: GossipAttestationPackage| { - let reprocess_tx = processor.reprocess_tx.clone(); - processor.process_gossip_attestation( - package.message_id, - package.peer_id, - package.attestation, - package.subnet_id, - package.should_import, - Some(reprocess_tx), - package.seen_timestamp, - ) - }; + let process_individual = + move |package: GossipAttestationPackage>| { + let reprocess_tx = processor.reprocess_tx.clone(); + processor.process_gossip_attestation( + package.message_id, + package.peer_id, + package.attestation, + package.subnet_id, + package.should_import, + Some(reprocess_tx), + package.seen_timestamp, + ) + }; // Define a closure for processing batches of attestations. let processor = self.clone(); diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 817e6b6440..f5fe7ee98b 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -336,9 +336,31 @@ impl NetworkBeaconProcessor { self: Arc>, block_root: Hash256, custody_columns: DataColumnSidecarList, - _seen_timestamp: Duration, + seen_timestamp: Duration, process_type: BlockProcessType, ) { + // custody_columns must always have at least one element + let Some(slot) = custody_columns.first().map(|d| d.slot()) else { + return; + }; + + if let Ok(current_slot) = self.chain.slot() { + if current_slot == slot { + let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); + metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); + } + } + + let mut indices = custody_columns.iter().map(|d| d.index).collect::>(); + indices.sort_unstable(); + debug!( + self.log, + "RPC custody data columns received"; + "indices" => ?indices, + "block_root" => %block_root, + "slot" => %slot, + ); + let mut result = self .chain .process_rpc_custody_columns(custody_columns) @@ -483,6 +505,7 @@ impl NetworkBeaconProcessor { debug!(self.log, "Backfill batch processed"; "batch_epoch" => epoch, "first_block_slot" => start_slot, + "keep_execution_payload" => !self.chain.store.get_config().prune_payloads, "last_block_slot" => end_slot, "processed_blocks" => sent_blocks, "processed_blobs" => n_blobs, diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index f9ec96df87..0eb416a35d 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -639,7 +639,7 @@ impl Router { ) { let request_id = match request_id { AppRequestId::Sync(sync_id) => match sync_id { - id @ SyncRequestId::RangeBlockAndBlobs { .. } => id, + id @ SyncRequestId::BlocksByRange { .. } => id, other => { crit!(self.log, "BlocksByRange response on incorrect request"; "request" => ?other); return; diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 49f73bf9c8..e1ef57c6ce 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -33,8 +33,8 @@ use task_executor::ShutdownReason; use tokio::sync::mpsc; use tokio::time::Sleep; use types::{ - ChainSpec, DataColumnSubnetId, EthSpec, ForkContext, Slot, SubnetId, SyncCommitteeSubscription, - SyncSubnetId, Unsigned, ValidatorSubscription, + ChainSpec, EthSpec, ForkContext, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, + Unsigned, ValidatorSubscription, }; mod tests; @@ -181,10 +181,6 @@ pub struct NetworkService { next_fork_subscriptions: Pin>>, /// A delay that expires when we need to unsubscribe from old fork topics. next_unsubscribe: Pin>>, - /// Subscribe to all the data column subnets. - subscribe_all_data_column_subnets: bool, - /// Subscribe to all the subnets once synced. - subscribe_all_subnets: bool, /// Shutdown beacon node after sync is complete. shutdown_after_sync: bool, /// Whether metrics are enabled or not. @@ -193,8 +189,6 @@ pub struct NetworkService { metrics_update: tokio::time::Interval, /// gossipsub_parameter_update timer gossipsub_parameter_update: tokio::time::Interval, - /// enable_light_client_server indicator - enable_light_client_server: bool, /// The logger for the network service. fork_context: Arc, log: slog::Logger, @@ -349,15 +343,12 @@ impl NetworkService { next_fork_update, next_fork_subscriptions, next_unsubscribe, - subscribe_all_data_column_subnets: config.subscribe_all_data_column_subnets, - subscribe_all_subnets: config.subscribe_all_subnets, shutdown_after_sync: config.shutdown_after_sync, metrics_enabled: config.metrics_enabled, metrics_update, gossipsub_parameter_update, fork_context, log: network_log, - enable_light_client_server: config.enable_light_client_server, }; Ok((network_service, network_globals, network_senders)) @@ -716,6 +707,7 @@ impl NetworkService { let mut subscribed_topics: Vec = vec![]; for topic_kind in core_topics_to_subscribe::( self.fork_context.current_fork(), + &self.network_globals.as_topic_config(), &self.fork_context.spec, ) { for fork_digest in self.required_gossip_fork_digests() { @@ -732,61 +724,18 @@ impl NetworkService { } } - if self.enable_light_client_server { - for light_client_topic_kind in - lighthouse_network::types::LIGHT_CLIENT_GOSSIP_TOPICS.iter() - { - for fork_digest in self.required_gossip_fork_digests() { - let light_client_topic = GossipTopic::new( - light_client_topic_kind.clone(), - GossipEncoding::default(), - fork_digest, - ); - if self.libp2p.subscribe(light_client_topic.clone()) { - subscribed_topics.push(light_client_topic); - } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %light_client_topic); - } - } - } - } - - if self.fork_context.spec.is_peer_das_scheduled() { - self.subscribe_to_peer_das_topics(&mut subscribed_topics); - } - // If we are to subscribe to all subnets we do it here - if self.subscribe_all_subnets { + if self.network_globals.config.subscribe_all_subnets { for subnet_id in 0..<::EthSpec as EthSpec>::SubnetBitfieldLength::to_u64() { let subnet = Subnet::Attestation(SubnetId::new(subnet_id)); // Update the ENR bitfield self.libp2p.update_enr_subnet(subnet, true); - for fork_digest in self.required_gossip_fork_digests() { - let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - if self.libp2p.subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); - } - } } let subnet_max = <::EthSpec as EthSpec>::SyncCommitteeSubnetCount::to_u64(); for subnet_id in 0..subnet_max { let subnet = Subnet::SyncCommittee(SyncSubnetId::new(subnet_id)); // Update the ENR bitfield self.libp2p.update_enr_subnet(subnet, true); - for fork_digest in self.required_gossip_fork_digests() { - let topic = GossipTopic::new( - subnet.into(), - GossipEncoding::default(), - fork_digest, - ); - if self.libp2p.subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); - } - } } } @@ -801,37 +750,6 @@ impl NetworkService { } } - /// Keeping these separate from core topics because it has custom logic: - /// 1. Data column subscription logic depends on subscription configuration. - /// 2. Data column topic subscriptions will be dynamic based on validator balances due to - /// validator custody. - /// - /// TODO(das): The downside with not including it in core fork topic is - we subscribe to - /// PeerDAS topics on startup if Fulu is scheduled, rather than waiting until the fork. - /// If this is an issue we could potentially consider adding the logic to - /// `network.subscribe_new_fork_topics()`. - fn subscribe_to_peer_das_topics(&mut self, subscribed_topics: &mut Vec) { - let column_subnets_to_subscribe = if self.subscribe_all_data_column_subnets { - &(0..self.fork_context.spec.data_column_sidecar_subnet_count) - .map(DataColumnSubnetId::new) - .collect() - } else { - &self.network_globals.sampling_subnets - }; - - for column_subnet in column_subnets_to_subscribe.iter() { - for fork_digest in self.required_gossip_fork_digests() { - let gossip_kind = Subnet::DataColumn(*column_subnet).into(); - let topic = GossipTopic::new(gossip_kind, GossipEncoding::default(), fork_digest); - if self.libp2p.subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); - } - } - } - } - /// Handle a message sent to the network service. async fn on_validator_subscription_msg(&mut self, msg: ValidatorSubscriptionMessage) { match msg { @@ -946,6 +864,7 @@ impl NetworkService { fn subscribed_core_topics(&self) -> bool { let core_topics = core_topics_to_subscribe::( self.fork_context.current_fork(), + &self.network_globals.as_topic_config(), &self.fork_context.spec, ); let core_topics: HashSet<&GossipKind> = HashSet::from_iter(&core_topics); diff --git a/beacon_node/network/src/subnet_service/mod.rs b/beacon_node/network/src/subnet_service/mod.rs index 33ae567eb3..de90e22254 100644 --- a/beacon_node/network/src/subnet_service/mod.rs +++ b/beacon_node/network/src/subnet_service/mod.rs @@ -216,6 +216,12 @@ impl SubnetService { || self.permanent_attestation_subscriptions.contains(subnet) } + /// Returns whether we are subscribed to a permanent subnet for testing purposes. + #[cfg(test)] + pub(crate) fn is_subscribed_permanent(&self, subnet: &Subnet) -> bool { + self.permanent_attestation_subscriptions.contains(subnet) + } + /// Processes a list of validator subscriptions. /// /// This is fundamentally called form the HTTP API when a validator requests duties from us @@ -629,9 +635,10 @@ impl Stream for SubnetService { // expire subscription. match self.scheduled_subscriptions.poll_next_unpin(cx) { Poll::Ready(Some(Ok(exact_subnet))) => { - let ExactSubnet { subnet, .. } = exact_subnet; - let current_slot = self.beacon_chain.slot_clock.now().unwrap_or_default(); - if let Err(e) = self.subscribe_to_subnet_immediately(subnet, current_slot + 1) { + let ExactSubnet { subnet, slot } = exact_subnet; + // Set the `end_slot` for the subscription to be `duty.slot + 1` so that we unsubscribe + // only at the end of the duty slot. + if let Err(e) = self.subscribe_to_subnet_immediately(subnet, slot + 1) { debug!(self.log, "Failed to subscribe to short lived subnet"; "subnet" => ?subnet, "err" => e); } self.waker diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 7283b4af31..0f3343df63 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -7,9 +7,6 @@ use beacon_chain::{ }; use genesis::{generate_deterministic_keypairs, interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH}; use lighthouse_network::NetworkConfig; -use logging::test_logger; -use slog::{o, Drain, Logger}; -use sloggers::{null::NullLoggerBuilder, Build}; use slot_clock::{SlotClock, SystemTimeSlotClock}; use std::sync::{Arc, LazyLock}; use std::time::{Duration, SystemTime}; @@ -21,10 +18,6 @@ use types::{ SyncCommitteeSubscription, SyncSubnetId, ValidatorSubscription, }; -// Set to enable/disable logging -// const TEST_LOG_LEVEL: Option = Some(slog::Level::Debug); -const TEST_LOG_LEVEL: Option = None; - const SLOT_DURATION_MILLIS: u64 = 400; type TestBeaconChainType = Witness< @@ -46,7 +39,7 @@ impl TestBeaconChain { let keypairs = generate_deterministic_keypairs(1); - let log = get_logger(TEST_LOG_LEVEL); + let log = logging::test_logger(); let store = HotColdDB::open_ephemeral(StoreConfig::default(), spec.clone(), log.clone()).unwrap(); @@ -98,28 +91,10 @@ pub fn recent_genesis_time() -> u64 { .as_secs() } -fn get_logger(log_level: Option) -> Logger { - if let Some(level) = log_level { - let drain = { - let decorator = slog_term::TermDecorator::new().build(); - let decorator = - logging::AlignedTermDecorator::new(decorator, logging::MAX_MESSAGE_WIDTH); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).chan_size(2048).build(); - drain.filter_level(level) - }; - - Logger::root(drain.fuse(), o!()) - } else { - let builder = NullLoggerBuilder; - builder.build().expect("should build logger") - } -} - static CHAIN: LazyLock = LazyLock::new(TestBeaconChain::new_with_system_clock); fn get_subnet_service() -> SubnetService { - let log = test_logger(); + let log = logging::test_logger(); let config = NetworkConfig::default(); let beacon_chain = CHAIN.chain.clone(); @@ -501,8 +476,6 @@ mod test { let committee_count = 1; // Makes 3 validator subscriptions to the same subnet but at different slots. - // There should be just 1 unsubscription event for each of the later slots subscriptions - // (subscription_slot2 and subscription_slot3). let subscription_slot1 = 0; let subscription_slot2 = MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD + 4; let subscription_slot3 = subscription_slot2 * 2; @@ -585,7 +558,7 @@ mod test { let expected_unsubscription = SubnetServiceMessage::Unsubscribe(Subnet::Attestation(subnet_id1)); - if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { + if !subnet_service.is_subscribed_permanent(&Subnet::Attestation(subnet_id1)) { assert_eq!(expected_subscription, events[0]); assert_eq!(expected_unsubscription, events[2]); } @@ -607,9 +580,18 @@ mod test { assert_eq!(no_events, []); - let second_subscribe_event = get_events(&mut subnet_service, None, 2).await; + let subscription_end_slot = current_slot + subscription_slot2 + 2; // +1 to get to the end of the duty slot, +1 for the slot to complete + let wait_slots = subnet_service + .beacon_chain + .slot_clock + .duration_to_slot(subscription_end_slot) + .unwrap() + .as_millis() as u64 + / SLOT_DURATION_MILLIS; + + let second_subscribe_event = get_events(&mut subnet_service, None, wait_slots as u32).await; // If the permanent and short lived subnets are different, we should get an unsubscription event. - if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { + if !subnet_service.is_subscribed_permanent(&Subnet::Attestation(subnet_id1)) { assert_eq!( [ expected_subscription.clone(), @@ -633,9 +615,18 @@ mod test { assert_eq!(no_events, []); - let third_subscribe_event = get_events(&mut subnet_service, None, 2).await; + let subscription_end_slot = current_slot + subscription_slot3 + 2; // +1 to get to the end of the duty slot, +1 for the slot to complete + let wait_slots = subnet_service + .beacon_chain + .slot_clock + .duration_to_slot(subscription_end_slot) + .unwrap() + .as_millis() as u64 + / SLOT_DURATION_MILLIS; - if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { + let third_subscribe_event = get_events(&mut subnet_service, None, wait_slots as u32).await; + + if !subnet_service.is_subscribed_permanent(&Subnet::Attestation(subnet_id1)) { assert_eq!( [expected_subscription, expected_unsubscription], third_subscribe_event[..] diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index a3d2c82642..4220f85fc3 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -422,24 +422,8 @@ impl BackFillSync { self.request_batches(network)?; self.process_completed_batches(network) } - Err(result) => { - let (expected_boundary, received_boundary, outcome) = match result { - Err(e) => { - self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0))?; - return Ok(ProcessResult::Successful); - } - Ok(v) => v, - }; - warn!(self.log, "Batch received out of range blocks"; "expected_boundary" => expected_boundary, "received_boundary" => received_boundary, - "peer_id" => %peer_id, batch); - - if let BatchOperationOutcome::Failed { blacklist: _ } = outcome { - error!(self.log, "Backfill failed"; "epoch" => batch_id, "received_boundary" => received_boundary, "expected_boundary" => expected_boundary); - self.fail_sync(BackFillError::BatchDownloadFailed(batch_id))?; - return Ok(ProcessResult::Successful); - } - // this batch can't be used, so we need to request it again. - self.retry_batch_download(network, batch_id)?; + Err(e) => { + self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0))?; Ok(ProcessResult::Successful) } } diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index ac4df42a4e..a29f9cf402 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -36,6 +36,7 @@ use beacon_chain::data_availability_checker::{ use beacon_chain::{AvailabilityProcessingStatus, BeaconChainTypes, BlockError}; pub use common::RequestState; use fnv::FnvHashMap; +use itertools::Itertools; use lighthouse_network::service::api_types::SingleLookupReqId; use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; @@ -478,8 +479,8 @@ impl BlockLookups { // continue_request will send for processing as the request state is AwaitingProcessing } Err(e) => { - // TODO(das): is it okay to not log the peer source of request failures? Then we - // should log individual requests failures in the SyncNetworkContext + // No need to log peer source here. When sending a DataColumnsByRoot request we log + // the peer and the request ID which is linked to this `id` value here. debug!(self.log, "Received lookup download failure"; "block_root" => ?block_root, @@ -644,8 +645,15 @@ impl BlockLookups { // but future errors may follow the same pattern. Generalize this // pattern with https://github.com/sigp/lighthouse/pull/6321 BlockError::AvailabilityCheck( - AvailabilityCheckError::InvalidColumn(index, _), - ) => peer_group.of_index(index as usize).collect(), + AvailabilityCheckError::InvalidColumn(errors), + ) => errors + .iter() + // Collect all peers that sent a column that was invalid. Must + // run .unique as a single peer can send multiple invalid + // columns. Penalize once to avoid insta-bans + .flat_map(|(index, _)| peer_group.of_index((*index) as usize)) + .unique() + .collect(), _ => peer_group.all().collect(), }; for peer in peers_to_penalize { diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 70a3fe4f5a..6c8a8eab63 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1,7 +1,6 @@ use beacon_chain::{ block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, get_block_root, }; -use lighthouse_network::PeerId; use std::{ collections::{HashMap, VecDeque}, sync::Arc, @@ -29,9 +28,6 @@ pub struct RangeBlockComponentsRequest { /// Used to determine if the number of data columns stream termination this accumulator should /// wait for. This may be less than the number of `expects_custody_columns` due to request batching. num_custody_column_requests: Option, - /// The peers the request was made to. - pub(crate) peer_ids: Vec, - max_blobs_per_block: usize, } impl RangeBlockComponentsRequest { @@ -39,8 +35,6 @@ impl RangeBlockComponentsRequest { expects_blobs: bool, expects_custody_columns: Option>, num_custody_column_requests: Option, - peer_ids: Vec, - max_blobs_per_block: usize, ) -> Self { Self { blocks: <_>::default(), @@ -52,50 +46,42 @@ impl RangeBlockComponentsRequest { expects_blobs, expects_custody_columns, num_custody_column_requests, - peer_ids, - max_blobs_per_block, } } - // TODO: This function should be deprecated when simplying the retry mechanism of this range - // requests. - pub fn get_requirements(&self) -> (bool, Option>) { - (self.expects_blobs, self.expects_custody_columns.clone()) + pub fn add_blocks(&mut self, blocks: Vec>>) { + for block in blocks { + self.blocks.push_back(block); + } + self.is_blocks_stream_terminated = true; } - pub fn add_block_response(&mut self, block_opt: Option>>) { - match block_opt { - Some(block) => self.blocks.push_back(block), - None => self.is_blocks_stream_terminated = true, + pub fn add_blobs(&mut self, blobs: Vec>>) { + for blob in blobs { + self.blobs.push_back(blob); } + self.is_sidecars_stream_terminated = true; } - pub fn add_sidecar_response(&mut self, sidecar_opt: Option>>) { - match sidecar_opt { - Some(sidecar) => self.blobs.push_back(sidecar), - None => self.is_sidecars_stream_terminated = true, - } - } - - pub fn add_data_column(&mut self, column_opt: Option>>) { - match column_opt { - Some(column) => self.data_columns.push_back(column), - // TODO(das): this mechanism is dangerous, if somehow there are two requests for the - // same column index it can terminate early. This struct should track that all requests - // for all custody columns terminate. - None => self.custody_columns_streams_terminated += 1, + pub fn add_custody_columns(&mut self, columns: Vec>>) { + for column in columns { + self.data_columns.push_back(column); } + // TODO(das): this mechanism is dangerous, if somehow there are two requests for the + // same column index it can terminate early. This struct should track that all requests + // for all custody columns terminate. + self.custody_columns_streams_terminated += 1; } pub fn into_responses(self, spec: &ChainSpec) -> Result>, String> { if let Some(expects_custody_columns) = self.expects_custody_columns.clone() { self.into_responses_with_custody_columns(expects_custody_columns, spec) } else { - self.into_responses_with_blobs() + self.into_responses_with_blobs(spec) } } - fn into_responses_with_blobs(self) -> Result>, String> { + fn into_responses_with_blobs(self, spec: &ChainSpec) -> Result>, String> { let RangeBlockComponentsRequest { blocks, blobs, .. } = self; // There can't be more more blobs than blocks. i.e. sending any blob (empty @@ -103,7 +89,8 @@ impl RangeBlockComponentsRequest { let mut responses = Vec::with_capacity(blocks.len()); let mut blob_iter = blobs.into_iter().peekable(); for block in blocks.into_iter() { - let mut blob_list = Vec::with_capacity(self.max_blobs_per_block); + let max_blobs_per_block = spec.max_blobs_per_block(block.epoch()) as usize; + let mut blob_list = Vec::with_capacity(max_blobs_per_block); while { let pair_next_blob = blob_iter .peek() @@ -114,7 +101,7 @@ impl RangeBlockComponentsRequest { blob_list.push(blob_iter.next().ok_or("Missing next blob".to_string())?); } - let mut blobs_buffer = vec![None; self.max_blobs_per_block]; + let mut blobs_buffer = vec![None; max_blobs_per_block]; for blob in blob_list { let blob_index = blob.index as usize; let Some(blob_opt) = blobs_buffer.get_mut(blob_index) else { @@ -128,7 +115,7 @@ impl RangeBlockComponentsRequest { } let blobs = RuntimeVariableList::new( blobs_buffer.into_iter().flatten().collect::>(), - self.max_blobs_per_block, + max_blobs_per_block, ) .map_err(|_| "Blobs returned exceeds max length".to_string())?; responses.push(RpcBlock::new(None, block, Some(blobs)).map_err(|e| format!("{e:?}"))?) @@ -246,30 +233,25 @@ mod tests { use beacon_chain::test_utils::{ generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, NumBlobs, }; - use lighthouse_network::PeerId; use rand::SeedableRng; - use types::{test_utils::XorShiftRng, ForkName, MinimalEthSpec as E}; + use std::sync::Arc; + use types::{test_utils::XorShiftRng, ForkName, MinimalEthSpec as E, SignedBeaconBlock}; #[test] fn no_blobs_into_responses() { let spec = test_spec::(); - let peer_id = PeerId::random(); let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng, &spec) .0 + .into() }) - .collect::>(); - let max_len = spec.max_blobs_per_block(blocks.first().unwrap().epoch()) as usize; - let mut info = - RangeBlockComponentsRequest::::new(false, None, None, vec![peer_id], max_len); + .collect::>>>(); + let mut info = RangeBlockComponentsRequest::::new(false, None, None); // Send blocks and complete terminate response - for block in blocks { - info.add_block_response(Some(block.into())); - } - info.add_block_response(None); + info.add_blocks(blocks); // Assert response is finished and RpcBlocks can be constructed assert!(info.is_finished()); @@ -279,7 +261,6 @@ mod tests { #[test] fn empty_blobs_into_responses() { let spec = test_spec::(); - let peer_id = PeerId::random(); let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { @@ -291,19 +272,15 @@ mod tests { &spec, ) .0 + .into() }) - .collect::>(); - let max_len = spec.max_blobs_per_block(blocks.first().unwrap().epoch()) as usize; - let mut info = - RangeBlockComponentsRequest::::new(true, None, None, vec![peer_id], max_len); + .collect::>>>(); + let mut info = RangeBlockComponentsRequest::::new(true, None, None); // Send blocks and complete terminate response - for block in blocks { - info.add_block_response(Some(block.into())); - } - info.add_block_response(None); + info.add_blocks(blocks); // Expect no blobs returned - info.add_sidecar_response(None); + info.add_blobs(vec![]); // Assert response is finished and RpcBlocks can be constructed, even if blobs weren't returned. // This makes sure we don't expect blobs here when they have expired. Checking this logic should @@ -316,7 +293,6 @@ mod tests { fn rpc_block_with_custody_columns() { let spec = test_spec::(); let expects_custody_columns = vec![1, 2, 3, 4]; - let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { @@ -328,34 +304,24 @@ mod tests { ) }) .collect::>(); - let max_len = spec.max_blobs_per_block(blocks.first().unwrap().0.epoch()) as usize; let mut info = RangeBlockComponentsRequest::::new( false, Some(expects_custody_columns.clone()), Some(expects_custody_columns.len()), - vec![PeerId::random()], - max_len, ); // Send blocks and complete terminate response - for block in &blocks { - info.add_block_response(Some(block.0.clone().into())); - } - info.add_block_response(None); + info.add_blocks(blocks.iter().map(|b| b.0.clone().into()).collect()); // Assert response is not finished assert!(!info.is_finished()); - // Send data columns interleaved - for block in &blocks { - for column in &block.1 { - if expects_custody_columns.contains(&column.index) { - info.add_data_column(Some(column.clone())); - } - } - } - - // Terminate the requests - for (i, _column_index) in expects_custody_columns.iter().enumerate() { - info.add_data_column(None); + // Send data columns + for (i, &column_index) in expects_custody_columns.iter().enumerate() { + info.add_custody_columns( + blocks + .iter() + .flat_map(|b| b.1.iter().filter(|d| d.index == column_index).cloned()) + .collect(), + ); if i < expects_custody_columns.len() - 1 { assert!( @@ -377,8 +343,21 @@ mod tests { #[test] fn rpc_block_with_custody_columns_batched() { let spec = test_spec::(); - let expects_custody_columns = vec![1, 2, 3, 4]; - let num_of_data_column_requests = 2; + let batched_column_requests = [vec![1_u64, 2], vec![3, 4]]; + let expects_custody_columns = batched_column_requests + .iter() + .flatten() + .cloned() + .collect::>(); + let custody_column_request_ids = + (0..batched_column_requests.len() as u32).collect::>(); + let num_of_data_column_requests = custody_column_request_ids.len(); + + let mut info = RangeBlockComponentsRequest::::new( + false, + Some(expects_custody_columns.clone()), + Some(num_of_data_column_requests), + ); let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) @@ -391,34 +370,25 @@ mod tests { ) }) .collect::>(); - let max_len = spec.max_blobs_per_block(blocks.first().unwrap().0.epoch()) as usize; - let mut info = RangeBlockComponentsRequest::::new( - false, - Some(expects_custody_columns.clone()), - Some(num_of_data_column_requests), - vec![PeerId::random()], - max_len, - ); + // Send blocks and complete terminate response - for block in &blocks { - info.add_block_response(Some(block.0.clone().into())); - } - info.add_block_response(None); + info.add_blocks(blocks.iter().map(|b| b.0.clone().into()).collect()); // Assert response is not finished assert!(!info.is_finished()); - // Send data columns interleaved - for block in &blocks { - for column in &block.1 { - if expects_custody_columns.contains(&column.index) { - info.add_data_column(Some(column.clone())); - } - } - } + for (i, column_indices) in batched_column_requests.iter().enumerate() { + // Send the set of columns in the same batch request + info.add_custody_columns( + blocks + .iter() + .flat_map(|b| { + b.1.iter() + .filter(|d| column_indices.contains(&d.index)) + .cloned() + }) + .collect::>(), + ); - // Terminate the requests - for i in 0..num_of_data_column_requests { - info.add_data_column(None); if i < num_of_data_column_requests - 1 { assert!( !info.is_finished(), diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index fd91dc78b1..a9e5f646cc 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -36,7 +36,7 @@ use super::backfill_sync::{BackFillSync, ProcessResult, SyncStart}; use super::block_lookups::BlockLookups; use super::network_context::{ - BlockOrBlob, CustodyByRootResult, RangeRequestId, RpcEvent, SyncNetworkContext, + CustodyByRootResult, RangeBlockComponent, RangeRequestId, RpcEvent, SyncNetworkContext, }; use super::peer_sampling::{Sampling, SamplingConfig, SamplingResult}; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; @@ -47,7 +47,6 @@ use crate::status::ToStatusMessage; use crate::sync::block_lookups::{ BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, }; -use crate::sync::block_sidecar_coupling::RangeBlockComponentsRequest; use crate::sync::network_context::PeerGroup; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::validator_monitor::timestamp_now; @@ -57,8 +56,9 @@ use beacon_chain::{ use futures::StreamExt; use lighthouse_network::rpc::RPCError; use lighthouse_network::service::api_types::{ - CustodyRequester, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, SamplingId, - SamplingRequester, SingleLookupReqId, SyncRequestId, + BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyRequester, + DataColumnsByRangeRequestId, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, + SamplingId, SamplingRequester, SingleLookupReqId, SyncRequestId, }; use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::SyncInfo; @@ -344,6 +344,16 @@ impl SyncManager { self.range_sync.state() } + #[cfg(test)] + pub(crate) fn range_sync_state(&self) -> super::range_sync::SyncChainStatus { + self.range_sync.state() + } + + #[cfg(test)] + pub(crate) fn __range_failed_chains(&mut self) -> Vec { + self.range_sync.__failed_chains() + } + #[cfg(test)] pub(crate) fn get_failed_chains(&mut self) -> Vec { self.block_lookups.get_failed_chains() @@ -368,11 +378,6 @@ impl SyncManager { self.sampling.get_request_status(block_root, index) } - #[cfg(test)] - pub(crate) fn range_sync_state(&self) -> super::range_sync::SyncChainStatus { - self.range_sync.state() - } - #[cfg(test)] pub(crate) fn update_execution_engine_state(&mut self, state: EngineState) { self.handle_new_execution_engine_state(state); @@ -491,36 +496,14 @@ impl SyncManager { SyncRequestId::DataColumnsByRoot(req_id) => { self.on_data_columns_by_root_response(req_id, peer_id, RpcEvent::RPCError(error)) } - SyncRequestId::RangeBlockAndBlobs { id } => { - if let Some(sender_id) = self.network.range_request_failed(id) { - match sender_id { - RangeRequestId::RangeSync { chain_id, batch_id } => { - self.range_sync.inject_error( - &mut self.network, - peer_id, - batch_id, - chain_id, - id, - ); - self.update_sync_state(); - } - RangeRequestId::BackfillSync { batch_id } => match self - .backfill_sync - .inject_error(&mut self.network, batch_id, &peer_id, id) - { - Ok(_) => {} - Err(_) => self.update_sync_state(), - }, - } - } else { - debug!( - self.log, - "RPC error for range request has no associated entry in network context, ungraceful disconnect"; - "peer_id" => %peer_id, - "request_id" => %id, - "error" => ?error, - ); - } + SyncRequestId::BlocksByRange(req_id) => { + self.on_blocks_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)) + } + SyncRequestId::BlobsByRange(req_id) => { + self.on_blobs_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)) + } + SyncRequestId::DataColumnsByRange(req_id) => { + self.on_data_columns_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)) } } } @@ -1051,14 +1034,13 @@ impl SyncManager { SyncRequestId::SingleBlock { id } => self.on_single_block_response( id, peer_id, - match block { - Some(block) => RpcEvent::Response(block, seen_timestamp), - None => RpcEvent::StreamTermination, - }, + RpcEvent::from_chunk(block, seen_timestamp), + ), + SyncRequestId::BlocksByRange(id) => self.on_blocks_by_range_response( + id, + peer_id, + RpcEvent::from_chunk(block, seen_timestamp), ), - SyncRequestId::RangeBlockAndBlobs { id } => { - self.range_block_and_blobs_response(id, peer_id, block.into()) - } _ => { crit!(self.log, "bad request id for block"; "peer_id" => %peer_id ); } @@ -1094,14 +1076,13 @@ impl SyncManager { SyncRequestId::SingleBlob { id } => self.on_single_blob_response( id, peer_id, - match blob { - Some(blob) => RpcEvent::Response(blob, seen_timestamp), - None => RpcEvent::StreamTermination, - }, + RpcEvent::from_chunk(blob, seen_timestamp), + ), + SyncRequestId::BlobsByRange(id) => self.on_blobs_by_range_response( + id, + peer_id, + RpcEvent::from_chunk(blob, seen_timestamp), ), - SyncRequestId::RangeBlockAndBlobs { id } => { - self.range_block_and_blobs_response(id, peer_id, blob.into()) - } _ => { crit!(self.log, "bad request id for blob"; "peer_id" => %peer_id); } @@ -1120,19 +1101,14 @@ impl SyncManager { self.on_data_columns_by_root_response( req_id, peer_id, - match data_column { - Some(data_column) => RpcEvent::Response(data_column, seen_timestamp), - None => RpcEvent::StreamTermination, - }, - ); - } - SyncRequestId::RangeBlockAndBlobs { id } => { - self.range_block_and_blobs_response( - id, - peer_id, - BlockOrBlob::CustodyColumns(data_column), + RpcEvent::from_chunk(data_column, seen_timestamp), ); } + SyncRequestId::DataColumnsByRange(id) => self.on_data_columns_by_range_response( + id, + peer_id, + RpcEvent::from_chunk(data_column, seen_timestamp), + ), _ => { crit!(self.log, "bad request id for data_column"; "peer_id" => %peer_id); } @@ -1188,17 +1164,63 @@ impl SyncManager { } } + fn on_blocks_by_range_response( + &mut self, + id: BlocksByRangeRequestId, + peer_id: PeerId, + block: RpcEvent>>, + ) { + if let Some(resp) = self.network.on_blocks_by_range_response(id, peer_id, block) { + self.on_range_components_response( + id.parent_request_id, + peer_id, + RangeBlockComponent::Block(resp), + ); + } + } + + fn on_blobs_by_range_response( + &mut self, + id: BlobsByRangeRequestId, + peer_id: PeerId, + blob: RpcEvent>>, + ) { + if let Some(resp) = self.network.on_blobs_by_range_response(id, peer_id, blob) { + self.on_range_components_response( + id.parent_request_id, + peer_id, + RangeBlockComponent::Blob(resp), + ); + } + } + + fn on_data_columns_by_range_response( + &mut self, + id: DataColumnsByRangeRequestId, + peer_id: PeerId, + data_column: RpcEvent>>, + ) { + if let Some(resp) = self + .network + .on_data_columns_by_range_response(id, peer_id, data_column) + { + self.on_range_components_response( + id.parent_request_id, + peer_id, + RangeBlockComponent::CustodyColumns(resp), + ); + } + } + fn on_custody_by_root_result( &mut self, requester: CustodyRequester, response: CustodyByRootResult, ) { - // TODO(das): get proper timestamp - let seen_timestamp = timestamp_now(); self.block_lookups .on_download_response::>( requester.0, - response.map(|(columns, peer_group)| (columns, peer_group, seen_timestamp)), + response, &mut self.network, ); } @@ -1230,27 +1252,26 @@ impl SyncManager { /// Handles receiving a response for a range sync request that should have both blocks and /// blobs. - fn range_block_and_blobs_response( + fn on_range_components_response( &mut self, - id: Id, + range_request_id: ComponentsByRangeRequestId, peer_id: PeerId, - block_or_blob: BlockOrBlob, + range_block_component: RangeBlockComponent, ) { if let Some(resp) = self .network - .range_block_and_blob_response(id, block_or_blob) + .range_block_component_response(range_request_id, range_block_component) { - let epoch = resp.sender_id.batch_id(); - match resp.responses { + match resp { Ok(blocks) => { - match resp.sender_id { + match range_request_id.requester { RangeRequestId::RangeSync { chain_id, batch_id } => { self.range_sync.blocks_by_range_response( &mut self.network, peer_id, chain_id, batch_id, - id, + range_request_id.id, blocks, ); self.update_sync_state(); @@ -1260,7 +1281,7 @@ impl SyncManager { &mut self.network, batch_id, &peer_id, - id, + range_request_id.id, blocks, ) { Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), @@ -1274,36 +1295,25 @@ impl SyncManager { } } } - Err(e) => { - // Re-insert the request so we can retry - self.network.insert_range_blocks_and_blobs_request( - id, - resp.sender_id, - RangeBlockComponentsRequest::new( - resp.expects_blobs, - resp.expects_custody_columns, - None, - vec![], - self.chain.spec.max_blobs_per_block(epoch) as usize, - ), - ); - // inform range that the request needs to be treated as failed - // With time we will want to downgrade this log - warn!( - self.log, - "Blocks and blobs request for range received invalid data"; - "peer_id" => %peer_id, - "sender_id" => ?resp.sender_id, - "error" => e.clone() - ); - let id = SyncRequestId::RangeBlockAndBlobs { id }; - self.network.report_peer( - peer_id, - PeerAction::MidToleranceError, - "block_blob_faulty_batch", - ); - self.inject_error(peer_id, id, RPCError::InvalidData(e)) - } + Err(_) => match range_request_id.requester { + RangeRequestId::RangeSync { chain_id, batch_id } => { + self.range_sync.inject_error( + &mut self.network, + peer_id, + batch_id, + chain_id, + range_request_id.id, + ); + self.update_sync_state(); + } + RangeRequestId::BackfillSync { batch_id } => match self + .backfill_sync + .inject_error(&mut self.network, batch_id, &peer_id, range_request_id.id) + { + Ok(_) => {} + Err(_) => self.update_sync_state(), + }, + }, } } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 4135f901b1..968a9bcddd 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -5,7 +5,7 @@ use self::custody::{ActiveCustodyRequest, Error as CustodyRequestError}; pub use self::requests::{BlocksByRootSingleRequest, DataColumnsByRootSingleBlockRequest}; use super::block_sidecar_coupling::RangeBlockComponentsRequest; use super::manager::BlockProcessType; -use super::range_sync::{BatchId, ByRangeRequestType, ChainId}; +use super::range_sync::ByRangeRequestType; use super::SyncMessage; use crate::metrics; use crate::network_beacon_processor::NetworkBeaconProcessor; @@ -17,13 +17,12 @@ use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; use fnv::FnvHashMap; -use lighthouse_network::rpc::methods::{ - BlobsByRangeRequest, DataColumnsByRangeRequest, OldBlocksByRangeRequest, - OldBlocksByRangeRequestV1, OldBlocksByRangeRequestV2, -}; +use lighthouse_network::rpc::methods::{BlobsByRangeRequest, DataColumnsByRangeRequest}; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError, RequestType}; +pub use lighthouse_network::service::api_types::RangeRequestId; use lighthouse_network::service::api_types::{ - AppRequestId, CustodyId, CustodyRequester, DataColumnsByRootRequestId, + AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, + CustodyId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, }; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; @@ -32,12 +31,13 @@ use rand::prelude::IteratorRandom; use rand::thread_rng; pub use requests::LookupVerifyError; use requests::{ - ActiveRequests, BlobsByRootRequestItems, BlocksByRootRequestItems, - DataColumnsByRootRequestItems, + ActiveRequests, BlobsByRangeRequestItems, BlobsByRootRequestItems, BlocksByRangeRequestItems, + BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, }; use slog::{debug, error, warn}; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; @@ -50,33 +50,6 @@ use types::{ pub mod custody; mod requests; -pub struct BlocksAndBlobsByRangeResponse { - pub sender_id: RangeRequestId, - pub responses: Result>, String>, - pub expects_blobs: bool, - pub expects_custody_columns: Option>, -} - -#[derive(Debug, Clone, Copy)] -pub enum RangeRequestId { - RangeSync { - chain_id: ChainId, - batch_id: BatchId, - }, - BackfillSync { - batch_id: BatchId, - }, -} - -impl RangeRequestId { - pub fn batch_id(&self) -> BatchId { - match self { - RangeRequestId::RangeSync { batch_id, .. } => *batch_id, - RangeRequestId::BackfillSync { batch_id, .. } => *batch_id, - } - } -} - #[derive(Debug)] pub enum RpcEvent { StreamTermination, @@ -84,15 +57,27 @@ pub enum RpcEvent { RPCError(RPCError), } +impl RpcEvent { + pub fn from_chunk(chunk: Option, seen_timestamp: Duration) -> Self { + match chunk { + Some(item) => RpcEvent::Response(item, seen_timestamp), + None => RpcEvent::StreamTermination, + } + } +} + pub type RpcResponseResult = Result<(T, Duration), RpcResponseError>; -pub type CustodyByRootResult = Result<(DataColumnSidecarList, PeerGroup), RpcResponseError>; +/// Duration = latest seen timestamp of all received data columns +pub type CustodyByRootResult = + Result<(DataColumnSidecarList, PeerGroup, Duration), RpcResponseError>; #[derive(Debug)] pub enum RpcResponseError { RpcError(RPCError), VerifyError(LookupVerifyError), CustodyRequestError(CustodyRequestError), + BlockComponentCouplingError(String), } #[derive(Debug, PartialEq, Eq)] @@ -110,16 +95,6 @@ pub enum SendErrorProcessor { ProcessorNotAvailable, } -impl std::fmt::Display for RpcResponseError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - RpcResponseError::RpcError(e) => write!(f, "RPC Error: {:?}", e), - RpcResponseError::VerifyError(e) => write!(f, "Lookup Verify Error: {:?}", e), - RpcResponseError::CustodyRequestError(e) => write!(f, "Custody Request Error: {:?}", e), - } - } -} - impl From for RpcResponseError { fn from(e: RPCError) -> Self { RpcResponseError::RpcError(e) @@ -199,13 +174,22 @@ pub struct SyncNetworkContext { /// A mapping of active DataColumnsByRoot requests data_columns_by_root_requests: ActiveRequests>, + /// A mapping of active BlocksByRange requests + blocks_by_range_requests: + ActiveRequests>, + /// A mapping of active BlobsByRange requests + blobs_by_range_requests: + ActiveRequests>, + /// A mapping of active DataColumnsByRange requests + data_columns_by_range_requests: + ActiveRequests>, /// Mapping of active custody column requests for a block root custody_by_root_requests: FnvHashMap>, - /// BlocksByRange requests paired with BlobsByRange - range_block_components_requests: - FnvHashMap)>, + /// BlocksByRange requests paired with other ByRange requests for data components + components_by_range_requests: + FnvHashMap>, /// Whether the ee is online. If it's not, we don't allow access to the /// `beacon_processor_send`. @@ -223,22 +207,10 @@ pub struct SyncNetworkContext { } /// Small enumeration to make dealing with block and blob requests easier. -pub enum BlockOrBlob { - Block(Option>>), - Blob(Option>>), - CustodyColumns(Option>>), -} - -impl From>>> for BlockOrBlob { - fn from(block: Option>>) -> Self { - BlockOrBlob::Block(block) - } -} - -impl From>>> for BlockOrBlob { - fn from(blob: Option>>) -> Self { - BlockOrBlob::Blob(blob) - } +pub enum RangeBlockComponent { + Block(RpcResponseResult>>>), + Blob(RpcResponseResult>>>), + CustodyColumns(RpcResponseResult>>>), } impl SyncNetworkContext { @@ -256,8 +228,11 @@ impl SyncNetworkContext { blocks_by_root_requests: ActiveRequests::new("blocks_by_root"), blobs_by_root_requests: ActiveRequests::new("blobs_by_root"), data_columns_by_root_requests: ActiveRequests::new("data_columns_by_root"), + blocks_by_range_requests: ActiveRequests::new("blocks_by_range"), + blobs_by_range_requests: ActiveRequests::new("blobs_by_range"), + data_columns_by_range_requests: ActiveRequests::new("data_columns_by_range"), custody_by_root_requests: <_>::default(), - range_block_components_requests: FnvHashMap::default(), + components_by_range_requests: FnvHashMap::default(), network_beacon_processor, chain, fork_context, @@ -272,37 +247,60 @@ impl SyncNetworkContext { /// Returns the ids of all the requests made to the given peer_id. pub fn peer_disconnected(&mut self, peer_id: &PeerId) -> Vec { - let failed_range_ids = - self.range_block_components_requests - .iter() - .filter_map(|(id, request)| { - if request.1.peer_ids.contains(peer_id) { - Some(SyncRequestId::RangeBlockAndBlobs { id: *id }) - } else { - None - } - }); + // Note: using destructuring pattern without a default case to make sure we don't forget to + // add new request types to this function. Otherwise, lookup sync can break and lookups + // will get stuck if a peer disconnects during an active requests. + let Self { + network_send: _, + request_id: _, + blocks_by_root_requests, + blobs_by_root_requests, + data_columns_by_root_requests, + blocks_by_range_requests, + blobs_by_range_requests, + data_columns_by_range_requests, + // custody_by_root_requests is a meta request of data_columns_by_root_requests + custody_by_root_requests: _, + // components_by_range_requests is a meta request of various _by_range requests + components_by_range_requests: _, + execution_engine_state: _, + network_beacon_processor: _, + chain: _, + fork_context: _, + log: _, + } = self; - let failed_block_ids = self - .blocks_by_root_requests + let blocks_by_root_ids = blocks_by_root_requests .active_requests_of_peer(peer_id) .into_iter() .map(|id| SyncRequestId::SingleBlock { id: *id }); - let failed_blob_ids = self - .blobs_by_root_requests + let blobs_by_root_ids = blobs_by_root_requests .active_requests_of_peer(peer_id) .into_iter() .map(|id| SyncRequestId::SingleBlob { id: *id }); - let failed_data_column_by_root_ids = self - .data_columns_by_root_requests + let data_column_by_root_ids = data_columns_by_root_requests .active_requests_of_peer(peer_id) .into_iter() .map(|req_id| SyncRequestId::DataColumnsByRoot(*req_id)); + let blocks_by_range_ids = blocks_by_range_requests + .active_requests_of_peer(peer_id) + .into_iter() + .map(|req_id| SyncRequestId::BlocksByRange(*req_id)); + let blobs_by_range_ids = blobs_by_range_requests + .active_requests_of_peer(peer_id) + .into_iter() + .map(|req_id| SyncRequestId::BlobsByRange(*req_id)); + let data_column_by_range_ids = data_columns_by_range_requests + .active_requests_of_peer(peer_id) + .into_iter() + .map(|req_id| SyncRequestId::DataColumnsByRange(*req_id)); - failed_range_ids - .chain(failed_block_ids) - .chain(failed_blob_ids) - .chain(failed_data_column_by_root_ids) + blocks_by_root_ids + .chain(blobs_by_root_ids) + .chain(data_column_by_root_ids) + .chain(blocks_by_range_ids) + .chain(blobs_by_range_ids) + .chain(data_column_by_range_ids) .collect() } @@ -361,117 +359,62 @@ impl SyncNetworkContext { peer_id: PeerId, batch_type: ByRangeRequestType, request: BlocksByRangeRequest, - sender_id: RangeRequestId, + requester: RangeRequestId, ) -> Result { - let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); - let id = self.next_id(); - let mut requested_peers = vec![peer_id]; - debug!( - self.log, - "Sending BlocksByRange request"; - "method" => "BlocksByRange", - "count" => request.count(), - "epoch" => epoch, - "peer" => %peer_id, - "id" => id, - ); - let rpc_request = match request { - BlocksByRangeRequest::V1(ref req) => { - RequestType::BlocksByRange(OldBlocksByRangeRequest::V1(OldBlocksByRangeRequestV1 { - start_slot: req.start_slot, - count: req.count, - step: 1, - })) - } - BlocksByRangeRequest::V2(ref req) => { - RequestType::BlocksByRange(OldBlocksByRangeRequest::V2(OldBlocksByRangeRequestV2 { - start_slot: req.start_slot, - count: req.count, - step: 1, - })) - } + // Create the overall components_by_range request ID before its individual components + let id = ComponentsByRangeRequestId { + id: self.next_id(), + requester, }; - self.network_send - .send(NetworkMessage::SendRequest { + + let _blocks_req_id = self.send_blocks_by_range_request(peer_id, request.clone(), id)?; + + let blobs_req_id = if matches!(batch_type, ByRangeRequestType::BlocksAndBlobs) { + Some(self.send_blobs_by_range_request( peer_id, - request: rpc_request, - request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), - }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; - - let expected_blobs = if matches!(batch_type, ByRangeRequestType::BlocksAndBlobs) { - debug!( - self.log, - "Sending BlobsByRange requests"; - "method" => "BlobsByRange", - "count" => request.count(), - "epoch" => epoch, - "peer" => %peer_id, - ); - - // Create the blob request based on the blocks request. - self.network_send - .send(NetworkMessage::SendRequest { - peer_id, - request: RequestType::BlobsByRange(BlobsByRangeRequest { - start_slot: *request.start_slot(), - count: *request.count(), - }), - request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), - }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; - true + BlobsByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + }, + id, + )?) } else { - false + None }; - let (expects_columns, num_of_column_req) = + let (expects_columns, data_column_requests) = if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { let column_indexes = self.network_globals().sampling_columns.clone(); - let mut num_of_custody_column_req = 0; - for (peer_id, columns_by_range_request) in - self.make_columns_by_range_requests(request, &column_indexes)? - { - requested_peers.push(peer_id); - - debug!( - self.log, - "Sending DataColumnsByRange requests"; - "method" => "DataColumnsByRange", - "count" => columns_by_range_request.count, - "epoch" => epoch, - "columns" => ?columns_by_range_request.columns, - "peer" => %peer_id, - "id" => id, - ); - - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request: RequestType::DataColumnsByRange(columns_by_range_request), - request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + let data_column_requests = self + .make_columns_by_range_requests(request, &column_indexes)? + .into_iter() + .map(|(peer_id, columns_by_range_request)| { + self.send_data_columns_by_range_request( + peer_id, + columns_by_range_request, + id, + ) }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .collect::, _>>()?; - num_of_custody_column_req += 1; - } - - (Some(column_indexes), Some(num_of_custody_column_req)) + ( + Some(column_indexes.into_iter().collect::>()), + Some(data_column_requests), + ) } else { (None, None) }; - let max_blobs_len = self.chain.spec.max_blobs_per_block(epoch); + let expected_blobs = blobs_req_id.is_some(); let info = RangeBlockComponentsRequest::new( expected_blobs, - expects_columns.map(|c| c.into_iter().collect()), - num_of_column_req, - requested_peers, - max_blobs_len as usize, + expects_columns, + data_column_requests.map(|items| items.len()), ); - self.range_block_components_requests - .insert(id, (sender_id, info)); - Ok(id) + self.components_by_range_requests.insert(id, info); + + Ok(id.id) } fn make_columns_by_range_requests( @@ -508,54 +451,43 @@ impl SyncNetworkContext { Ok(peer_id_to_request_map) } - pub fn range_request_failed(&mut self, request_id: Id) -> Option { - let sender_id = self - .range_block_components_requests - .remove(&request_id) - .map(|(sender_id, _info)| sender_id); - if let Some(sender_id) = sender_id { - debug!( - self.log, - "Sync range request failed"; - "request_id" => request_id, - "sender_id" => ?sender_id - ); - Some(sender_id) - } else { - debug!(self.log, "Sync range request failed"; "request_id" => request_id); - None - } - } - /// Received a blocks by range or blobs by range response for a request that couples blocks ' /// and blobs. - pub fn range_block_and_blob_response( + pub fn range_block_component_response( &mut self, - request_id: Id, - block_or_blob: BlockOrBlob, - ) -> Option> { - let Entry::Occupied(mut entry) = self.range_block_components_requests.entry(request_id) - else { + id: ComponentsByRangeRequestId, + range_block_component: RangeBlockComponent, + ) -> Option>, RpcResponseError>> { + let Entry::Occupied(mut entry) = self.components_by_range_requests.entry(id) else { metrics::inc_counter_vec(&metrics::SYNC_UNKNOWN_NETWORK_REQUESTS, &["range_blocks"]); return None; }; - let (_, info) = entry.get_mut(); - match block_or_blob { - BlockOrBlob::Block(maybe_block) => info.add_block_response(maybe_block), - BlockOrBlob::Blob(maybe_sidecar) => info.add_sidecar_response(maybe_sidecar), - BlockOrBlob::CustodyColumns(column) => info.add_data_column(column), + if let Err(e) = { + let request = entry.get_mut(); + match range_block_component { + RangeBlockComponent::Block(resp) => resp.map(|(blocks, _)| { + request.add_blocks(blocks); + }), + RangeBlockComponent::Blob(resp) => resp.map(|(blobs, _)| { + request.add_blobs(blobs); + }), + RangeBlockComponent::CustodyColumns(resp) => resp.map(|(custody_columns, _)| { + request.add_custody_columns(custody_columns); + }), + } + } { + entry.remove(); + return Some(Err(e)); } - if info.is_finished() { + + if entry.get_mut().is_finished() { // If the request is finished, dequeue everything - let (sender_id, info) = entry.remove(); - let (expects_blobs, expects_custody_columns) = info.get_requirements(); - Some(BlocksAndBlobsByRangeResponse { - sender_id, - responses: info.into_responses(&self.chain.spec), - expects_blobs, - expects_custody_columns, - }) + let request = entry.remove(); + let blocks = request + .into_responses(&self.chain.spec) + .map_err(RpcResponseError::BlockComponentCouplingError); + Some(blocks) } else { None } @@ -606,17 +538,10 @@ impl SyncNetworkContext { } } - let req_id = self.next_id(); - let id = SingleLookupReqId { lookup_id, req_id }; - - debug!( - self.log, - "Sending BlocksByRoot Request"; - "method" => "BlocksByRoot", - "block_root" => ?block_root, - "peer" => %peer_id, - "id" => ?id - ); + let id = SingleLookupReqId { + lookup_id, + req_id: self.next_id(), + }; let request = BlocksByRootSingleRequest(block_root); @@ -634,6 +559,15 @@ impl SyncNetworkContext { }) .map_err(|_| RpcRequestSendError::NetworkSendError)?; + debug!( + self.log, + "Sync RPC request sent"; + "method" => "BlocksByRoot", + "block_root" => ?block_root, + "peer" => %peer_id, + "id" => %id + ); + self.blocks_by_root_requests.insert( id, peer_id, @@ -643,7 +577,7 @@ impl SyncNetworkContext { BlocksByRootRequestItems::new(request), ); - Ok(LookupRequestResult::RequestSent(req_id)) + Ok(LookupRequestResult::RequestSent(id.req_id)) } /// Request necessary blobs for `block_root`. Requests only the necessary blobs by checking: @@ -689,22 +623,14 @@ impl SyncNetworkContext { return Ok(LookupRequestResult::NoRequestNeeded("no indices to fetch")); } - let req_id = self.next_id(); - let id = SingleLookupReqId { lookup_id, req_id }; - - debug!( - self.log, - "Sending BlobsByRoot Request"; - "method" => "BlobsByRoot", - "block_root" => ?block_root, - "blob_indices" => ?indices, - "peer" => %peer_id, - "id" => ?id - ); + let id = SingleLookupReqId { + lookup_id, + req_id: self.next_id(), + }; let request = BlobsByRootSingleBlockRequest { block_root, - indices, + indices: indices.clone(), }; // Lookup sync event safety: Refer to `Self::block_lookup_request` `network_send.send` call @@ -716,6 +642,16 @@ impl SyncNetworkContext { }) .map_err(|_| RpcRequestSendError::NetworkSendError)?; + debug!( + self.log, + "Sync RPC request sent"; + "method" => "BlobsByRoot", + "block_root" => ?block_root, + "blob_indices" => ?indices, + "peer" => %peer_id, + "id" => %id + ); + self.blobs_by_root_requests.insert( id, peer_id, @@ -726,7 +662,7 @@ impl SyncNetworkContext { BlobsByRootRequestItems::new(request), ); - Ok(LookupRequestResult::RequestSent(req_id)) + Ok(LookupRequestResult::RequestSent(id.req_id)) } /// Request to send a single `data_columns_by_root` request to the network. @@ -737,35 +673,35 @@ impl SyncNetworkContext { request: DataColumnsByRootSingleBlockRequest, expect_max_responses: bool, ) -> Result, &'static str> { - let req_id = DataColumnsByRootRequestId { + let id = DataColumnsByRootRequestId { id: self.next_id(), requester, }; - debug!( - self.log, - "Sending DataColumnsByRoot Request"; - "method" => "DataColumnsByRoot", - "block_root" => ?request.block_root, - "indices" => ?request.indices, - "peer" => %peer_id, - "requester" => ?requester, - "req_id" => %req_id, - ); self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: RequestType::DataColumnsByRoot(request.clone().into_request(&self.chain.spec)), - request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRoot(req_id)), + request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), })?; + debug!( + self.log, + "Sync RPC request sent"; + "method" => "DataColumnsByRoot", + "block_root" => ?request.block_root, + "indices" => ?request.indices, + "peer" => %peer_id, + "id" => %id, + ); + self.data_columns_by_root_requests.insert( - req_id, + id, peer_id, expect_max_responses, DataColumnsByRootRequestItems::new(request), ); - Ok(LookupRequestResult::RequestSent(req_id)) + Ok(LookupRequestResult::RequestSent(id)) } /// Request to fetch all needed custody columns of a specific block. This function may not send @@ -798,15 +734,17 @@ impl SyncNetworkContext { return Ok(LookupRequestResult::NoRequestNeeded("no indices to fetch")); } - let req_id = self.next_id(); - let id = SingleLookupReqId { lookup_id, req_id }; + let id = SingleLookupReqId { + lookup_id, + req_id: self.next_id(), + }; debug!( self.log, "Starting custody columns request"; "block_root" => ?block_root, "indices" => ?custody_indexes_to_fetch, - "id" => ?id + "id" => %id ); let requester = CustodyRequester(id); @@ -825,12 +763,134 @@ impl SyncNetworkContext { // created cannot return data immediately, it must send some request to the network // first. And there must exist some request, `custody_indexes_to_fetch` is not empty. self.custody_by_root_requests.insert(requester, request); - Ok(LookupRequestResult::RequestSent(req_id)) + Ok(LookupRequestResult::RequestSent(id.req_id)) } Err(e) => Err(RpcRequestSendError::CustodyRequestError(e)), } } + fn send_blocks_by_range_request( + &mut self, + peer_id: PeerId, + request: BlocksByRangeRequest, + parent_request_id: ComponentsByRangeRequestId, + ) -> Result { + let id = BlocksByRangeRequestId { + id: self.next_id(), + parent_request_id, + }; + self.network_send + .send(NetworkMessage::SendRequest { + peer_id, + request: RequestType::BlocksByRange(request.clone().into()), + request_id: AppRequestId::Sync(SyncRequestId::BlocksByRange(id)), + }) + .map_err(|_| RpcRequestSendError::NetworkSendError)?; + + debug!( + self.log, + "Sync RPC request sent"; + "method" => "BlocksByRange", + "slots" => request.count(), + "epoch" => Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()), + "peer" => %peer_id, + "id" => %id, + ); + + self.blocks_by_range_requests.insert( + id, + peer_id, + // false = do not enforce max_requests are returned for *_by_range methods. We don't + // know if there are missed blocks. + false, + BlocksByRangeRequestItems::new(request), + ); + Ok(id) + } + + fn send_blobs_by_range_request( + &mut self, + peer_id: PeerId, + request: BlobsByRangeRequest, + parent_request_id: ComponentsByRangeRequestId, + ) -> Result { + let id = BlobsByRangeRequestId { + id: self.next_id(), + parent_request_id, + }; + let request_epoch = Slot::new(request.start_slot).epoch(T::EthSpec::slots_per_epoch()); + + // Create the blob request based on the blocks request. + self.network_send + .send(NetworkMessage::SendRequest { + peer_id, + request: RequestType::BlobsByRange(request.clone()), + request_id: AppRequestId::Sync(SyncRequestId::BlobsByRange(id)), + }) + .map_err(|_| RpcRequestSendError::NetworkSendError)?; + + debug!( + self.log, + "Sync RPC request sent"; + "method" => "BlobsByRange", + "slots" => request.count, + "epoch" => request_epoch, + "peer" => %peer_id, + "id" => %id, + ); + + let max_blobs_per_block = self.chain.spec.max_blobs_per_block(request_epoch); + self.blobs_by_range_requests.insert( + id, + peer_id, + // false = do not enforce max_requests are returned for *_by_range methods. We don't + // know if there are missed blocks. + false, + BlobsByRangeRequestItems::new(request, max_blobs_per_block), + ); + Ok(id) + } + + fn send_data_columns_by_range_request( + &mut self, + peer_id: PeerId, + request: DataColumnsByRangeRequest, + parent_request_id: ComponentsByRangeRequestId, + ) -> Result { + let id = DataColumnsByRangeRequestId { + id: self.next_id(), + parent_request_id, + }; + + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: RequestType::DataColumnsByRange(request.clone()), + request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRange(id)), + }) + .map_err(|_| RpcRequestSendError::NetworkSendError)?; + + debug!( + self.log, + "Sync RPC request sent"; + "method" => "DataColumnsByRange", + "slots" => request.count, + "epoch" => Slot::new(request.start_slot).epoch(T::EthSpec::slots_per_epoch()), + "columns" => ?request.columns, + "peer" => %peer_id, + "id" => %id, + ); + + self.data_columns_by_range_requests.insert( + id, + peer_id, + // false = do not enforce max_requests are returned for *_by_range methods. We don't + // know if there are missed blocks. + false, + DataColumnsByRangeRequestItems::new(request), + ); + Ok(id) + } + pub fn is_execution_engine_online(&self) -> bool { self.execution_engine_state == EngineState::Online } @@ -929,16 +989,6 @@ impl SyncNetworkContext { } } - pub fn insert_range_blocks_and_blobs_request( - &mut self, - id: Id, - sender_id: RangeRequestId, - info: RangeBlockComponentsRequest, - ) { - self.range_block_components_requests - .insert(id, (sender_id, info)); - } - /// Attempt to make progress on all custody_by_root requests. Some request may be stale waiting /// for custody peers. Returns a Vec of results as zero or more requests may fail in this /// attempt. @@ -973,8 +1023,8 @@ impl SyncNetworkContext { peer_id: PeerId, rpc_event: RpcEvent>>, ) -> Option>>> { - let response = self.blocks_by_root_requests.on_response(id, rpc_event); - let response = response.map(|res| { + let resp = self.blocks_by_root_requests.on_response(id, rpc_event); + let resp = resp.map(|res| { res.and_then(|(mut blocks, seen_timestamp)| { // Enforce that exactly one chunk = one block is returned. ReqResp behavior limits the // response count to at most 1. @@ -986,10 +1036,7 @@ impl SyncNetworkContext { } }) }); - if let Some(Err(RpcResponseError::VerifyError(e))) = &response { - self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); - } - response + self.on_rpc_response_result(id, "BlocksByRoot", resp, peer_id, |_| 1) } pub(crate) fn on_single_blob_response( @@ -998,8 +1045,8 @@ impl SyncNetworkContext { peer_id: PeerId, rpc_event: RpcEvent>>, ) -> Option>> { - let response = self.blobs_by_root_requests.on_response(id, rpc_event); - let response = response.map(|res| { + let resp = self.blobs_by_root_requests.on_response(id, rpc_event); + let resp = resp.map(|res| { res.and_then(|(blobs, seen_timestamp)| { if let Some(max_len) = blobs .first() @@ -1018,10 +1065,7 @@ impl SyncNetworkContext { } }) }); - if let Some(Err(RpcResponseError::VerifyError(e))) = &response { - self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); - } - response + self.on_rpc_response_result(id, "BlobsByRoot", resp, peer_id, |_| 1) } #[allow(clippy::type_complexity)] @@ -1034,14 +1078,73 @@ impl SyncNetworkContext { let resp = self .data_columns_by_root_requests .on_response(id, rpc_event); - self.report_rpc_response_errors(resp, peer_id) + self.on_rpc_response_result(id, "DataColumnsByRoot", resp, peer_id, |_| 1) } - fn report_rpc_response_errors( + #[allow(clippy::type_complexity)] + pub(crate) fn on_blocks_by_range_response( &mut self, + id: BlocksByRangeRequestId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) -> Option>>>> { + let resp = self.blocks_by_range_requests.on_response(id, rpc_event); + self.on_rpc_response_result(id, "BlocksByRange", resp, peer_id, |b| b.len()) + } + + #[allow(clippy::type_complexity)] + pub(crate) fn on_blobs_by_range_response( + &mut self, + id: BlobsByRangeRequestId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) -> Option>>>> { + let resp = self.blobs_by_range_requests.on_response(id, rpc_event); + self.on_rpc_response_result(id, "BlobsByRangeRequest", resp, peer_id, |b| b.len()) + } + + #[allow(clippy::type_complexity)] + pub(crate) fn on_data_columns_by_range_response( + &mut self, + id: DataColumnsByRangeRequestId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) -> Option>> { + let resp = self + .data_columns_by_range_requests + .on_response(id, rpc_event); + self.on_rpc_response_result(id, "DataColumnsByRange", resp, peer_id, |d| d.len()) + } + + fn on_rpc_response_result usize>( + &mut self, + id: I, + method: &'static str, resp: Option>, peer_id: PeerId, + get_count: F, ) -> Option> { + match &resp { + None => {} + Some(Ok((v, _))) => { + debug!( + self.log, + "Sync RPC request completed"; + "id" => %id, + "method" => method, + "count" => get_count(v) + ); + } + Some(Err(e)) => { + debug!( + self.log, + "Sync RPC request error"; + "id" => %id, + "method" => method, + "error" => ?e + ); + } + } if let Some(Err(RpcResponseError::VerifyError(e))) = &resp { self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); } @@ -1089,7 +1192,7 @@ impl SyncNetworkContext { // Convert a result from internal format of `ActiveCustodyRequest` (error first to use ?) to // an Option first to use in an `if let Some() { act on result }` block. match result.as_ref() { - Some(Ok((columns, peer_group))) => { + Some(Ok((columns, peer_group, _))) => { debug!(self.log, "Custody request success, removing"; "id" => ?id, "count" => columns.len(), "peers" => ?peer_group) } Some(Err(e)) => { @@ -1107,7 +1210,7 @@ impl SyncNetworkContext { id: Id, block_root: Hash256, block: RpcBlock, - duration: Duration, + seen_timestamp: Duration, ) -> Result<(), SendErrorProcessor> { let beacon_processor = self .beacon_processor_if_enabled() @@ -1120,7 +1223,7 @@ impl SyncNetworkContext { .send_rpc_beacon_block( block_root, block, - duration, + seen_timestamp, BlockProcessType::SingleBlock { id }, ) .map_err(|e| { @@ -1138,7 +1241,7 @@ impl SyncNetworkContext { id: Id, block_root: Hash256, blobs: FixedBlobSidecarList, - duration: Duration, + seen_timestamp: Duration, ) -> Result<(), SendErrorProcessor> { let beacon_processor = self .beacon_processor_if_enabled() @@ -1151,7 +1254,7 @@ impl SyncNetworkContext { .send_rpc_blobs( block_root, blobs, - duration, + seen_timestamp, BlockProcessType::SingleBlob { id }, ) .map_err(|e| { @@ -1169,7 +1272,7 @@ impl SyncNetworkContext { _id: Id, block_root: Hash256, custody_columns: DataColumnSidecarList, - duration: Duration, + seen_timestamp: Duration, process_type: BlockProcessType, ) -> Result<(), SendErrorProcessor> { let beacon_processor = self @@ -1179,7 +1282,7 @@ impl SyncNetworkContext { debug!(self.log, "Sending custody columns for processing"; "block" => ?block_root, "process_type" => ?process_type); beacon_processor - .send_rpc_custody_columns(block_root, custody_columns, duration, process_type) + .send_rpc_custody_columns(block_root, custody_columns, seen_timestamp, process_type) .map_err(|e| { error!( self.log, @@ -1191,21 +1294,27 @@ impl SyncNetworkContext { } pub(crate) fn register_metrics(&self) { - metrics::set_gauge_vec( - &metrics::SYNC_ACTIVE_NETWORK_REQUESTS, - &["blocks_by_root"], - self.blocks_by_root_requests.len() as i64, - ); - metrics::set_gauge_vec( - &metrics::SYNC_ACTIVE_NETWORK_REQUESTS, - &["blobs_by_root"], - self.blobs_by_root_requests.len() as i64, - ); - metrics::set_gauge_vec( - &metrics::SYNC_ACTIVE_NETWORK_REQUESTS, - &["range_blocks"], - self.range_block_components_requests.len() as i64, - ); + for (id, count) in [ + ("blocks_by_root", self.blocks_by_root_requests.len()), + ("blobs_by_root", self.blobs_by_root_requests.len()), + ( + "data_columns_by_root", + self.data_columns_by_root_requests.len(), + ), + ("blocks_by_range", self.blocks_by_range_requests.len()), + ("blobs_by_range", self.blobs_by_range_requests.len()), + ( + "data_columns_by_range", + self.data_columns_by_range_requests.len(), + ), + ("custody_by_root", self.custody_by_root_requests.len()), + ( + "components_by_range", + self.components_by_range_requests.len(), + ), + ] { + metrics::set_gauge_vec(&metrics::SYNC_ACTIVE_NETWORK_REQUESTS, &[id], count as i64); + } } } diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 8a29545c21..38353d3ea2 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,7 +1,7 @@ use crate::sync::network_context::{ DataColumnsByRootRequestId, DataColumnsByRootSingleBlockRequest, }; - +use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::service::api_types::{CustodyId, DataColumnsByRootRequester}; @@ -61,7 +61,8 @@ struct ActiveBatchColumnsRequest { indices: Vec, } -pub type CustodyRequestResult = Result, PeerGroup)>, Error>; +pub type CustodyRequestResult = + Result, PeerGroup, Duration)>, Error>; impl ActiveCustodyRequest { pub(crate) fn new( @@ -102,8 +103,6 @@ impl ActiveCustodyRequest { resp: RpcResponseResult>, cx: &mut SyncNetworkContext, ) -> CustodyRequestResult { - // TODO(das): Should downscore peers for verify errors here - let Some(batch_request) = self.active_batch_columns_requests.get_mut(&req_id) else { warn!(self.log, "Received custody column response for unrequested index"; @@ -115,7 +114,7 @@ impl ActiveCustodyRequest { }; match resp { - Ok((data_columns, _seen_timestamp)) => { + Ok((data_columns, seen_timestamp)) => { debug!(self.log, "Custody column download success"; "id" => ?self.custody_id, @@ -141,7 +140,12 @@ impl ActiveCustodyRequest { .ok_or(Error::BadState("unknown column_index".to_owned()))?; if let Some(data_column) = data_columns.remove(column_index) { - column_request.on_download_success(req_id, peer_id, data_column)?; + column_request.on_download_success( + req_id, + peer_id, + data_column, + seen_timestamp, + )?; } else { // Peer does not have the requested data. // TODO(das) do not consider this case a success. We know for sure the block has @@ -204,20 +208,23 @@ impl ActiveCustodyRequest { if self.column_requests.values().all(|r| r.is_downloaded()) { // All requests have completed successfully. let mut peers = HashMap::>::new(); + let mut seen_timestamps = vec![]; let columns = std::mem::take(&mut self.column_requests) .into_values() .map(|request| { - let (peer, data_column) = request.complete()?; + let (peer, data_column, seen_timestamp) = request.complete()?; peers .entry(peer) .or_default() .push(data_column.index as usize); + seen_timestamps.push(seen_timestamp); Ok(data_column) }) .collect::, _>>()?; let peer_group = PeerGroup::from_set(peers); - return Ok(Some((columns, peer_group))); + let max_seen_timestamp = seen_timestamps.into_iter().max().unwrap_or(timestamp_now()); + return Ok(Some((columns, peer_group, max_seen_timestamp))); } let mut columns_to_request_by_peer = HashMap::>::new(); @@ -335,7 +342,7 @@ struct ColumnRequest { enum Status { NotStarted(Instant), Downloading(DataColumnsByRootRequestId), - Downloaded(PeerId, Arc>), + Downloaded(PeerId, Arc>, Duration), } impl ColumnRequest { @@ -404,6 +411,7 @@ impl ColumnRequest { req_id: DataColumnsByRootRequestId, peer_id: PeerId, data_column: Arc>, + seen_timestamp: Duration, ) -> Result<(), Error> { match &self.status { Status::Downloading(expected_req_id) => { @@ -413,7 +421,7 @@ impl ColumnRequest { req_id, }); } - self.status = Status::Downloaded(peer_id, data_column); + self.status = Status::Downloaded(peer_id, data_column, seen_timestamp); Ok(()) } other => Err(Error::BadState(format!( @@ -422,9 +430,11 @@ impl ColumnRequest { } } - fn complete(self) -> Result<(PeerId, Arc>), Error> { + fn complete(self) -> Result<(PeerId, Arc>, Duration), Error> { match self.status { - Status::Downloaded(peer_id, data_column) => Ok((peer_id, data_column)), + Status::Downloaded(peer_id, data_column, seen_timestamp) => { + Ok((peer_id, data_column, seen_timestamp)) + } other => Err(Error::BadState(format!( "bad state complete expected Downloaded got {other:?}" ))), diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 4a5a16459d..c9b85e47b6 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -4,10 +4,13 @@ use beacon_chain::validator_monitor::timestamp_now; use fnv::FnvHashMap; use lighthouse_network::PeerId; use strum::IntoStaticStr; -use types::Hash256; +use types::{Hash256, Slot}; +pub use blobs_by_range::BlobsByRangeRequestItems; pub use blobs_by_root::{BlobsByRootRequestItems, BlobsByRootSingleBlockRequest}; +pub use blocks_by_range::BlocksByRangeRequestItems; pub use blocks_by_root::{BlocksByRootRequestItems, BlocksByRootSingleRequest}; +pub use data_columns_by_range::DataColumnsByRangeRequestItems; pub use data_columns_by_root::{ DataColumnsByRootRequestItems, DataColumnsByRootSingleBlockRequest, }; @@ -16,8 +19,11 @@ use crate::metrics; use super::{RpcEvent, RpcResponseResult}; +mod blobs_by_range; mod blobs_by_root; +mod blocks_by_range; mod blocks_by_root; +mod data_columns_by_range; mod data_columns_by_root; #[derive(Debug, PartialEq, Eq, IntoStaticStr)] @@ -26,8 +32,9 @@ pub enum LookupVerifyError { TooManyResponses, UnrequestedBlockRoot(Hash256), UnrequestedIndex(u64), + UnrequestedSlot(Slot), InvalidInclusionProof, - DuplicateData, + DuplicatedData(Slot, u64), InternalError(String), } diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs new file mode 100644 index 0000000000..9c6f516199 --- /dev/null +++ b/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs @@ -0,0 +1,56 @@ +use super::{ActiveRequestItems, LookupVerifyError}; +use lighthouse_network::rpc::methods::BlobsByRangeRequest; +use std::sync::Arc; +use types::{BlobSidecar, EthSpec}; + +/// Accumulates results of a blobs_by_range request. Only returns items after receiving the +/// stream termination. +pub struct BlobsByRangeRequestItems { + request: BlobsByRangeRequest, + items: Vec>>, + max_blobs_per_block: u64, +} + +impl BlobsByRangeRequestItems { + pub fn new(request: BlobsByRangeRequest, max_blobs_per_block: u64) -> Self { + Self { + request, + items: vec![], + max_blobs_per_block, + } + } +} + +impl ActiveRequestItems for BlobsByRangeRequestItems { + type Item = Arc>; + + fn add(&mut self, blob: Self::Item) -> Result { + if blob.slot() < self.request.start_slot + || blob.slot() >= self.request.start_slot + self.request.count + { + return Err(LookupVerifyError::UnrequestedSlot(blob.slot())); + } + if blob.index >= self.max_blobs_per_block { + return Err(LookupVerifyError::UnrequestedIndex(blob.index)); + } + if !blob.verify_blob_sidecar_inclusion_proof() { + return Err(LookupVerifyError::InvalidInclusionProof); + } + if self + .items + .iter() + .any(|existing| existing.slot() == blob.slot() && existing.index == blob.index) + { + return Err(LookupVerifyError::DuplicatedData(blob.slot(), blob.index)); + } + + self.items.push(blob); + + // Skip check if blobs are ready as it's rare that all blocks have max blobs + Ok(false) + } + + fn consume(&mut self) -> Vec { + std::mem::take(&mut self.items) + } +} diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs index a670229884..547c51198e 100644 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs @@ -57,7 +57,7 @@ impl ActiveRequestItems for BlobsByRootRequestItems { return Err(LookupVerifyError::UnrequestedIndex(blob.index)); } if self.items.iter().any(|b| b.index == blob.index) { - return Err(LookupVerifyError::DuplicateData); + return Err(LookupVerifyError::DuplicatedData(blob.slot(), blob.index)); } self.items.push(blob); diff --git a/beacon_node/network/src/sync/network_context/requests/blocks_by_range.rs b/beacon_node/network/src/sync/network_context/requests/blocks_by_range.rs new file mode 100644 index 0000000000..c7d2dda01e --- /dev/null +++ b/beacon_node/network/src/sync/network_context/requests/blocks_by_range.rs @@ -0,0 +1,48 @@ +use super::{ActiveRequestItems, LookupVerifyError}; +use lighthouse_network::rpc::BlocksByRangeRequest; +use std::sync::Arc; +use types::{EthSpec, SignedBeaconBlock}; + +/// Accumulates results of a blocks_by_range request. Only returns items after receiving the +/// stream termination. +pub struct BlocksByRangeRequestItems { + request: BlocksByRangeRequest, + items: Vec>>, +} + +impl BlocksByRangeRequestItems { + pub fn new(request: BlocksByRangeRequest) -> Self { + Self { + request, + items: vec![], + } + } +} + +impl ActiveRequestItems for BlocksByRangeRequestItems { + type Item = Arc>; + + fn add(&mut self, block: Self::Item) -> Result { + if block.slot().as_u64() < *self.request.start_slot() + || block.slot().as_u64() >= self.request.start_slot() + self.request.count() + { + return Err(LookupVerifyError::UnrequestedSlot(block.slot())); + } + if self + .items + .iter() + .any(|existing| existing.slot() == block.slot()) + { + // DuplicatedData is a common error for all components, default index to 0 + return Err(LookupVerifyError::DuplicatedData(block.slot(), 0)); + } + + self.items.push(block); + + Ok(self.items.len() >= *self.request.count() as usize) + } + + fn consume(&mut self) -> Vec { + std::mem::take(&mut self.items) + } +} diff --git a/beacon_node/network/src/sync/network_context/requests/data_columns_by_range.rs b/beacon_node/network/src/sync/network_context/requests/data_columns_by_range.rs new file mode 100644 index 0000000000..9dabb2defa --- /dev/null +++ b/beacon_node/network/src/sync/network_context/requests/data_columns_by_range.rs @@ -0,0 +1,54 @@ +use super::{ActiveRequestItems, LookupVerifyError}; +use lighthouse_network::rpc::methods::DataColumnsByRangeRequest; +use std::sync::Arc; +use types::{DataColumnSidecar, EthSpec}; + +/// Accumulates results of a data_columns_by_range request. Only returns items after receiving the +/// stream termination. +pub struct DataColumnsByRangeRequestItems { + request: DataColumnsByRangeRequest, + items: Vec>>, +} + +impl DataColumnsByRangeRequestItems { + pub fn new(request: DataColumnsByRangeRequest) -> Self { + Self { + request, + items: vec![], + } + } +} + +impl ActiveRequestItems for DataColumnsByRangeRequestItems { + type Item = Arc>; + + fn add(&mut self, data_column: Self::Item) -> Result { + if data_column.slot() < self.request.start_slot + || data_column.slot() >= self.request.start_slot + self.request.count + { + return Err(LookupVerifyError::UnrequestedSlot(data_column.slot())); + } + if !self.request.columns.contains(&data_column.index) { + return Err(LookupVerifyError::UnrequestedIndex(data_column.index)); + } + if !data_column.verify_inclusion_proof() { + return Err(LookupVerifyError::InvalidInclusionProof); + } + if self.items.iter().any(|existing| { + existing.slot() == data_column.slot() && existing.index == data_column.index + }) { + return Err(LookupVerifyError::DuplicatedData( + data_column.slot(), + data_column.index, + )); + } + + self.items.push(data_column); + + Ok(self.items.len() >= self.request.count as usize * self.request.columns.len()) + } + + fn consume(&mut self) -> Vec { + std::mem::take(&mut self.items) + } +} diff --git a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs index 1b8d46ff07..4e02737f08 100644 --- a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs @@ -57,7 +57,10 @@ impl ActiveRequestItems for DataColumnsByRootRequestItems { return Err(LookupVerifyError::UnrequestedIndex(data_column.index)); } if self.items.iter().any(|d| d.index == data_column.index) { - return Err(LookupVerifyError::DuplicateData); + return Err(LookupVerifyError::DuplicatedData( + data_column.slot(), + data_column.index, + )); } self.items.push(data_column); diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 53fb55b14d..912287a8a4 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -1,4 +1,4 @@ -use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; +use beacon_chain::block_verification_types::RpcBlock; use lighthouse_network::rpc::methods::BlocksByRangeRequest; use lighthouse_network::service::api_types::Id; use lighthouse_network::PeerId; @@ -271,42 +271,9 @@ impl BatchInfo { pub fn download_completed( &mut self, blocks: Vec>, - ) -> Result< - usize, /* Received blocks */ - Result<(Slot, Slot, BatchOperationOutcome), WrongState>, - > { + ) -> Result { match self.state.poison() { BatchState::Downloading(peer, _request_id) => { - // verify that blocks are in range - if let Some(last_slot) = blocks.last().map(|b| b.slot()) { - // the batch is non-empty - let first_slot = blocks[0].slot(); - - let failed_range = if first_slot < self.start_slot { - Some((self.start_slot, first_slot)) - } else if self.end_slot < last_slot { - Some((self.end_slot, last_slot)) - } else { - None - }; - - if let Some((expected, received)) = failed_range { - // this is a failed download, register the attempt and check if the batch - // can be tried again - self.failed_download_attempts.push(peer); - self.state = if self.failed_download_attempts.len() - >= B::max_batch_download_attempts() as usize - { - BatchState::Failed - } else { - // drop the blocks - BatchState::AwaitingDownload - }; - - return Err(Ok((expected, received, self.outcome()))); - } - } - let received = blocks.len(); self.state = BatchState::AwaitingProcessing(peer, blocks, Instant::now()); Ok(received) @@ -314,10 +281,10 @@ impl BatchInfo { BatchState::Poisoned => unreachable!("Poisoned batch"), other => { self.state = other; - Err(Err(WrongState(format!( + Err(WrongState(format!( "Download completed for batch in wrong state {:?}", self.state - )))) + ))) } } } diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 4eb73f5483..cab08dd278 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1,7 +1,6 @@ use super::batch::{BatchInfo, BatchProcessingResult, BatchState}; use super::RangeSyncType; use crate::metrics; -use crate::metrics::PEERS_PER_COLUMN_SUBNET; use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::network_context::RangeRequestId; use crate::sync::{network_context::SyncNetworkContext, BatchOperationOutcome, BatchProcessResult}; @@ -10,7 +9,6 @@ use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::service::api_types::Id; use lighthouse_network::{PeerAction, PeerId}; -use metrics::set_int_gauge; use rand::seq::SliceRandom; use rand::Rng; use slog::{crit, debug, o, warn}; @@ -265,40 +263,21 @@ impl SyncingChain { } }; - { - // A stream termination has been sent. This batch has ended. Process a completed batch. - // Remove the request from the peer's active batches - self.peers - .get_mut(peer_id) - .map(|active_requests| active_requests.remove(&batch_id)); + // A stream termination has been sent. This batch has ended. Process a completed batch. + // Remove the request from the peer's active batches + self.peers + .get_mut(peer_id) + .map(|active_requests| active_requests.remove(&batch_id)); - match batch.download_completed(blocks) { - Ok(received) => { - let awaiting_batches = batch_id - .saturating_sub(self.optimistic_start.unwrap_or(self.processing_target)) - / EPOCHS_PER_BATCH; - debug!(self.log, "Batch downloaded"; "epoch" => batch_id, "blocks" => received, "batch_state" => self.visualize_batch_state(), "awaiting_batches" => awaiting_batches); + let received = batch.download_completed(blocks)?; + let awaiting_batches = batch_id + .saturating_sub(self.optimistic_start.unwrap_or(self.processing_target)) + / EPOCHS_PER_BATCH; + debug!(self.log, "Batch downloaded"; "epoch" => batch_id, "blocks" => received, "batch_state" => self.visualize_batch_state(), "awaiting_batches" => awaiting_batches); - // pre-emptively request more blocks from peers whilst we process current blocks, - self.request_batches(network)?; - self.process_completed_batches(network) - } - Err(result) => { - let (expected_boundary, received_boundary, outcome) = result?; - warn!(self.log, "Batch received out of range blocks"; "expected_boundary" => expected_boundary, "received_boundary" => received_boundary, - "peer_id" => %peer_id, batch); - - if let BatchOperationOutcome::Failed { blacklist } = outcome { - return Err(RemoveChain::ChainFailed { - blacklist, - failing_batch: batch_id, - }); - } - // this batch can't be used, so we need to request it again. - self.retry_batch_download(network, batch_id) - } - } - } + // pre-emptively request more blocks from peers whilst we process current blocks, + self.request_batches(network)?; + self.process_completed_batches(network) } /// Processes the batch with the given id. @@ -1125,11 +1104,6 @@ impl SyncingChain { .good_custody_subnet_peer(*subnet_id) .count(); - set_int_gauge( - &PEERS_PER_COLUMN_SUBNET, - &[&subnet_id.to_string()], - peer_count as i64, - ); peer_count > 0 }); peers_on_all_custody_subnets diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 15bdf85e20..4028530946 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -477,7 +477,7 @@ impl ChainCollection { .find(|(_, chain)| chain.has_same_target(target_head_slot, target_head_root)) { Some((&id, chain)) => { - debug!(self.log, "Adding peer to known chain"; "peer_id" => %peer, "sync_type" => ?sync_type, &chain); + debug!(self.log, "Adding peer to known chain"; "peer_id" => %peer, "sync_type" => ?sync_type, "id" => id); debug_assert_eq!(chain.target_head_root, target_head_root); debug_assert_eq!(chain.target_head_slot, target_head_slot); if let Err(remove_reason) = chain.add_peer(network, peer) { diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 78679403bb..38b032136c 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -94,6 +94,11 @@ where } } + #[cfg(test)] + pub(crate) fn __failed_chains(&mut self) -> Vec { + self.failed_chains.keys().copied().collect() + } + pub fn state(&self) -> SyncChainStatus { self.chains.state() } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 9ab581950c..10117285eb 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -27,6 +27,7 @@ use beacon_chain::{ PayloadVerificationOutcome, PayloadVerificationStatus, }; use beacon_processor::WorkEvent; +use lighthouse_network::discovery::CombinedKey; use lighthouse_network::{ rpc::{RPCError, RequestType, RpcErrorResponse}, service::api_types::{ @@ -39,18 +40,16 @@ use lighthouse_network::{ use slog::info; use slot_clock::{SlotClock, TestingSlotClock}; use tokio::sync::mpsc; -use types::ForkContext; use types::{ data_column_sidecar::ColumnIndex, test_utils::{SeedableRng, TestRandom, XorShiftRng}, - BeaconState, BeaconStateBase, BlobSidecar, DataColumnSidecar, EthSpec, ForkName, Hash256, - MinimalEthSpec as E, SignedBeaconBlock, Slot, + BeaconState, BeaconStateBase, BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, ForkName, + Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, }; const D: Duration = Duration::new(0, 0); const PARENT_FAIL_TOLERANCE: u8 = SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS; const SAMPLING_REQUIRED_SUCCESSES: usize = 2; - type DCByRootIds = Vec; type DCByRootId = (SyncRequestId, Vec); @@ -117,7 +116,9 @@ impl TestRig { let spec = chain.spec.clone(); - let rng = XorShiftRng::from_seed([42; 16]); + // deterministic seed + let rng = ChaCha20Rng::from_seed([0u8; 32]); + TestRig { beacon_processor_rx, beacon_processor_rx_queue: vec![], @@ -154,7 +155,7 @@ impl TestRig { } } - fn test_setup_after_fulu() -> Option { + pub fn test_setup_after_fulu() -> Option { let r = Self::test_setup(); if r.fork_name.fulu_enabled() { Some(r) @@ -369,20 +370,26 @@ impl TestRig { } pub fn new_connected_peer(&mut self) -> PeerId { + let key = self.determinstic_key(); self.network_globals .peers .write() - .__add_connected_peer_testing_only(false, &self.harness.spec) + .__add_connected_peer_testing_only(false, &self.harness.spec, key) } pub fn new_connected_supernode_peer(&mut self) -> PeerId { + let key = self.determinstic_key(); self.network_globals .peers .write() - .__add_connected_peer_testing_only(true, &self.harness.spec) + .__add_connected_peer_testing_only(true, &self.harness.spec, key) } - fn new_connected_peers_for_peerdas(&mut self) { + fn determinstic_key(&mut self) -> CombinedKey { + k256::ecdsa::SigningKey::random(&mut self.rng).into() + } + + pub fn new_connected_peers_for_peerdas(&mut self) { // Enough sampling peers with few columns for _ in 0..100 { self.new_connected_peer(); @@ -706,7 +713,6 @@ impl TestRig { self.complete_data_columns_by_root_request(id, data_columns); // Expect work event - // TODO(das): worth it to append sender id to the work event for stricter assertion? self.expect_rpc_sample_verify_work_event(); // Respond with valid result @@ -748,7 +754,6 @@ impl TestRig { } // Expect work event - // TODO(das): worth it to append sender id to the work event for stricter assertion? self.expect_rpc_custody_column_work_event(); // Respond with valid result @@ -1113,7 +1118,7 @@ impl TestRig { } #[track_caller] - fn expect_empty_network(&mut self) { + pub fn expect_empty_network(&mut self) { self.drain_network_rx(); if !self.network_rx_queue.is_empty() { let n = self.network_rx_queue.len(); diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index 6ed5c7f8fa..ef2bec80b8 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -7,12 +7,13 @@ use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_processor::WorkEvent; use lighthouse_network::NetworkGlobals; +use rand_chacha::ChaCha20Rng; use slog::Logger; use slot_clock::ManualSlotClock; use std::sync::Arc; use store::MemoryStore; use tokio::sync::mpsc; -use types::{test_utils::XorShiftRng, ChainSpec, ForkName, MinimalEthSpec as E}; +use types::{ChainSpec, ForkName, MinimalEthSpec as E}; mod lookups; mod range; @@ -61,7 +62,7 @@ struct TestRig { /// Beacon chain harness harness: BeaconChainHarness>, /// `rng` for generating test blocks and blobs. - rng: XorShiftRng, + rng: ChaCha20Rng, fork_name: ForkName, log: Logger, spec: Arc, diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index cfd89f7b44..ddd4626cce 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -1,22 +1,28 @@ use super::*; +use crate::network_beacon_processor::ChainSegmentProcessId; use crate::status::ToStatusMessage; use crate::sync::manager::SLOT_IMPORT_TOLERANCE; +use crate::sync::network_context::RangeRequestId; use crate::sync::range_sync::RangeSyncType; use crate::sync::SyncMessage; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; use beacon_chain::{block_verification_types::RpcBlock, EngineState, NotifyExecutionLayer}; +use beacon_processor::WorkType; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, DataColumnsByRangeRequest, OldBlocksByRangeRequest, OldBlocksByRangeRequestV2, }; use lighthouse_network::rpc::{RequestType, StatusMessage}; -use lighthouse_network::service::api_types::{AppRequestId, Id, SyncRequestId}; +use lighthouse_network::service::api_types::{ + AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, DataColumnsByRangeRequestId, + SyncRequestId, +}; use lighthouse_network::{PeerId, SyncInfo}; use std::time::Duration; use types::{ - BlobSidecarList, BlockImportSource, EthSpec, Hash256, MinimalEthSpec as E, SignedBeaconBlock, - SignedBeaconBlockHash, Slot, + BlobSidecarList, BlockImportSource, Epoch, EthSpec, Hash256, MinimalEthSpec as E, + SignedBeaconBlock, SignedBeaconBlockHash, Slot, }; const D: Duration = Duration::new(0, 0); @@ -28,8 +34,8 @@ pub(crate) enum DataSidecars { enum ByRangeDataRequestIds { PreDeneb, - PrePeerDAS(Id, PeerId), - PostPeerDAS(Vec<(Id, PeerId)>), + PrePeerDAS(BlobsByRangeRequestId, PeerId), + PostPeerDAS(Vec<(DataColumnsByRangeRequestId, PeerId)>), } /// Sync tests are usually written in the form: @@ -40,7 +46,7 @@ enum ByRangeDataRequestIds { /// To make writting tests succint, the machinery in this testing rig automatically identifies /// _which_ request to complete. Picking the right request is critical for tests to pass, so this /// filter allows better expressivity on the criteria to identify the right request. -#[derive(Default)] +#[derive(Default, Debug, Clone)] struct RequestFilter { peer: Option, epoch: Option, @@ -71,7 +77,7 @@ impl TestRig { /// Produce a head peer with an advanced head fn add_head_peer_with_root(&mut self, head_root: Hash256) -> PeerId { let local_info = self.local_info(); - self.add_peer(SyncInfo { + self.add_random_peer(SyncInfo { head_root, head_slot: local_info.head_slot + 1 + Slot::new(SLOT_IMPORT_TOLERANCE as u64), ..local_info @@ -87,7 +93,7 @@ impl TestRig { fn add_finalized_peer_with_root(&mut self, finalized_root: Hash256) -> PeerId { let local_info = self.local_info(); let finalized_epoch = local_info.finalized_epoch + 2; - self.add_peer(SyncInfo { + self.add_random_peer(SyncInfo { finalized_epoch, finalized_root, head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), @@ -95,6 +101,17 @@ impl TestRig { }) } + fn finalized_remote_info_advanced_by(&self, advanced_epochs: Epoch) -> SyncInfo { + let local_info = self.local_info(); + let finalized_epoch = local_info.finalized_epoch + advanced_epochs; + SyncInfo { + finalized_epoch, + finalized_root: Hash256::random(), + head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), + head_root: Hash256::random(), + } + } + fn local_info(&self) -> SyncInfo { let StatusMessage { fork_digest: _, @@ -111,28 +128,59 @@ impl TestRig { } } - fn add_peer(&mut self, remote_info: SyncInfo) -> PeerId { + fn add_random_peer_not_supernode(&mut self, remote_info: SyncInfo) -> PeerId { + let peer_id = self.new_connected_peer(); + self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info)); + peer_id + } + + fn add_random_peer(&mut self, remote_info: SyncInfo) -> PeerId { // Create valid peer known to network globals // TODO(fulu): Using supernode peers to ensure we have peer across all column // subnets for syncing. Should add tests connecting to full node peers. let peer_id = self.new_connected_supernode_peer(); // Send peer to sync - self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info.clone())); + self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info)); peer_id } + fn add_random_peers(&mut self, remote_info: SyncInfo, count: usize) { + for _ in 0..count { + let peer = self.new_connected_peer(); + self.add_peer(peer, remote_info.clone()); + } + } + + fn add_peer(&mut self, peer: PeerId, remote_info: SyncInfo) { + self.send_sync_message(SyncMessage::AddPeer(peer, remote_info)); + } + fn assert_state(&self, state: RangeSyncType) { assert_eq!( self.sync_manager .range_sync_state() .expect("State is ok") - .expect("Range should be syncing") + .expect("Range should be syncing, there are no chains") .0, state, "not expected range sync state" ); } + fn assert_no_chains_exist(&self) { + if let Some(chain) = self.sync_manager.get_range_sync_chains().unwrap() { + panic!("There still exists a chain {chain:?}"); + } + } + + fn assert_no_failed_chains(&mut self) { + assert_eq!( + self.sync_manager.__range_failed_chains(), + Vec::::new(), + "Expected no failed chains" + ) + } + #[track_caller] fn expect_chain_segments(&mut self, count: usize) { for i in 0..count { @@ -151,7 +199,7 @@ impl TestRig { fn find_blocks_by_range_request( &mut self, request_filter: RequestFilter, - ) -> ((Id, PeerId), ByRangeDataRequestIds) { + ) -> ((BlocksByRangeRequestId, PeerId), ByRangeDataRequestIds) { let filter_f = |peer: PeerId, start_slot: u64| { if let Some(expected_epoch) = request_filter.epoch { let epoch = Slot::new(start_slot).epoch(E::slots_per_epoch()).as_u64(); @@ -167,7 +215,7 @@ impl TestRig { true }; - let block_req_id = self + let block_req = self .pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { peer_id, @@ -175,11 +223,13 @@ impl TestRig { RequestType::BlocksByRange(OldBlocksByRangeRequest::V2( OldBlocksByRangeRequestV2 { start_slot, .. }, )), - request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + request_id: AppRequestId::Sync(SyncRequestId::BlocksByRange(id)), } if filter_f(*peer_id, *start_slot) => Some((*id, *peer_id)), _ => None, }) - .expect("Should have a blocks by range request"); + .unwrap_or_else(|e| { + panic!("Should have a BlocksByRange request, filter {request_filter:?}: {e:?}") + }); let by_range_data_requests = if self.after_fulu() { let mut data_columns_requests = vec![]; @@ -190,14 +240,14 @@ impl TestRig { RequestType::DataColumnsByRange(DataColumnsByRangeRequest { start_slot, .. }), - request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRange(id)), } if filter_f(*peer_id, *start_slot) => Some((*id, *peer_id)), _ => None, }) { data_columns_requests.push(data_columns_request); } if data_columns_requests.is_empty() { - panic!("Found zero DataColumnsByRange requests"); + panic!("Found zero DataColumnsByRange requests, filter {request_filter:?}"); } ByRangeDataRequestIds::PostPeerDAS(data_columns_requests) } else if self.after_deneb() { @@ -206,29 +256,34 @@ impl TestRig { NetworkMessage::SendRequest { peer_id, request: RequestType::BlobsByRange(BlobsByRangeRequest { start_slot, .. }), - request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + request_id: AppRequestId::Sync(SyncRequestId::BlobsByRange(id)), } if filter_f(*peer_id, *start_slot) => Some((*id, *peer_id)), _ => None, }) - .expect("Should have a blobs by range request"); + .unwrap_or_else(|e| { + panic!("Should have a blobs by range request, filter {request_filter:?}: {e:?}") + }); ByRangeDataRequestIds::PrePeerDAS(id, peer) } else { ByRangeDataRequestIds::PreDeneb }; - (block_req_id, by_range_data_requests) + (block_req, by_range_data_requests) } - fn find_and_complete_blocks_by_range_request(&mut self, request_filter: RequestFilter) { + fn find_and_complete_blocks_by_range_request( + &mut self, + request_filter: RequestFilter, + ) -> RangeRequestId { let ((blocks_req_id, block_peer), by_range_data_request_ids) = self.find_blocks_by_range_request(request_filter); // Complete the request with a single stream termination self.log(&format!( - "Completing BlocksByRange request {blocks_req_id} with empty stream" + "Completing BlocksByRange request {blocks_req_id:?} with empty stream" )); self.send_sync_message(SyncMessage::RpcBlock { - request_id: SyncRequestId::RangeBlockAndBlobs { id: blocks_req_id }, + request_id: SyncRequestId::BlocksByRange(blocks_req_id), peer_id: block_peer, beacon_block: None, seen_timestamp: D, @@ -239,10 +294,10 @@ impl TestRig { ByRangeDataRequestIds::PrePeerDAS(id, peer_id) => { // Complete the request with a single stream termination self.log(&format!( - "Completing BlobsByRange request {id} with empty stream" + "Completing BlobsByRange request {id:?} with empty stream" )); self.send_sync_message(SyncMessage::RpcBlob { - request_id: SyncRequestId::RangeBlockAndBlobs { id }, + request_id: SyncRequestId::BlobsByRange(id), peer_id, blob_sidecar: None, seen_timestamp: D, @@ -252,10 +307,10 @@ impl TestRig { // Complete the request with a single stream termination for (id, peer_id) in data_column_req_ids { self.log(&format!( - "Completing DataColumnsByRange request {id} with empty stream" + "Completing DataColumnsByRange request {id:?} with empty stream" )); self.send_sync_message(SyncMessage::RpcDataColumn { - request_id: SyncRequestId::RangeBlockAndBlobs { id }, + request_id: SyncRequestId::DataColumnsByRange(id), peer_id, data_column: None, seen_timestamp: D, @@ -263,6 +318,60 @@ impl TestRig { } } } + + blocks_req_id.parent_request_id.requester + } + + fn find_and_complete_processing_chain_segment(&mut self, id: ChainSegmentProcessId) { + self.pop_received_processor_event(|ev| { + (ev.work_type() == WorkType::ChainSegment).then_some(()) + }) + .unwrap_or_else(|e| panic!("Expected chain segment work event: {e}")); + + self.log(&format!( + "Completing ChainSegment processing work {id:?} with success" + )); + self.send_sync_message(SyncMessage::BatchProcessed { + sync_type: id, + result: crate::sync::BatchProcessResult::Success { + sent_blocks: 8, + imported_blocks: 8, + }, + }); + } + + fn complete_and_process_range_sync_until( + &mut self, + last_epoch: u64, + request_filter: RequestFilter, + ) { + for epoch in 0..last_epoch { + // Note: In this test we can't predict the block peer + let id = + self.find_and_complete_blocks_by_range_request(request_filter.clone().epoch(epoch)); + if let RangeRequestId::RangeSync { batch_id, .. } = id { + assert_eq!(batch_id.as_u64(), epoch, "Unexpected batch_id"); + } else { + panic!("unexpected RangeRequestId {id:?}"); + } + + let id = match id { + RangeRequestId::RangeSync { chain_id, batch_id } => { + ChainSegmentProcessId::RangeBatchId(chain_id, batch_id) + } + RangeRequestId::BackfillSync { batch_id } => { + ChainSegmentProcessId::BackSyncBatchId(batch_id) + } + }; + + self.find_and_complete_processing_chain_segment(id); + if epoch < last_epoch - 1 { + self.assert_state(RangeSyncType::Finalized); + } else { + self.assert_no_chains_exist(); + self.assert_no_failed_chains(); + } + } } async fn create_canonical_block(&mut self) -> (SignedBeaconBlock, Option>) { @@ -439,3 +548,65 @@ fn pause_and_resume_on_ee_offline() { // The head chain and finalized chain (2) should be in the processing queue rig.expect_chain_segments(2); } + +/// To attempt to finalize the peer's status finalized checkpoint we synced to its finalized epoch + +/// 2 epochs + 1 slot. +const EXTRA_SYNCED_EPOCHS: u64 = 2 + 1; + +#[test] +fn finalized_sync_enough_global_custody_peers_few_chain_peers() { + // Run for all forks + let mut r = TestRig::test_setup(); + // This test creates enough global custody peers to satisfy column queries but only adds few + // peers to the chain + r.new_connected_peers_for_peerdas(); + + let advanced_epochs: u64 = 2; + let remote_info = r.finalized_remote_info_advanced_by(advanced_epochs.into()); + + // Current priorization only sends batches to idle peers, so we need enough peers for each batch + // TODO: Test this with a single peer in the chain, it should still work + r.add_random_peers( + remote_info, + (advanced_epochs + EXTRA_SYNCED_EPOCHS) as usize, + ); + r.assert_state(RangeSyncType::Finalized); + + let last_epoch = advanced_epochs + EXTRA_SYNCED_EPOCHS; + r.complete_and_process_range_sync_until(last_epoch, filter()); +} + +#[test] +fn finalized_sync_not_enough_custody_peers_on_start() { + let mut r = TestRig::test_setup(); + // Only run post-PeerDAS + if !r.fork_name.fulu_enabled() { + return; + } + + let advanced_epochs: u64 = 2; + let remote_info = r.finalized_remote_info_advanced_by(advanced_epochs.into()); + + // Unikely that the single peer we added has enough columns for us. Tests are determinstic and + // this error should never be hit + r.add_random_peer_not_supernode(remote_info.clone()); + r.assert_state(RangeSyncType::Finalized); + + // Because we don't have enough peers on all columns we haven't sent any request. + // NOTE: There's a small chance that this single peer happens to custody exactly the set we + // expect, in that case the test will fail. Find a way to make the test deterministic. + r.expect_empty_network(); + + // Generate enough peers and supernodes to cover all custody columns + r.new_connected_peers_for_peerdas(); + // Note: not necessary to add this peers to the chain, as we draw from the global pool + // We still need to add enough peers to trigger batch downloads with idle peers. Same issue as + // the test above. + r.add_random_peers( + remote_info, + (advanced_epochs + EXTRA_SYNCED_EPOCHS - 1) as usize, + ); + + let last_epoch = advanced_epochs + EXTRA_SYNCED_EPOCHS; + r.complete_and_process_range_sync_until(last_epoch, filter()); +} diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 1339c15825..4c2daecdd3 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -18,15 +18,6 @@ pub fn cli_app() -> Command { /* * Configuration directory locations. */ - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER) - ) .arg( Arg::new("network-dir") .long("network-dir") @@ -156,16 +147,16 @@ pub fn cli_app() -> Command { .long("listen-address") .value_name("ADDRESS") .help("The address lighthouse will listen for UDP and TCP connections. To listen \ - over IpV4 and IpV6 set this flag twice with the different values.\n\ + over IPv4 and IPv6 set this flag twice with the different values.\n\ Examples:\n\ - --listen-address '0.0.0.0' will listen over IPv4.\n\ - --listen-address '::' will listen over IPv6.\n\ - --listen-address '0.0.0.0' --listen-address '::' will listen over both \ IPv4 and IPv6. The order of the given addresses is not relevant. However, \ - multiple IPv4, or multiple IPv6 addresses will not be accepted.") + multiple IPv4, or multiple IPv6 addresses will not be accepted. \ + If omitted, Lighthouse will listen on all interfaces, for both IPv4 and IPv6.") .action(ArgAction::Append) .num_args(0..=2) - .default_value("0.0.0.0") .display_order(0) ) .arg( @@ -185,8 +176,7 @@ pub fn cli_app() -> Command { .long("port6") .value_name("PORT") .help("The TCP/UDP ports to listen on over IPv6 when listening over both IPv4 and \ - IPv6. Defaults to 9090 when required. The Quic UDP port will be set to this value + 1.") - .default_value("9090") + IPv6. Defaults to --port. The Quic UDP port will be set to this value + 1.") .action(ArgAction::Set) .display_order(0) ) @@ -1504,9 +1494,18 @@ pub fn cli_app() -> Command { .arg( Arg::new("light-client-server") .long("light-client-server") - .help("Act as a full node supporting light clients on the p2p network \ - [experimental]") + .help("DEPRECATED") .action(ArgAction::SetTrue) + + .help_heading(FLAG_HEADER) + .display_order(0) + ) + .arg( + Arg::new("disable-light-client-server") + .long("disable-light-client-server") + .help("Disables light client support on the p2p network") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) .display_order(0) ) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 6d3c18d363..24d569bea2 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -176,11 +176,19 @@ pub fn get_config( parse_required(cli_args, "http-duplicate-block-status")?; client_config.http_api.enable_light_client_server = - cli_args.get_flag("light-client-server"); + !cli_args.get_flag("disable-light-client-server"); } if cli_args.get_flag("light-client-server") { - client_config.chain.enable_light_client_server = true; + warn!( + log, + "The --light-client-server flag is deprecated. The light client server is enabled \ + by default" + ); + } + + if cli_args.get_flag("disable-light-client-server") { + client_config.chain.enable_light_client_server = false; } if let Some(cache_size) = clap_utils::parse_optional(cli_args, "shuffling-cache-size")? { @@ -897,12 +905,13 @@ pub fn parse_listening_addresses( ) -> Result { let listen_addresses_str = cli_args .get_many::("listen-address") - .expect("--listen_addresses has a default value"); + .unwrap_or_default(); let use_zero_ports = parse_flag(cli_args, "zero-ports"); // parse the possible ips let mut maybe_ipv4 = None; let mut maybe_ipv6 = None; + for addr_str in listen_addresses_str { let addr = addr_str.parse::().map_err(|parse_error| { format!("Failed to parse listen-address ({addr_str}) as an Ip address: {parse_error}") @@ -912,8 +921,8 @@ pub fn parse_listening_addresses( IpAddr::V4(v4_addr) => match &maybe_ipv4 { Some(first_ipv4_addr) => { return Err(format!( - "When setting the --listen-address option twice, use an IpV4 address and an Ipv6 address. \ - Got two IpV4 addresses {first_ipv4_addr} and {v4_addr}" + "When setting the --listen-address option twice, use an IPv4 address and an IPv6 address. \ + Got two IPv4 addresses {first_ipv4_addr} and {v4_addr}" )); } None => maybe_ipv4 = Some(v4_addr), @@ -921,8 +930,8 @@ pub fn parse_listening_addresses( IpAddr::V6(v6_addr) => match &maybe_ipv6 { Some(first_ipv6_addr) => { return Err(format!( - "When setting the --listen-address option twice, use an IpV4 address and an Ipv6 address. \ - Got two IpV6 addresses {first_ipv6_addr} and {v6_addr}" + "When setting the --listen-address option twice, use an IPv4 address and an IPv6 address. \ + Got two IPv6 addresses {first_ipv6_addr} and {v6_addr}" )); } None => maybe_ipv6 = Some(v6_addr), @@ -936,12 +945,11 @@ pub fn parse_listening_addresses( .expect("--port has a default value") .parse::() .map_err(|parse_error| format!("Failed to parse --port as an integer: {parse_error}"))?; - let port6 = cli_args + let maybe_port6 = cli_args .get_one::("port6") .map(|s| str::parse::(s)) .transpose() - .map_err(|parse_error| format!("Failed to parse --port6 as an integer: {parse_error}"))? - .unwrap_or(9090); + .map_err(|parse_error| format!("Failed to parse --port6 as an integer: {parse_error}"))?; // parse the possible discovery ports. let maybe_disc_port = cli_args @@ -977,11 +985,22 @@ pub fn parse_listening_addresses( format!("Failed to parse --quic6-port as an integer: {parse_error}") })?; + // Here we specify the default listening addresses for Lighthouse. + // By default, we listen on 0.0.0.0. + // + // IF the host supports a globally routable IPv6 address, we also listen on ::. + if matches!((maybe_ipv4, maybe_ipv6), (None, None)) { + maybe_ipv4 = Some(Ipv4Addr::UNSPECIFIED); + + if NetworkConfig::is_ipv6_supported() { + maybe_ipv6 = Some(Ipv6Addr::UNSPECIFIED); + } + } + // Now put everything together let listening_addresses = match (maybe_ipv4, maybe_ipv6) { (None, None) => { - // This should never happen unless clap is broken - return Err("No listening addresses provided".into()); + unreachable!("This path is handled above this match statement"); } (None, Some(ipv6)) => { // A single ipv6 address was provided. Set the ports @@ -989,6 +1008,10 @@ pub fn parse_listening_addresses( warn!(log, "When listening only over IPv6, use the --port flag. The value of --port6 will be ignored."); } + // If we are only listening on ipv6 and the user has specified --port6, lets just use + // that. + let port = maybe_port6.unwrap_or(port); + // use zero ports if required. If not, use the given port. let tcp_port = use_zero_ports .then(unused_port::unused_tcp6_port) @@ -1055,6 +1078,9 @@ pub fn parse_listening_addresses( }) } (Some(ipv4), Some(ipv6)) => { + // If --port6 is not set, we use --port + let port6 = maybe_port6.unwrap_or(port); + let ipv4_tcp_port = use_zero_ports .then(unused_port::unused_tcp4_port) .transpose()? @@ -1074,7 +1100,7 @@ pub fn parse_listening_addresses( ipv4_tcp_port + 1 }); - // Defaults to 9090 when required + // Defaults to 9000 when required let ipv6_tcp_port = use_zero_ports .then(unused_port::unused_tcp6_port) .transpose()? @@ -1413,7 +1439,7 @@ pub fn set_network_config( } // Light client server config. - config.enable_light_client_server = parse_flag(cli_args, "light-client-server"); + config.enable_light_client_server = !parse_flag(cli_args, "disable-light-client-server"); // The self limiter is enabled by default. If the `self-limiter-protocols` flag is not provided, // the default params will be used. diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 134be9ec0d..e4a857b799 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -516,7 +516,7 @@ impl, Cold: ItemStore> HotColdDB .ok_or(Error::AddPayloadLogicError) } - /// Prepare a signed beacon block for storage in the datbase *without* its payload. + /// Prepare a signed beacon block for storage in the database *without* its payload. pub fn blinded_block_as_kv_store_ops( &self, key: &Hash256, @@ -666,7 +666,9 @@ impl, Cold: ItemStore> HotColdDB .hot_db .get_bytes(ExecutionPayload::::db_column(), key)? { - Some(bytes) => Ok(Some(ExecutionPayload::from_ssz_bytes(&bytes, fork_name)?)), + Some(bytes) => Ok(Some(ExecutionPayload::from_ssz_bytes_by_fork( + &bytes, fork_name, + )?)), None => Ok(None), } } diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index c0f6b5485e..0dc1000aa0 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -162,8 +162,8 @@ To listen over both IPv4 and IPv6: > > **IPv6**: > -> It listens on the default value of --port6 (`9090`) for both UDP and TCP. -> QUIC will use port `9091` for UDP, which is the default `--port6` value (`9090`) + 1. +> It listens on the default value of --port6 (`9000`) for both UDP and TCP. +> QUIC will use port `9001` for UDP, which is the default `--port6` value (`9000`) + 1. > When using `--listen-address :: --listen-address --port 9909 --discovery-port6 9999`, listening will be set up as follows: > @@ -174,8 +174,8 @@ To listen over both IPv4 and IPv6: > > **IPv6**: > -> It listens on the default value of `--port6` (`9090`) for TCP, and port `9999` for UDP. -> QUIC will use port `9091` for UDP, which is the default `--port6` value (`9090`) + 1. +> It listens on the default value of `--port6` (`9000`) for TCP, and port `9999` for UDP. +> QUIC will use port `9001` for UDP, which is the default `--port6` value (`9000`) + 1. ### Configuring Lighthouse to advertise IPv6 reachable addresses diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 2d12010094..cbcb1ec5a3 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -230,7 +230,7 @@ Options: peer without an ENR. --listen-address [
...] The address lighthouse will listen for UDP and TCP connections. To - listen over IpV4 and IpV6 set this flag twice with the different + listen over IPv4 and IPv6 set this flag twice with the different values. Examples: - --listen-address '0.0.0.0' will listen over IPv4. @@ -238,7 +238,8 @@ Options: - --listen-address '0.0.0.0' --listen-address '::' will listen over both IPv4 and IPv6. The order of the given addresses is not relevant. However, multiple IPv4, or multiple IPv6 addresses will not be - accepted. [default: 0.0.0.0] + accepted. If omitted, Lighthouse will listen on all interfaces, for + both IPv4 and IPv6. --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] @@ -301,8 +302,8 @@ Options: [default: 9000] --port6 The TCP/UDP ports to listen on over IPv6 when listening over both IPv4 - and IPv6. Defaults to 9090 when required. The Quic UDP port will be - set to this value + 1. [default: 9090] + and IPv6. Defaults to --port. The Quic UDP port will be set to this + value + 1. --prepare-payload-lookahead The time before the start of a proposal slot at which payload attributes should be sent. Low values are useful for execution nodes @@ -458,6 +459,8 @@ Flags: boot. --disable-inbound-rate-limiter Disables the inbound rate limiter (requests received by this node). + --disable-light-client-server + Disables light client support on the p2p network --disable-log-timestamp If present, do not include timestamps in logging output. --disable-malloc-tuning @@ -511,8 +514,7 @@ Flags: already-subscribed subnets, use with --subscribe-all-subnets to ensure all attestations are received for import. --light-client-server - Act as a full node supporting light clients on the p2p network - [experimental] + DEPRECATED --log-color Force outputting colors when emitting logs to the terminal. --logfile-compress diff --git a/book/src/help_vc.md b/book/src/help_vc.md index 71e21d68c9..948a09f44d 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -18,16 +18,16 @@ Options: certificate path. --broadcast Comma-separated list of beacon API topics to broadcast to all beacon - nodes. Possible values are: none, attestations, blocks, subscriptions, - sync-committee. Default (when flag is omitted) is to broadcast - subscriptions only. + nodes. Default (when flag is omitted) is to broadcast subscriptions + only. [possible values: none, attestations, blocks, subscriptions, + sync-committee] --builder-boost-factor Defines the boost factor, a percentage multiplier to apply to the builder's payload value when choosing between a builder payload header and payload from the local execution node. - --builder-registration-timestamp-override + --builder-registration-timestamp-override This flag takes a unix timestamp value that will be used to override - the timestamp used in the builder api registration + the timestamp used in the builder api registration. -d, --datadir Used to specify a custom root data directory for lighthouse keys and databases. Defaults to $HOME/.lighthouse/{network} where network is @@ -41,7 +41,7 @@ Options: The gas limit to be used in all builder proposals for all validators managed by this validator client. Note this will not necessarily be used if the gas limit set here moves too far from the previous block's - gas limit. [default: 30,000,000] + gas limit. [default: 30000000] --genesis-state-url A URL of a beacon-API compatible server from which to download the genesis state. Checkpoint sync server URLs can generally be used with @@ -68,7 +68,8 @@ Options: is supplied, the CORS allowed origin is set to the listen address of this server (e.g., http://localhost:5062). --http-port - Set the listen TCP port for the RESTful HTTP API server. + Set the listen TCP port for the RESTful HTTP API server. [default: + 5062] --http-token-path Path to file containing the HTTP API token for validator client authentication. If not specified, defaults to @@ -96,6 +97,7 @@ Options: set to 0, background file logging is disabled. [default: 200] --metrics-address
Set the listen address for the Prometheus metrics HTTP server. + [default: 127.0.0.1] --metrics-allow-origin Set the value of the Access-Control-Allow-Origin response HTTP header. Use * to allow any origin (not recommended in production). If no value @@ -103,6 +105,7 @@ Options: this server (e.g., http://localhost:5064). --metrics-port Set the listen TCP port for the Prometheus metrics HTTP server. + [default: 5064] --monitoring-endpoint
Enables the monitoring service for sending system metrics to a remote endpoint. This can be used to monitor your setup on certain services @@ -113,7 +116,7 @@ Options: provide an untrusted URL. --monitoring-endpoint-period Defines how many seconds to wait between each message sent to the - monitoring-endpoint. Default: 60s + monitoring-endpoint. [default: 60] --network Name of the Eth2 chain Lighthouse will sync and follow. [possible values: mainnet, gnosis, chiado, sepolia, holesky] @@ -145,8 +148,8 @@ Options: each validator along with the common slashing protection database and the validator_definitions.yml --web3-signer-keep-alive-timeout - Keep-alive timeout for each web3signer connection. Set to 'null' to - never timeout [default: 20000] + Keep-alive timeout for each web3signer connection. Set to '0' to never + timeout. [default: 20000] --web3-signer-max-idle-connections Maximum number of idle connections to maintain per web3signer host. Default is unlimited. diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index 7c8d2b16fd..94dcfac5e1 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boot_node" -version = "6.0.1" +version = "7.0.0-beta.0" authors = ["Sigma Prime "] edition = { workspace = true } diff --git a/boot_node/src/cli.rs b/boot_node/src/cli.rs index 440a9d27e2..0f274885d1 100644 --- a/boot_node/src/cli.rs +++ b/boot_node/src/cli.rs @@ -13,15 +13,6 @@ pub fn cli_app() -> Command { surface compared to a full beacon node.") .styles(get_color_style()) .display_order(0) - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER) - ) .arg( Arg::new("enr-address") .long("enr-address") diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 1ab07a122c..8b4f81a641 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -49,6 +49,7 @@ pub const CONSENSUS_BLOCK_VALUE_HEADER: &str = "Eth-Consensus-Block-Value"; pub const CONTENT_TYPE_HEADER: &str = "Content-Type"; pub const SSZ_CONTENT_TYPE_HEADER: &str = "application/octet-stream"; +pub const JSON_CONTENT_TYPE_HEADER: &str = "application/json"; #[derive(Debug)] pub enum Error { @@ -112,9 +113,9 @@ impl Error { Error::InvalidSignatureHeader => None, Error::MissingSignatureHeader => None, Error::InvalidJson(_) => None, + Error::InvalidSsz(_) => None, Error::InvalidServerSentEvent(_) => None, Error::InvalidHeaders(_) => None, - Error::InvalidSsz(_) => None, Error::TokenReadError(..) => None, Error::NoServerPubkey | Error::NoToken => None, } diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 9de9f06d78..b0beab86f6 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1669,7 +1669,7 @@ impl FullBlockContents { } /// SSZ decode with fork variant determined by slot. - pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { + pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { let slot_len = ::ssz_fixed_len(); let slot_bytes = bytes .get(0..slot_len) @@ -1683,10 +1683,7 @@ impl FullBlockContents { } /// SSZ decode with fork variant passed in explicitly. - pub fn from_ssz_bytes_for_fork( - bytes: &[u8], - fork_name: ForkName, - ) -> Result { + pub fn from_ssz_bytes_for_fork(bytes: &[u8], fork_name: ForkName) -> Result { if fork_name.deneb_enabled() { let mut builder = ssz::SszDecoderBuilder::new(bytes); @@ -1841,7 +1838,7 @@ impl PublishBlockRequest { } /// SSZ decode with fork variant determined by `fork_name`. - pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { + pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { if fork_name.deneb_enabled() { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_anonymous_variable_length_item()?; @@ -1850,7 +1847,7 @@ impl PublishBlockRequest { let mut decoder = builder.build()?; let block = decoder.decode_next_with(|bytes| { - SignedBeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) + SignedBeaconBlock::from_ssz_bytes_by_fork(bytes, fork_name) })?; let kzg_proofs = decoder.decode_next()?; let blobs = decoder.decode_next()?; @@ -1859,7 +1856,7 @@ impl PublishBlockRequest { Some((kzg_proofs, blobs)), )) } else { - SignedBeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) + SignedBeaconBlock::from_ssz_bytes_by_fork(bytes, fork_name) .map(|block| PublishBlockRequest::Block(Arc::new(block))) } } @@ -1951,6 +1948,24 @@ pub enum FullPayloadContents { PayloadAndBlobs(ExecutionPayloadAndBlobs), } +impl ForkVersionDecode for FullPayloadContents { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + if fork_name.deneb_enabled() { + Ok(Self::PayloadAndBlobs( + ExecutionPayloadAndBlobs::from_ssz_bytes_by_fork(bytes, fork_name)?, + )) + } else if fork_name.bellatrix_enabled() { + Ok(Self::Payload(ExecutionPayload::from_ssz_bytes_by_fork( + bytes, fork_name, + )?)) + } else { + Err(ssz::DecodeError::BytesInvalid(format!( + "FullPayloadContents decoding for {fork_name} not implemented" + ))) + } + } +} + impl FullPayloadContents { pub fn new( execution_payload: ExecutionPayload, @@ -2017,6 +2032,36 @@ pub struct ExecutionPayloadAndBlobs { pub blobs_bundle: BlobsBundle, } +impl ForkVersionDecode for ExecutionPayloadAndBlobs { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + let mut builder = ssz::SszDecoderBuilder::new(bytes); + builder.register_anonymous_variable_length_item()?; + builder.register_type::>()?; + let mut decoder = builder.build()?; + + if fork_name.deneb_enabled() { + let execution_payload = decoder.decode_next_with(|bytes| { + ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) + })?; + let blobs_bundle = decoder.decode_next()?; + Ok(Self { + execution_payload, + blobs_bundle, + }) + } else { + Err(DecodeError::BytesInvalid(format!( + "ExecutionPayloadAndBlobs decoding for {fork_name} not implemented" + ))) + } + } +} + +#[derive(Debug)] +pub enum ContentType { + Json, + Ssz, +} + #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Encode, Decode)] #[serde(bound = "E: EthSpec")] pub struct BlobsBundle { diff --git a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml index d0b61422e0..e5f38b8c9b 100644 --- a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml @@ -35,7 +35,7 @@ DENEB_FORK_VERSION: 0x05017000 DENEB_FORK_EPOCH: 29696 # Electra ELECTRA_FORK_VERSION: 0x06017000 -ELECTRA_FORK_EPOCH: 18446744073709551615 +ELECTRA_FORK_EPOCH: 115968 # Fulu FULU_FORK_VERSION: 0x07017000 FULU_FORK_EPOCH: 18446744073709551615 @@ -127,6 +127,18 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 # `uint64(6)` MAX_BLOBS_PER_BLOCK: 6 +# Electra +# 2**7 * 10**9 (= 128,000,000,000) +MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 +# 2**8 * 10**9 (= 256,000,000,000) +MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 +# `9` +BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 +# `uint64(9)` +MAX_BLOBS_PER_BLOCK_ELECTRA: 9 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 + # DAS NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 diff --git a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml index 7564d8f0f6..af78332205 100644 --- a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml @@ -36,6 +36,10 @@ CAPELLA_FORK_EPOCH: 56832 DENEB_FORK_VERSION: 0x90000073 DENEB_FORK_EPOCH: 132608 +# Electra +ELECTRA_FORK_VERSION: 0x90000074 +ELECTRA_FORK_EPOCH: 222464 + # Time parameters # --------------------------------------------------------------- # 12 seconds @@ -73,6 +77,8 @@ PROPOSER_SCORE_BOOST: 40 REORG_HEAD_WEIGHT_THRESHOLD: 20 # 160% REORG_PARENT_WEIGHT_THRESHOLD: 160 +# `2` epochs +REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 # Deposit contract # --------------------------------------------------------------- @@ -122,6 +128,18 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 # `uint64(6)` MAX_BLOBS_PER_BLOCK: 6 +# Electra +# 2**7 * 10**9 (= 128,000,000,000) +MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 +# 2**8 * 10**9 (= 256,000,000,000) +MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 +# `9` +BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 +# `uint64(9)` +MAX_BLOBS_PER_BLOCK_ELECTRA: 9 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 + # DAS NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index a35b8c42c1..cfffdbbb09 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -17,8 +17,8 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v6.0.1-", - fallback = "Lighthouse/v6.0.1" + prefix = "Lighthouse/v7.0.0-beta.0-", + fallback = "Lighthouse/v7.0.0-beta.0" ); /// Returns the first eight characters of the latest commit hash for this build. @@ -54,7 +54,7 @@ pub fn version_with_platform() -> String { /// /// `1.5.1` pub fn version() -> &'static str { - "6.0.1" + "7.0.0-beta.0" } /// Returns the name of the current client running. @@ -71,9 +71,10 @@ mod test { #[test] fn version_formatting() { - let re = - Regex::new(r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-rc.[0-9])?(-[[:xdigit:]]{7})?\+?$") - .unwrap(); + let re = Regex::new( + r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta).[0-9])?(-[[:xdigit:]]{7})?\+?$", + ) + .unwrap(); assert!( re.is_match(VERSION), "version doesn't match regex: {}", diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation.rs index 276b27b0f8..1485842edb 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation.rs @@ -2,7 +2,6 @@ use crate::slot_data::SlotData; use crate::{test_utils::TestRandom, Hash256, Slot}; use crate::{Checkpoint, ForkVersionDeserialize}; use derivative::Derivative; -use safe_arith::ArithError; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use ssz_types::BitVector; @@ -12,22 +11,17 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; use super::{ - AggregateSignature, AttestationData, BitList, ChainSpec, CommitteeIndex, Domain, EthSpec, Fork, - SecretKey, Signature, SignedRoot, + AggregateSignature, AttestationData, BitList, ChainSpec, Domain, EthSpec, Fork, SecretKey, + Signature, SignedRoot, }; #[derive(Debug, PartialEq)] pub enum Error { SszTypesError(ssz_types::Error), AlreadySigned(usize), - SubnetCountIsZero(ArithError), IncorrectStateVariant, InvalidCommitteeLength, InvalidCommitteeIndex, - AttesterNotInCommittee(u64), - InvalidCommittee, - MissingCommittee, - NoCommitteeForSlotAndIndex { slot: Slot, index: CommitteeIndex }, } impl From for Error { @@ -587,38 +581,6 @@ pub struct SingleAttestation { pub signature: AggregateSignature, } -impl SingleAttestation { - pub fn to_attestation(&self, committee: &[usize]) -> Result, Error> { - let aggregation_bit = committee - .iter() - .enumerate() - .find_map(|(i, &validator_index)| { - if self.attester_index as usize == validator_index { - return Some(i); - } - None - }) - .ok_or(Error::AttesterNotInCommittee(self.attester_index))?; - - let mut committee_bits: BitVector = BitVector::default(); - committee_bits - .set(self.committee_index as usize, true) - .map_err(|_| Error::InvalidCommitteeIndex)?; - - let mut aggregation_bits = - BitList::with_capacity(committee.len()).map_err(|_| Error::InvalidCommitteeLength)?; - - aggregation_bits.set(aggregation_bit, true)?; - - Ok(Attestation::Electra(AttestationElectra { - aggregation_bits, - committee_bits, - data: self.data.clone(), - signature: self.signature.clone(), - })) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/builder_bid.rs b/consensus/types/src/builder_bid.rs index ac53c41216..49911c3909 100644 --- a/consensus/types/src/builder_bid.rs +++ b/consensus/types/src/builder_bid.rs @@ -3,25 +3,37 @@ use crate::{ ChainSpec, EthSpec, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, - ForkVersionDeserialize, SignedRoot, Uint256, + ForkVersionDecode, ForkVersionDeserialize, SignedRoot, Uint256, }; use bls::PublicKeyBytes; use bls::Signature; use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; +use ssz_derive::{Decode, Encode}; use superstruct::superstruct; use tree_hash_derive::TreeHash; #[superstruct( variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( - derive(PartialEq, Debug, Serialize, Deserialize, TreeHash, Clone), + derive( + PartialEq, + Debug, + Encode, + Serialize, + Deserialize, + TreeHash, + Decode, + Clone + ), serde(bound = "E: EthSpec", deny_unknown_fields) ), map_ref_into(ExecutionPayloadHeaderRef), map_ref_mut_into(ExecutionPayloadHeaderRefMut) )] -#[derive(PartialEq, Debug, Serialize, Deserialize, TreeHash, Clone)] +#[derive(PartialEq, Debug, Encode, Serialize, Deserialize, TreeHash, Clone)] #[serde(bound = "E: EthSpec", deny_unknown_fields, untagged)] +#[ssz(enum_behaviour = "transparent")] #[tree_hash(enum_behaviour = "transparent")] pub struct BuilderBid { #[superstruct(only(Bellatrix), partial_getter(rename = "header_bellatrix"))] @@ -65,16 +77,54 @@ impl<'a, E: EthSpec> BuilderBidRefMut<'a, E> { } } +impl ForkVersionDecode for BuilderBid { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + let builder_bid = match fork_name { + ForkName::Altair | ForkName::Base => { + return Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for ExecutionPayloadHeader: {fork_name}", + ))) + } + ForkName::Bellatrix => { + BuilderBid::Bellatrix(BuilderBidBellatrix::from_ssz_bytes(bytes)?) + } + ForkName::Capella => BuilderBid::Capella(BuilderBidCapella::from_ssz_bytes(bytes)?), + ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb::from_ssz_bytes(bytes)?), + ForkName::Electra => BuilderBid::Electra(BuilderBidElectra::from_ssz_bytes(bytes)?), + ForkName::Fulu => BuilderBid::Fulu(BuilderBidFulu::from_ssz_bytes(bytes)?), + }; + Ok(builder_bid) + } +} + impl SignedRoot for BuilderBid {} /// Validator registration, for use in interacting with servers implementing the builder API. -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] +#[derive(PartialEq, Debug, Encode, Serialize, Deserialize, Clone)] #[serde(bound = "E: EthSpec")] pub struct SignedBuilderBid { pub message: BuilderBid, pub signature: Signature, } +impl ForkVersionDecode for SignedBuilderBid { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + let mut builder = ssz::SszDecoderBuilder::new(bytes); + + builder.register_anonymous_variable_length_item()?; + builder.register_type::()?; + + let mut decoder = builder.build()?; + let message = decoder + .decode_next_with(|bytes| BuilderBid::from_ssz_bytes_by_fork(bytes, fork_name))?; + let signature = decoder.decode_next()?; + + Ok(Self { message, signature }) + } +} + impl ForkVersionDeserialize for BuilderBid { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: serde_json::value::Value, diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index f21b6e47f5..58c793027d 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -735,6 +735,10 @@ impl ChainSpec { } } + pub fn all_data_column_sidecar_subnets(&self) -> impl Iterator { + (0..self.data_column_sidecar_subnet_count).map(DataColumnSubnetId::new) + } + /// Returns a `ChainSpec` compatible with the Ethereum Foundation specification. pub fn mainnet() -> Self { Self { diff --git a/consensus/types/src/data_column_custody_group.rs b/consensus/types/src/data_column_custody_group.rs index bb204c34a2..9e9505da9f 100644 --- a/consensus/types/src/data_column_custody_group.rs +++ b/consensus/types/src/data_column_custody_group.rs @@ -17,6 +17,8 @@ pub enum DataColumnCustodyGroupError { /// The `get_custody_groups` function is used to determine the custody groups that a node is /// assigned to. /// +/// Note: `get_custody_groups(node_id, x)` is a subset of `get_custody_groups(node_id, y)` if `x < y`. +/// /// spec: https://github.com/ethereum/consensus-specs/blob/8e0d0d48e81d6c7c5a8253ab61340f5ea5bac66a/specs/fulu/das-core.md#get_custody_groups pub fn get_custody_groups( raw_node_id: [u8; 32], diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index 2df66343af..5d756c8529 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -40,7 +40,7 @@ pub type Withdrawals = VariableList::MaxWithdrawal map_ref_into(ExecutionPayloadHeader) )] #[derive( - Debug, Clone, Serialize, Encode, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary, + Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, )] #[derivative(PartialEq, Hash(bound = "E: EthSpec"))] #[serde(bound = "E: EthSpec", untagged)] @@ -102,8 +102,9 @@ impl<'a, E: EthSpec> ExecutionPayloadRef<'a, E> { } } -impl ExecutionPayload { - pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { +impl ForkVersionDecode for ExecutionPayload { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { match fork_name { ForkName::Base | ForkName::Altair => Err(ssz::DecodeError::BytesInvalid(format!( "unsupported fork for ExecutionPayload: {fork_name}", @@ -117,7 +118,9 @@ impl ExecutionPayload { ForkName::Fulu => ExecutionPayloadFulu::from_ssz_bytes(bytes).map(Self::Fulu), } } +} +impl ExecutionPayload { #[allow(clippy::arithmetic_side_effects)] /// Returns the maximum size of an execution payload. pub fn max_execution_payload_bellatrix_size() -> usize { diff --git a/consensus/types/src/fork_context.rs b/consensus/types/src/fork_context.rs index 33f1c51d44..a6360705ba 100644 --- a/consensus/types/src/fork_context.rs +++ b/consensus/types/src/fork_context.rs @@ -22,61 +22,22 @@ impl ForkContext { genesis_validators_root: Hash256, spec: &ChainSpec, ) -> Self { - let mut fork_to_digest = vec![( - ForkName::Base, - ChainSpec::compute_fork_digest(spec.genesis_fork_version, genesis_validators_root), - )]; - - // Only add Altair to list of forks if it's enabled - // Note: `altair_fork_epoch == None` implies altair hasn't been activated yet on the config. - if spec.altair_fork_epoch.is_some() { - fork_to_digest.push(( - ForkName::Altair, - ChainSpec::compute_fork_digest(spec.altair_fork_version, genesis_validators_root), - )); - } - - // Only add Bellatrix to list of forks if it's enabled - // Note: `bellatrix_fork_epoch == None` implies bellatrix hasn't been activated yet on the config. - if spec.bellatrix_fork_epoch.is_some() { - fork_to_digest.push(( - ForkName::Bellatrix, - ChainSpec::compute_fork_digest( - spec.bellatrix_fork_version, - genesis_validators_root, - ), - )); - } - - if spec.capella_fork_epoch.is_some() { - fork_to_digest.push(( - ForkName::Capella, - ChainSpec::compute_fork_digest(spec.capella_fork_version, genesis_validators_root), - )); - } - - if spec.deneb_fork_epoch.is_some() { - fork_to_digest.push(( - ForkName::Deneb, - ChainSpec::compute_fork_digest(spec.deneb_fork_version, genesis_validators_root), - )); - } - - if spec.electra_fork_epoch.is_some() { - fork_to_digest.push(( - ForkName::Electra, - ChainSpec::compute_fork_digest(spec.electra_fork_version, genesis_validators_root), - )); - } - - if spec.fulu_fork_epoch.is_some() { - fork_to_digest.push(( - ForkName::Fulu, - ChainSpec::compute_fork_digest(spec.fulu_fork_version, genesis_validators_root), - )); - } - - let fork_to_digest: HashMap = fork_to_digest.into_iter().collect(); + let fork_to_digest: HashMap = ForkName::list_all() + .into_iter() + .filter_map(|fork| { + if spec.fork_epoch(fork).is_some() { + Some(( + fork, + ChainSpec::compute_fork_digest( + spec.fork_version_for_name(fork), + genesis_validators_root, + ), + )) + } else { + None + } + }) + .collect(); let digest_to_fork = fork_to_digest .clone() diff --git a/consensus/types/src/fork_name.rs b/consensus/types/src/fork_name.rs index 40557e0cb9..e92db49485 100644 --- a/consensus/types/src/fork_name.rs +++ b/consensus/types/src/fork_name.rs @@ -34,14 +34,12 @@ impl ForkName { } pub fn list_all_fork_epochs(spec: &ChainSpec) -> Vec<(ForkName, Option)> { - vec![ - (ForkName::Altair, spec.altair_fork_epoch), - (ForkName::Bellatrix, spec.bellatrix_fork_epoch), - (ForkName::Capella, spec.capella_fork_epoch), - (ForkName::Deneb, spec.deneb_fork_epoch), - (ForkName::Electra, spec.electra_fork_epoch), - (ForkName::Fulu, spec.fulu_fork_epoch), - ] + ForkName::list_all() + .into_iter() + // Skip Base + .skip(1) + .map(|fork| (fork, spec.fork_epoch(fork))) + .collect() } pub fn latest() -> ForkName { diff --git a/consensus/types/src/fork_versioned_response.rs b/consensus/types/src/fork_versioned_response.rs index cd78b5b3ca..7e4efd05d6 100644 --- a/consensus/types/src/fork_versioned_response.rs +++ b/consensus/types/src/fork_versioned_response.rs @@ -4,6 +4,11 @@ use serde::{Deserialize, Deserializer, Serialize}; use serde_json::value::Value; use std::sync::Arc; +pub trait ForkVersionDecode: Sized { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result; +} + pub trait ForkVersionDeserialize: Sized + DeserializeOwned { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: Value, diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 8e664d99fa..4e88b6da77 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -181,7 +181,9 @@ pub use crate::fork::Fork; pub use crate::fork_context::ForkContext; pub use crate::fork_data::ForkData; pub use crate::fork_name::{ForkName, InconsistentFork}; -pub use crate::fork_versioned_response::{ForkVersionDeserialize, ForkVersionedResponse}; +pub use crate::fork_versioned_response::{ + ForkVersionDecode, ForkVersionDeserialize, ForkVersionedResponse, +}; pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; pub use crate::inclusion_list::{InclusionList, InclusionListTransactions, SignedInclusionList}; diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/signed_beacon_block.rs index d9bf9bf55d..eb5925a29b 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/signed_beacon_block.rs @@ -86,6 +86,17 @@ pub struct SignedBeaconBlock = FullP pub signature: Signature, } +impl> ForkVersionDecode + for SignedBeaconBlock +{ + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + Self::from_ssz_bytes_with(bytes, |bytes| { + BeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) + }) + } +} + pub type SignedBlindedBeaconBlock = SignedBeaconBlock>; impl> SignedBeaconBlock { @@ -108,16 +119,6 @@ impl> SignedBeaconBlock Self::from_ssz_bytes_with(bytes, |bytes| BeaconBlock::from_ssz_bytes(bytes, spec)) } - /// SSZ decode with explicit fork variant. - pub fn from_ssz_bytes_for_fork( - bytes: &[u8], - fork_name: ForkName, - ) -> Result { - Self::from_ssz_bytes_with(bytes, |bytes| { - BeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) - }) - } - /// SSZ decode which attempts to decode all variants (slow). pub fn any_from_ssz_bytes(bytes: &[u8]) -> Result { Self::from_ssz_bytes_with(bytes, BeaconBlock::any_from_ssz_bytes) diff --git a/consensus/types/src/sync_committee_contribution.rs b/consensus/types/src/sync_committee_contribution.rs index c348c3e8be..9bae770fe5 100644 --- a/consensus/types/src/sync_committee_contribution.rs +++ b/consensus/types/src/sync_committee_contribution.rs @@ -1,7 +1,6 @@ use super::{AggregateSignature, EthSpec, SignedRoot}; use crate::slot_data::SlotData; use crate::{test_utils::TestRandom, BitVector, Hash256, Slot, SyncCommitteeMessage}; -use safe_arith::ArithError; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -11,7 +10,6 @@ use tree_hash_derive::TreeHash; pub enum Error { SszTypesError(ssz_types::Error), AlreadySigned(usize), - SubnetCountIsZero(ArithError), } /// An aggregation of `SyncCommitteeMessage`s, used in creating a `SignedContributionAndProof`. diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index e26fe59413..bfe0f19cd0 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -8,12 +8,6 @@ edition = "2021" [dependencies] arbitrary = { workspace = true } c-kzg = { workspace = true } -# Required to maintain the pin from https://github.com/sigp/lighthouse/pull/6608 -crate_crypto_internal_eth_kzg_bls12_381 = { workspace = true } -crate_crypto_internal_eth_kzg_erasure_codes = { workspace = true } -crate_crypto_internal_eth_kzg_maybe_rayon = { workspace = true } -crate_crypto_internal_eth_kzg_polynomial = { workspace = true } -crate_crypto_kzg_multi_open_fk20 = { workspace = true } derivative = { workspace = true } ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } diff --git a/database_manager/src/cli.rs b/database_manager/src/cli.rs index 9db807df2c..c62da1206f 100644 --- a/database_manager/src/cli.rs +++ b/database_manager/src/cli.rs @@ -66,16 +66,6 @@ pub struct DatabaseManager { )] pub backend: store::config::DatabaseBackend, - #[clap( - long, - global = true, - help = "Prints help information", - action = clap::ArgAction::HelpLong, - display_order = 0, - help_heading = FLAG_HEADER - )] - help: Option, - #[clap(subcommand)] pub subcommand: DatabaseManagerSubcommand, } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 72be77a70b..74b7ddcb2a 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "6.0.1" +version = "7.0.0-beta.0" authors = ["Paul Hauner "] edition = { workspace = true } diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index c95735d41c..fc73a2cb93 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "6.0.1" +version = "7.0.0-beta.0" authors = ["Sigma Prime "] edition = { workspace = true } autotests = false diff --git a/lighthouse/src/cli.rs b/lighthouse/src/cli.rs index 90d3e811eb..ed665d2a47 100644 --- a/lighthouse/src/cli.rs +++ b/lighthouse/src/cli.rs @@ -1,9 +1,12 @@ use clap::Parser; use database_manager::cli::DatabaseManager; use serde::{Deserialize, Serialize}; +use validator_client::cli::ValidatorClient; #[derive(Parser, Clone, Deserialize, Serialize, Debug)] pub enum LighthouseSubcommands { #[clap(name = "database_manager")] - DatabaseManager(DatabaseManager), + DatabaseManager(Box), + #[clap(name = "validator_client")] + ValidatorClient(Box), } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index dd7401d49e..d7a14e3809 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -399,10 +399,10 @@ fn main() { .action(ArgAction::HelpLong) .display_order(0) .help_heading(FLAG_HEADER) + .global(true) ) .subcommand(beacon_node::cli_app()) .subcommand(boot_node::cli_app()) - .subcommand(validator_client::cli_app()) .subcommand(account_manager::cli_app()) .subcommand(validator_manager::cli_app()); @@ -673,12 +673,49 @@ fn run( return Ok(()); } - if let Ok(LighthouseSubcommands::DatabaseManager(db_manager_config)) = - LighthouseSubcommands::from_arg_matches(matches) - { - info!(log, "Running database manager for {} network", network_name); - database_manager::run(matches, &db_manager_config, environment)?; - return Ok(()); + match LighthouseSubcommands::from_arg_matches(matches) { + Ok(LighthouseSubcommands::DatabaseManager(db_manager_config)) => { + info!(log, "Running database manager for {} network", network_name); + database_manager::run(matches, &db_manager_config, environment)?; + return Ok(()); + } + Ok(LighthouseSubcommands::ValidatorClient(validator_client_config)) => { + let context = environment.core_context(); + let log = context.log().clone(); + let executor = context.executor.clone(); + let config = validator_client::Config::from_cli( + matches, + &validator_client_config, + context.log(), + ) + .map_err(|e| format!("Unable to initialize validator config: {}", e))?; + // Dump configs if `dump-config` or `dump-chain-config` flags are set + clap_utils::check_dump_configs::<_, E>(matches, &config, &context.eth2_config.spec)?; + + let shutdown_flag = matches.get_flag("immediate-shutdown"); + if shutdown_flag { + info!(log, "Validator client immediate shutdown triggered."); + return Ok(()); + } + + executor.clone().spawn( + async move { + if let Err(e) = ProductionValidatorClient::new(context, config) + .and_then(|mut vc| async move { vc.start_service().await }) + .await + { + crit!(log, "Failed to start validator client"; "reason" => e); + // Ignore the error since it always occurs during normal operation when + // shutting down. + let _ = executor + .shutdown_sender() + .try_send(ShutdownReason::Failure("Failed to start validator client")); + } + }, + "validator_client", + ); + } + Err(_) => (), }; info!(log, "Lighthouse started"; "version" => VERSION); @@ -733,38 +770,9 @@ fn run( "beacon_node", ); } - Some(("validator_client", matches)) => { - let context = environment.core_context(); - let log = context.log().clone(); - let executor = context.executor.clone(); - let config = validator_client::Config::from_cli(matches, context.log()) - .map_err(|e| format!("Unable to initialize validator config: {}", e))?; - // Dump configs if `dump-config` or `dump-chain-config` flags are set - clap_utils::check_dump_configs::<_, E>(matches, &config, &context.eth2_config.spec)?; - - let shutdown_flag = matches.get_flag("immediate-shutdown"); - if shutdown_flag { - info!(log, "Validator client immediate shutdown triggered."); - return Ok(()); - } - - executor.clone().spawn( - async move { - if let Err(e) = ProductionValidatorClient::new(context, config) - .and_then(|mut vc| async move { vc.start_service().await }) - .await - { - crit!(log, "Failed to start validator client"; "reason" => e); - // Ignore the error since it always occurs during normal operation when - // shutting down. - let _ = executor - .shutdown_sender() - .try_send(ShutdownReason::Failure("Failed to start validator client")); - } - }, - "validator_client", - ); - } + // TODO(clap-derive) delete this once we've fully migrated to clap derive. + // Qt the moment this needs to exist so that we dont trigger a crit. + Some(("validator_client", _)) => (), _ => { crit!(log, "No subcommand supplied. See --help ."); return Err("No subcommand supplied.".into()); diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 1063a80ff4..03314930b9 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2504,9 +2504,9 @@ fn light_client_server_default() { CommandLineTest::new() .run_with_zero_port() .with_config(|config| { - assert!(!config.network.enable_light_client_server); - assert!(!config.chain.enable_light_client_server); - assert!(!config.http_api.enable_light_client_server); + assert!(config.network.enable_light_client_server); + assert!(config.chain.enable_light_client_server); + assert!(config.http_api.enable_light_client_server); }); } @@ -2522,13 +2522,26 @@ fn light_client_server_enabled() { } #[test] -fn light_client_http_server_enabled() { +fn light_client_server_disabled() { CommandLineTest::new() - .flag("http", None) - .flag("light-client-server", None) + .flag("disable-light-client-server", None) .run_with_zero_port() .with_config(|config| { - assert!(config.http_api.enable_light_client_server); + assert!(!config.network.enable_light_client_server); + assert!(!config.chain.enable_light_client_server); + }); +} + +#[test] +fn light_client_http_server_disabled() { + CommandLineTest::new() + .flag("http", None) + .flag("disable-light-client-server", None) + .run_with_zero_port() + .with_config(|config| { + assert!(!config.http_api.enable_light_client_server); + assert!(!config.network.enable_light_client_server); + assert!(!config.chain.enable_light_client_server); }); } diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 1945399c86..f28e7d9829 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -407,6 +407,13 @@ fn metrics_port_flag() { .with_config(|config| assert_eq!(config.http_metrics.listen_port, 9090)); } #[test] +fn metrics_port_flag_default() { + CommandLineTest::new() + .flag("metrics", None) + .run() + .with_config(|config| assert_eq!(config.http_metrics.listen_port, 5064)); +} +#[test] fn metrics_allow_origin_flag() { CommandLineTest::new() .flag("metrics", None) @@ -458,7 +465,7 @@ fn no_doppelganger_protection_flag() { fn no_gas_limit_flag() { CommandLineTest::new() .run() - .with_config(|config| assert!(config.validator_store.gas_limit.is_none())); + .with_config(|config| assert!(config.validator_store.gas_limit == Some(30_000_000))); } #[test] fn gas_limit_flag() { @@ -560,7 +567,7 @@ fn broadcast_flag() { }); // Other valid variants CommandLineTest::new() - .flag("broadcast", Some("blocks, subscriptions")) + .flag("broadcast", Some("blocks,subscriptions")) .run() .with_config(|config| { assert_eq!( @@ -605,7 +612,7 @@ fn beacon_nodes_sync_tolerances_flag() { } #[test] -#[should_panic(expected = "Unknown API topic")] +#[should_panic(expected = "invalid value")] fn wrong_broadcast_flag() { CommandLineTest::new() .flag("broadcast", Some("foo, subscriptions")) diff --git a/scripts/ci/check-lockbud.sh b/scripts/ci/check-lockbud.sh new file mode 100755 index 0000000000..8e1d33b53b --- /dev/null +++ b/scripts/ci/check-lockbud.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Run lockbud to check for deadlocks and capture the output +output=$(cargo lockbud -k deadlock -b -l tokio_util 2>&1) + +# Check if lockbud returned any issues +if echo "$output" | grep -q '"bug_kind"'; then + # Print the JSON payload + echo "Lockbud detected issues:" + echo "$output" + + # Exit with a non-zero status to indicate an error + exit 1 +else + echo "No issues detected by Lockbud." + exit 0 +fi \ No newline at end of file diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 7b507f8c50..c32a670e9a 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.5.0-beta.1 +TESTS_TAG := v1.5.0-beta.2 TESTS = general minimal mainnet TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 02a01555b4..8a662b72e3 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -49,9 +49,8 @@ excluded_paths = [ "bls12-381-tests/hash_to_G2", "tests/.*/eip6110", "tests/.*/whisk", - # TODO(electra): SingleAttestation tests are waiting on Eitan's PR - "tests/.*/electra/ssz_static/SingleAttestation", - "tests/.*/fulu/ssz_static/SingleAttestation", + # TODO(das): Fulu tests are ignored for now + "tests/.*/fulu", "tests/.*/fulu/ssz_static/MatrixEntry", ] diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index adb5bee768..7178edb151 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -25,9 +25,9 @@ use std::fmt::Debug; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconState, - BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, FullPayload, - ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SyncAggregate, - WithdrawalRequest, + BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, + ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, + SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] @@ -398,7 +398,7 @@ impl Operation for WithdrawalsPayload { fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file_with(path, |bytes| { - ExecutionPayload::from_ssz_bytes(bytes, fork_name) + ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) }) .map(|payload| WithdrawalsPayload { payload: payload.into(), diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index d1ddd6a48f..481c9b2169 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -680,6 +680,11 @@ impl Handler for ForkChoiceHandler { return false; } + // Deposit tests exist only after Electra. + if self.handler_name == "deposit_with_reorg" && !fork_name.electra_enabled() { + return false; + } + // These tests check block validity (which may include signatures) and there is no need to // run them with fake crypto. cfg!(not(feature = "fake_crypto")) diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 285ac951a6..dfee385958 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -155,6 +155,7 @@ type_name!(SignedBeaconBlockHeader); type_name_generic!(SignedContributionAndProof); type_name!(SignedVoluntaryExit); type_name!(SigningData); +type_name!(SingleAttestation); type_name_generic!(SyncCommitteeContribution); type_name!(SyncCommitteeMessage); type_name!(SyncAggregatorSelectionData); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index bba7efde49..1f5a7dd997 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -282,6 +282,12 @@ mod ssz_static { .run(); } + #[test] + fn single_attestation() { + SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); + } + #[test] fn attester_slashing() { SszStaticHandler::, MinimalEthSpec>::pre_electra() @@ -880,6 +886,12 @@ fn fork_choice_get_proposer_head() { ForkChoiceHandler::::new("get_proposer_head").run(); } +#[test] +fn fork_choice_deposit_with_reorg() { + ForkChoiceHandler::::new("deposit_with_reorg").run(); + // There is no mainnet variant for this test. +} + #[test] fn optimistic_sync() { OptimisticSyncHandler::::default().run(); diff --git a/testing/execution_engine_integration/src/geth.rs b/testing/execution_engine_integration/src/geth.rs index 0bd96a5c93..ea143ed433 100644 --- a/testing/execution_engine_integration/src/geth.rs +++ b/testing/execution_engine_integration/src/geth.rs @@ -7,7 +7,10 @@ use std::{env, fs}; use tempfile::TempDir; use unused_port::unused_tcp4_port; -const GETH_BRANCH: &str = "master"; +// This is not currently used due to the following breaking changes in geth that requires updating our tests: +// 1. removal of `personal` namespace in v1.14.12: See #30704 +// 2. removal of `totalDifficulty` field from RPC in v1.14.11. See #30386. +// const GETH_BRANCH: &str = "master"; const GETH_REPO_URL: &str = "https://github.com/ethereum/go-ethereum"; pub fn build_result(repo_dir: &Path) -> Output { @@ -27,12 +30,14 @@ pub fn build(execution_clients_dir: &Path) { } // Get the latest tag on the branch - let last_release = build_utils::get_latest_release(&repo_dir, GETH_BRANCH).unwrap(); - build_utils::checkout(&repo_dir, dbg!(&last_release)).unwrap(); + // let last_release = build_utils::get_latest_release(&repo_dir, GETH_BRANCH).unwrap(); + // Using an older release due to breaking changes in recent releases. See comment on `GETH_BRANCH` const. + let release_tag = "v1.14.10"; + build_utils::checkout(&repo_dir, dbg!(release_tag)).unwrap(); // Build geth build_utils::check_command_output(build_result(&repo_dir), || { - format!("geth make failed using release {last_release}") + format!("geth make failed using release {release_tag}") }); } diff --git a/testing/validator_test_rig/Cargo.toml b/testing/validator_test_rig/Cargo.toml new file mode 100644 index 0000000000..76560b8afc --- /dev/null +++ b/testing/validator_test_rig/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "validator_test_rig" +version = "0.1.0" +edition = { workspace = true } + +[dependencies] +eth2 = { workspace = true } +logging = { workspace = true } +mockito = { workspace = true } +regex = { workspace = true } +sensitive_url = { workspace = true } +serde_json = { workspace = true } +slog = { workspace = true } +types = { workspace = true } diff --git a/testing/validator_test_rig/src/lib.rs b/testing/validator_test_rig/src/lib.rs new file mode 100644 index 0000000000..a0a979dfc8 --- /dev/null +++ b/testing/validator_test_rig/src/lib.rs @@ -0,0 +1 @@ +pub mod mock_beacon_node; diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs new file mode 100644 index 0000000000..f875116155 --- /dev/null +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -0,0 +1,132 @@ +use eth2::types::{GenericResponse, SyncingData}; +use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts}; +use logging::test_logger; +use mockito::{Matcher, Mock, Server, ServerGuard}; +use regex::Regex; +use sensitive_url::SensitiveUrl; +use slog::{info, Logger}; +use std::marker::PhantomData; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use types::{ChainSpec, ConfigAndPreset, EthSpec, SignedBlindedBeaconBlock}; + +pub struct MockBeaconNode { + server: ServerGuard, + pub beacon_api_client: BeaconNodeHttpClient, + log: Logger, + _phantom: PhantomData, + pub received_blocks: Arc>>>, +} + +impl MockBeaconNode { + pub async fn new() -> Self { + // mock server logging + let server = Server::new_async().await; + let beacon_api_client = BeaconNodeHttpClient::new( + SensitiveUrl::from_str(&server.url()).unwrap(), + Timeouts::set_all(Duration::from_secs(1)), + ); + let log = test_logger(); + Self { + server, + beacon_api_client, + log, + _phantom: PhantomData, + received_blocks: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Resets all mocks + #[allow(dead_code)] + pub fn reset_mocks(&mut self) { + self.server.reset(); + } + + pub fn mock_config_spec(&mut self, spec: &ChainSpec) { + let path_pattern = Regex::new(r"^/eth/v1/config/spec$").unwrap(); + let config_and_preset = ConfigAndPreset::from_chain_spec::(spec, None); + let data = GenericResponse::from(config_and_preset); + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .with_status(200) + .with_body(serde_json::to_string(&data).unwrap()) + .create(); + } + + pub fn mock_get_node_syncing(&mut self, response: SyncingData) { + let path_pattern = Regex::new(r"^/eth/v1/node/syncing$").unwrap(); + + let data = GenericResponse::from(response); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .with_status(200) + .with_body(serde_json::to_string(&data).unwrap()) + .create(); + } + + /// Mocks the `post_beacon_blinded_blocks_v2_ssz` response with an optional `delay`. + pub fn mock_post_beacon_blinded_blocks_v2_ssz(&mut self, delay: Duration) -> Mock { + let path_pattern = Regex::new(r"^/eth/v2/beacon/blinded_blocks$").unwrap(); + let log = self.log.clone(); + let url = self.server.url(); + + let received_blocks = Arc::clone(&self.received_blocks); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .with_status(200) + .with_body_from_request(move |request| { + info!( + log, + "{}", + format!( + "Received published block request on server {} with delay {} s", + url, + delay.as_secs(), + ) + ); + + let body = request.body().expect("Failed to get request body"); + let block: SignedBlindedBeaconBlock = + SignedBlindedBeaconBlock::any_from_ssz_bytes(body) + .expect("Failed to deserialize body as SignedBlindedBeaconBlock"); + + received_blocks.lock().unwrap().push(block); + + std::thread::sleep(delay); + vec![] + }) + .create() + } + + pub fn mock_offline_node(&mut self) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/node/version$").unwrap(); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .with_status(StatusCode::INTERNAL_SERVER_ERROR.as_u16() as usize) + .with_header("content-type", "application/json") + .with_body(r#"{"message":"Internal Server Error"}"#) + .create() + } + + pub fn mock_online_node(&mut self) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/node/version$").unwrap(); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + r#"{ + "data": { + "version": "lighthouse-mock" + } + }"#, + ) + .create() + } +} diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 504d96ae1c..fb6007b00a 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -8,9 +8,6 @@ edition = { workspace = true } name = "validator_client" path = "src/lib.rs" -[dev-dependencies] -tokio = { workspace = true } - [dependencies] account_utils = { workspace = true } beacon_node_fallback = { workspace = true } diff --git a/validator_client/beacon_node_fallback/Cargo.toml b/validator_client/beacon_node_fallback/Cargo.toml index c15ded43d7..598020d137 100644 --- a/validator_client/beacon_node_fallback/Cargo.toml +++ b/validator_client/beacon_node_fallback/Cargo.toml @@ -9,6 +9,7 @@ name = "beacon_node_fallback" path = "src/lib.rs" [dependencies] +clap = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } futures = { workspace = true } @@ -20,3 +21,7 @@ strum = { workspace = true } tokio = { workspace = true } types = { workspace = true } validator_metrics = { workspace = true } + +[dev-dependencies] +logging = { workspace = true } +validator_test_rig = { workspace = true } diff --git a/validator_client/beacon_node_fallback/src/beacon_node_health.rs b/validator_client/beacon_node_fallback/src/beacon_node_health.rs index e5b0487656..80d3fb7efd 100644 --- a/validator_client/beacon_node_fallback/src/beacon_node_health.rs +++ b/validator_client/beacon_node_fallback/src/beacon_node_health.rs @@ -1,11 +1,9 @@ use super::CandidateError; use eth2::BeaconNodeHttpClient; -use itertools::Itertools; use serde::{Deserialize, Serialize}; use slog::{warn, Logger}; use std::cmp::Ordering; use std::fmt::{Debug, Display, Formatter}; -use std::str::FromStr; use types::Slot; /// Sync distances between 0 and DEFAULT_SYNC_TOLERANCE are considered `synced`. @@ -53,29 +51,6 @@ impl Default for BeaconNodeSyncDistanceTiers { } } -impl FromStr for BeaconNodeSyncDistanceTiers { - type Err = String; - - fn from_str(s: &str) -> Result { - let values: (u64, u64, u64) = s - .split(',') - .map(|s| { - s.parse() - .map_err(|e| format!("Invalid sync distance modifier: {e:?}")) - }) - .collect::, _>>()? - .into_iter() - .collect_tuple() - .ok_or("Invalid number of sync distance modifiers".to_string())?; - - Ok(BeaconNodeSyncDistanceTiers { - synced: Slot::new(values.0), - small: Slot::new(values.0 + values.1), - medium: Slot::new(values.0 + values.1 + values.2), - }) - } -} - impl BeaconNodeSyncDistanceTiers { /// Takes a given sync distance and determines its tier based on the `sync_tolerance` defined by /// the CLI. @@ -90,6 +65,17 @@ impl BeaconNodeSyncDistanceTiers { SyncDistanceTier::Large } } + + pub fn from_vec(tiers: &[u64]) -> Result { + if tiers.len() != 3 { + return Err("Invalid number of sync distance modifiers".to_string()); + } + Ok(BeaconNodeSyncDistanceTiers { + synced: Slot::new(tiers[0]), + small: Slot::new(tiers[0] + tiers[1]), + medium: Slot::new(tiers[0] + tiers[1] + tiers[2]), + }) + } } /// Execution Node health metrics. @@ -320,7 +306,6 @@ mod tests { SyncDistanceTier, }; use crate::Config; - use std::str::FromStr; use types::Slot; #[test] @@ -423,7 +408,7 @@ mod tests { // medium 9..=12 // large: 13.. - let distance_tiers = BeaconNodeSyncDistanceTiers::from_str("4,4,4").unwrap(); + let distance_tiers = BeaconNodeSyncDistanceTiers::from_vec(&[4, 4, 4]).unwrap(); let synced_low = new_distance_tier(0, &distance_tiers); let synced_high = new_distance_tier(4, &distance_tiers); diff --git a/validator_client/beacon_node_fallback/src/lib.rs b/validator_client/beacon_node_fallback/src/lib.rs index 526479d67f..2659771b77 100644 --- a/validator_client/beacon_node_fallback/src/lib.rs +++ b/validator_client/beacon_node_fallback/src/lib.rs @@ -7,6 +7,7 @@ use beacon_node_health::{ check_node_health, BeaconNodeHealth, BeaconNodeSyncDistanceTiers, ExecutionEngineHealth, IsOptimistic, SyncDistanceTier, }; +use clap::ValueEnum; use environment::RuntimeContext; use eth2::BeaconNodeHttpClient; use futures::future; @@ -20,7 +21,8 @@ use std::future::Future; use std::marker::PhantomData; use std::sync::Arc; use std::time::{Duration, Instant}; -use strum::{EnumString, EnumVariantNames}; +use std::vec::Vec; +use strum::EnumVariantNames; use tokio::{sync::RwLock, time::sleep}; use types::{ChainSpec, Config as ConfigSpec, EthSpec, Slot}; use validator_metrics::{inc_counter_vec, ENDPOINT_ERRORS, ENDPOINT_REQUESTS}; @@ -727,9 +729,10 @@ async fn sort_nodes_by_health(nodes: &mut Vec } /// Serves as a cue for `BeaconNodeFallback` to tell which requests need to be broadcasted. -#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, EnumString, EnumVariantNames)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, EnumVariantNames, ValueEnum)] #[strum(serialize_all = "kebab-case")] pub enum ApiTopic { + None, Attestations, Blocks, InclusionLists, @@ -756,25 +759,31 @@ mod tests { use crate::beacon_node_health::BeaconNodeHealthTier; use eth2::SensitiveUrl; use eth2::Timeouts; - use std::str::FromStr; + use logging::test_logger; + use slot_clock::TestingSlotClock; use strum::VariantNames; - use types::{MainnetEthSpec, Slot}; + use types::{BeaconBlockDeneb, MainnetEthSpec, Slot}; + use types::{EmptyBlock, Signature, SignedBeaconBlockDeneb, SignedBlindedBeaconBlock}; + use validator_test_rig::mock_beacon_node::MockBeaconNode; type E = MainnetEthSpec; #[test] fn api_topic_all() { let all = ApiTopic::all(); - assert_eq!(all.len(), ApiTopic::VARIANTS.len()); - assert!(ApiTopic::VARIANTS + // ignore NONE variant + let mut variants = ApiTopic::VARIANTS.to_vec(); + variants.retain(|s| *s != "none"); + assert_eq!(all.len(), variants.len()); + assert!(variants .iter() - .map(|topic| ApiTopic::from_str(topic).unwrap()) + .map(|topic| ApiTopic::from_str(topic, true).unwrap()) .eq(all.into_iter())); } #[tokio::test] async fn check_candidate_order() { - // These fields is irrelvant for sorting. They are set to arbitrary values. + // These fields are irrelevant for sorting. They are set to arbitrary values. let head = Slot::new(99); let optimistic_status = IsOptimistic::No; let execution_status = ExecutionEngineHealth::Healthy; @@ -882,4 +891,172 @@ mod tests { assert_eq!(candidates, expected_candidates); } + + async fn new_mock_beacon_node( + index: usize, + spec: &ChainSpec, + ) -> (MockBeaconNode, CandidateBeaconNode) { + let mut mock_beacon_node = MockBeaconNode::::new().await; + mock_beacon_node.mock_config_spec(spec); + + let beacon_node = + CandidateBeaconNode::::new(mock_beacon_node.beacon_api_client.clone(), index); + + (mock_beacon_node, beacon_node) + } + + fn create_beacon_node_fallback( + candidates: Vec>, + topics: Vec, + spec: Arc, + log: Logger, + ) -> BeaconNodeFallback { + let mut beacon_node_fallback = + BeaconNodeFallback::new(candidates, Config::default(), topics, spec, log); + + beacon_node_fallback.set_slot_clock(TestingSlotClock::new( + Slot::new(1), + Duration::from_secs(0), + Duration::from_secs(12), + )); + + beacon_node_fallback + } + + #[tokio::test] + async fn update_all_candidates_should_update_sync_status() { + let spec = Arc::new(MainnetEthSpec::default_spec()); + let (mut mock_beacon_node_1, beacon_node_1) = new_mock_beacon_node(0, &spec).await; + let (mut mock_beacon_node_2, beacon_node_2) = new_mock_beacon_node(1, &spec).await; + let (mut mock_beacon_node_3, beacon_node_3) = new_mock_beacon_node(2, &spec).await; + + let beacon_node_fallback = create_beacon_node_fallback( + // Put this out of order to be sorted later + vec![ + beacon_node_2.clone(), + beacon_node_3.clone(), + beacon_node_1.clone(), + ], + vec![], + spec.clone(), + test_logger(), + ); + + // BeaconNodeHealthTier 1 + mock_beacon_node_1.mock_get_node_syncing(eth2::types::SyncingData { + is_syncing: false, + is_optimistic: false, + el_offline: false, + head_slot: Slot::new(1), + sync_distance: Slot::new(0), + }); + // BeaconNodeHealthTier 3 + mock_beacon_node_2.mock_get_node_syncing(eth2::types::SyncingData { + is_syncing: false, + is_optimistic: false, + el_offline: true, + head_slot: Slot::new(1), + sync_distance: Slot::new(0), + }); + // BeaconNodeHealthTier 5 + mock_beacon_node_3.mock_get_node_syncing(eth2::types::SyncingData { + is_syncing: false, + is_optimistic: true, + el_offline: false, + head_slot: Slot::new(1), + sync_distance: Slot::new(0), + }); + + beacon_node_fallback.update_all_candidates().await; + + let candidates = beacon_node_fallback.candidates.read().await; + assert_eq!( + vec![beacon_node_1, beacon_node_2, beacon_node_3], + *candidates + ); + } + + #[tokio::test] + async fn broadcast_should_send_to_all_bns() { + let spec = Arc::new(MainnetEthSpec::default_spec()); + let (mut mock_beacon_node_1, beacon_node_1) = new_mock_beacon_node(0, &spec).await; + let (mut mock_beacon_node_2, beacon_node_2) = new_mock_beacon_node(1, &spec).await; + + let beacon_node_fallback = create_beacon_node_fallback( + vec![beacon_node_1, beacon_node_2], + vec![ApiTopic::Blocks], + spec.clone(), + test_logger(), + ); + + mock_beacon_node_1.mock_post_beacon_blinded_blocks_v2_ssz(Duration::from_secs(0)); + mock_beacon_node_2.mock_post_beacon_blinded_blocks_v2_ssz(Duration::from_secs(0)); + + let signed_block = SignedBlindedBeaconBlock::::Deneb(SignedBeaconBlockDeneb { + message: BeaconBlockDeneb::empty(&spec), + signature: Signature::empty(), + }); + + // trigger broadcast to `post_beacon_blinded_blocks_v2` + let result = beacon_node_fallback + .broadcast(|client| { + let signed_block_cloned = signed_block.clone(); + async move { + client + .post_beacon_blinded_blocks_v2_ssz(&signed_block_cloned, None) + .await + } + }) + .await; + + assert!(result.is_ok()); + + let received_blocks_from_bn_1 = mock_beacon_node_1.received_blocks.lock().unwrap(); + let received_blocks_from_bn_2 = mock_beacon_node_2.received_blocks.lock().unwrap(); + assert_eq!(received_blocks_from_bn_1.len(), 1); + assert_eq!(received_blocks_from_bn_2.len(), 1); + } + + #[tokio::test] + async fn first_success_should_try_nodes_in_order() { + let spec = Arc::new(MainnetEthSpec::default_spec()); + let (mut mock_beacon_node_1, beacon_node_1) = new_mock_beacon_node(0, &spec).await; + let (mut mock_beacon_node_2, beacon_node_2) = new_mock_beacon_node(1, &spec).await; + let (mut mock_beacon_node_3, beacon_node_3) = new_mock_beacon_node(2, &spec).await; + + let beacon_node_fallback = create_beacon_node_fallback( + vec![beacon_node_1, beacon_node_2, beacon_node_3], + vec![], + spec.clone(), + test_logger(), + ); + + let mock1 = mock_beacon_node_1.mock_offline_node(); + let mock2 = mock_beacon_node_2.mock_offline_node(); + let mock3 = mock_beacon_node_3.mock_online_node(); + + let result_success = beacon_node_fallback + .first_success(|client| async move { client.get_node_version().await }) + .await; + + // mock3 expects to be called once since it is online in the first pass + mock3.expect(1).assert(); + assert!(result_success.is_ok()); + + // make all beacon node offline and the result should error + let _mock3 = mock_beacon_node_3.mock_offline_node(); + + let result_failure = beacon_node_fallback + .first_success(|client| async move { client.get_node_version().await }) + .await; + + assert!(result_failure.is_err()); + + // Both mock1 and mock2 should be called 3 times: + // - the first time is for the result_success case, + // - the second time is when it calls all 3 mock beacon nodes and all fails in the first pass, + // - which gives the third call because the function gives a second pass if no candidates succeeded in the first pass + mock1.expect(3).assert(); + mock2.expect(3).assert(); + } } diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index b2d1ebb3c2..dfcd2064e5 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -1,490 +1,478 @@ -use clap::{builder::ArgPredicate, Arg, ArgAction, Command}; -use clap_utils::{get_color_style, FLAG_HEADER}; +use beacon_node_fallback::ApiTopic; +use clap::builder::ArgPredicate; +pub use clap::{FromArgMatches, Parser}; +use clap_utils::get_color_style; +use clap_utils::FLAG_HEADER; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use types::Address; -pub fn cli_app() -> Command { - Command::new("validator_client") - .visible_aliases(["v", "vc", "validator"]) - .styles(get_color_style()) - .display_order(0) - .about( - "When connected to a beacon node, performs the duties of a staked \ +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap( + name = "validator_client", + visible_aliases = &["v", "vc", "validator"], + about = "When connected to a beacon node, performs the duties of a staked \ validator (e.g., proposing blocks and attestations).", - ) - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER) - ) - .arg( - Arg::new("beacon-nodes") - .long("beacon-nodes") - .value_name("NETWORK_ADDRESSES") - .help("Comma-separated addresses to one or more beacon node HTTP APIs. \ - Default is http://localhost:5052." - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("proposer-nodes") - .long("proposer-nodes") - .value_name("NETWORK_ADDRESSES") - .help("Comma-separated addresses to one or more beacon node HTTP APIs. \ - These specify nodes that are used to send beacon block proposals. A failure will revert back to the standard beacon nodes specified in --beacon-nodes." - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("broadcast") - .long("broadcast") - .value_name("API_TOPICS") - .help("Comma-separated list of beacon API topics to broadcast to all beacon nodes. \ - Possible values are: none, attestations, blocks, subscriptions, \ - sync-committee. Default (when flag is omitted) is to broadcast \ - subscriptions only." - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("validators-dir") - .long("validators-dir") - .alias("validator-dir") - .value_name("VALIDATORS_DIR") - .help( - "The directory which contains the validator keystores, deposit data for \ - each validator along with the common slashing protection database \ - and the validator_definitions.yml" - ) - .action(ArgAction::Set) - .conflicts_with("datadir") - .display_order(0) - ) - .arg( - Arg::new("secrets-dir") - .long("secrets-dir") - .value_name("SECRETS_DIRECTORY") - .help( - "The directory which contains the password to unlock the validator \ - voting keypairs. Each password should be contained in a file where the \ - name is the 0x-prefixed hex representation of the validators voting public \ - key. Defaults to ~/.lighthouse/{network}/secrets.", - ) - .action(ArgAction::Set) - .conflicts_with("datadir") - .display_order(0) - ) - .arg( - Arg::new("init-slashing-protection") - .long("init-slashing-protection") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .help( - "If present, do not require the slashing protection database to exist before \ - running. You SHOULD NOT use this flag unless you're certain that a new \ - slashing protection database is required. Usually, your database \ - will have been initialized when you imported your validator keys. If you \ - misplace your database and then run with this flag you risk being slashed." - ) - .display_order(0) - ) - .arg( - Arg::new("disable-auto-discover") - .long("disable-auto-discover") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .help( - "If present, do not attempt to discover new validators in the validators-dir. Validators \ - will need to be manually added to the validator_definitions.yml file." - ) - .display_order(0) - ) - .arg( - Arg::new("use-long-timeouts") - .long("use-long-timeouts") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .help("If present, the validator client will use longer timeouts for requests \ - made to the beacon node. This flag is generally not recommended, \ - longer timeouts can cause missed duties when fallbacks are used.") - .display_order(0) - ) - .arg( - Arg::new("beacon-nodes-tls-certs") - .long("beacon-nodes-tls-certs") - .value_name("CERTIFICATE-FILES") - .action(ArgAction::Set) - .help("Comma-separated paths to custom TLS certificates to use when connecting \ - to a beacon node (and/or proposer node). These certificates must be in PEM format and are used \ - in addition to the OS trust store. Commas must only be used as a \ - delimiter, and must not be part of the certificate path.") - .display_order(0) - ) - // This overwrites the graffiti configured in the beacon node. - .arg( - Arg::new("graffiti") - .long("graffiti") - .help("Specify your custom graffiti to be included in blocks.") - .value_name("GRAFFITI") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("graffiti-file") - .long("graffiti-file") - .help("Specify a graffiti file to load validator graffitis from.") - .value_name("GRAFFITI-FILE") - .action(ArgAction::Set) - .conflicts_with("graffiti") - .display_order(0) - ) - .arg( - Arg::new("suggested-fee-recipient") - .long("suggested-fee-recipient") - .help("Once the merge has happened, this address will receive transaction fees \ - from blocks proposed by this validator client. If a fee recipient is \ - configured in the validator definitions it takes priority over this value.") - .value_name("FEE-RECIPIENT") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("distributed") - .long("distributed") - .help("Enables functionality required for running the validator in a distributed validator cluster.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - /* REST API related arguments */ - .arg( - Arg::new("http") - .long("http") - .help("Enable the RESTful HTTP API server. Disabled by default.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - /* - * Note: The HTTP server is **not** encrypted (i.e., not HTTPS) and therefore it is - * unsafe to publish on a public network. - * - * If the `--http-address` flag is used, the `--unencrypted-http-transport` flag - * must also be used in order to make it clear to the user that this is unsafe. - */ - .arg( - Arg::new("http-address") - .long("http-address") - .requires("http") - .value_name("ADDRESS") - .help("Set the address for the HTTP address. The HTTP server is not encrypted \ - and therefore it is unsafe to publish on a public network. When this \ - flag is used, it additionally requires the explicit use of the \ - `--unencrypted-http-transport` flag to ensure the user is aware of the \ - risks involved. For access via the Internet, users should apply \ - transport-layer security like a HTTPS reverse-proxy or SSH tunnelling.") - .requires("unencrypted-http-transport") - .display_order(0) - ) - .arg( - Arg::new("unencrypted-http-transport") - .long("unencrypted-http-transport") - .help("This is a safety flag to ensure that the user is aware that the http \ - transport is unencrypted and using a custom HTTP address is unsafe.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .requires("http-address") - .display_order(0) - ) - .arg( - Arg::new("http-port") - .long("http-port") - .requires("http") - .value_name("PORT") - .help("Set the listen TCP port for the RESTful HTTP API server.") - .default_value_if("http", ArgPredicate::IsPresent, "5062") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("http-allow-origin") - .long("http-allow-origin") - .requires("http") - .value_name("ORIGIN") - .help("Set the value of the Access-Control-Allow-Origin response HTTP header. \ + styles = get_color_style(), + next_line_help = true, + term_width = 80, + disable_help_flag = true, + disable_help_subcommand = true, + display_order = 0, +)] +pub struct ValidatorClient { + #[clap( + long, + value_name = "NETWORK_ADDRESSES", + value_delimiter = ',', + help = "Comma-separated addresses to one or more beacon node HTTP APIs. \ + Default is http://localhost:5052.", + display_order = 0 + )] + pub beacon_nodes: Option>, + + #[clap( + long, + value_name = "NETWORK_ADDRESSES", + value_delimiter = ',', + help = "Comma-separated addresses to one or more beacon node HTTP APIs. \ + These specify nodes that are used to send beacon block proposals. \ + A failure will revert back to the standard beacon nodes specified in --beacon-nodes.", + display_order = 0 + )] + pub proposer_nodes: Option>, + + #[clap( + long, + value_name = "API_TOPICS", + value_delimiter = ',', + help = "Comma-separated list of beacon API topics to broadcast to all beacon nodes. \ + Default (when flag is omitted) is to broadcast subscriptions only.", + display_order = 0 + )] + pub broadcast: Option>, + + #[clap( + long, + alias = "validator-dir", + value_name = "VALIDATORS_DIR", + conflicts_with = "datadir", + help = "The directory which contains the validator keystores, deposit data for \ + each validator along with the common slashing protection database \ + and the validator_definitions.yml", + display_order = 0 + )] + pub validators_dir: Option, + + #[clap( + long, + value_name = "SECRETS_DIRECTORY", + conflicts_with = "datadir", + help = "The directory which contains the password to unlock the validator \ + voting keypairs. Each password should be contained in a file where the \ + name is the 0x-prefixed hex representation of the validators voting public \ + key. Defaults to ~/.lighthouse/{network}/secrets.", + display_order = 0 + )] + pub secrets_dir: Option, + + #[clap( + long, + help = "If present, do not require the slashing protection database to exist before \ + running. You SHOULD NOT use this flag unless you're certain that a new \ + slashing protection database is required. Usually, your database \ + will have been initialized when you imported your validator keys. If you \ + misplace your database and then run with this flag you risk being slashed.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub init_slashing_protection: bool, + + #[clap( + long, + help = "If present, do not attempt to discover new validators in the validators-dir. Validators \ + will need to be manually added to the validator_definitions.yml file.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub disable_auto_discover: bool, + + #[clap( + long, + help = "If present, the validator client will use longer timeouts for requests \ + made to the beacon node. This flag is generally not recommended, \ + longer timeouts can cause missed duties when fallbacks are used.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub use_long_timeouts: bool, + + #[clap( + long, + value_name = "CERTIFICATE-FILES", + value_delimiter = ',', + help = "Comma-separated paths to custom TLS certificates to use when connecting \ + to a beacon node (and/or proposer node). These certificates must be in PEM format and are used \ + in addition to the OS trust store. Commas must only be used as a \ + delimiter, and must not be part of the certificate path.", + display_order = 0 + )] + pub beacon_nodes_tls_certs: Option>, + + // This overwrites the graffiti configured in the beacon node. + #[clap( + long, + value_name = "GRAFFITI", + help = "Specify your custom graffiti to be included in blocks.", + display_order = 0 + )] + pub graffiti: Option, + + #[clap( + long, + value_name = "GRAFFITI-FILE", + conflicts_with = "graffiti", + help = "Specify a graffiti file to load validator graffitis from.", + display_order = 0 + )] + pub graffiti_file: Option, + + #[clap( + long, + value_name = "FEE-RECIPIENT", + help = "Once the merge has happened, this address will receive transaction fees \ + from blocks proposed by this validator client. If a fee recipient is \ + configured in the validator definitions it takes priority over this value.", + display_order = 0 + )] + pub suggested_fee_recipient: Option
, + + #[clap( + long, + help = "Enables functionality required for running the validator in a distributed validator cluster.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub distributed: bool, + + /* REST API related arguments */ + #[clap( + long, + help = "Enable the RESTful HTTP API server. Disabled by default.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub http: bool, + + /* + * Note: The HTTP server is **not** encrypted (i.e., not HTTPS) and therefore it is + * unsafe to publish on a public network. + * + * If the `--http-address` flag is used, the `--unencrypted-http-transport` flag + * must also be used in order to make it clear to the user that this is unsafe. + */ + #[clap( + long, + value_name = "ADDRESS", + requires = "unencrypted_http_transport", + help = "Set the address for the HTTP address. The HTTP server is not encrypted \ + and therefore it is unsafe to publish on a public network. When this \ + flag is used, it additionally requires the explicit use of the \ + `--unencrypted-http-transport` flag to ensure the user is aware of the \ + risks involved. For access via the Internet, users should apply \ + transport-layer security like a HTTPS reverse-proxy or SSH tunnelling.", + display_order = 0 + )] + pub http_address: Option, + + #[clap( + long, + requires = "http_address", + help = "This is a safety flag to ensure that the user is aware that the http \ + transport is unencrypted and using a custom HTTP address is unsafe.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub unencrypted_http_transport: bool, + + #[clap( + long, + value_name = "PORT", + default_value_t = 5062, + help = "Set the listen TCP port for the RESTful HTTP API server.", + display_order = 0 + )] + pub http_port: u16, + + #[clap( + long, + value_name = "ORIGIN", + help = "Set the value of the Access-Control-Allow-Origin response HTTP header. \ Use * to allow any origin (not recommended in production). \ If no value is supplied, the CORS allowed origin is set to the listen \ - address of this server (e.g., http://localhost:5062).") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("http-allow-keystore-export") - .long("http-allow-keystore-export") - .requires("http") - .help("If present, allow access to the DELETE /lighthouse/keystores HTTP \ - API method, which allows exporting keystores and passwords to HTTP API \ - consumers who have access to the API token. This method is useful for \ - exporting validators, however it should be used with caution since it \ - exposes private key data to authorized users.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("http-store-passwords-in-secrets-dir") - .long("http-store-passwords-in-secrets-dir") - .requires("http") - .help("If present, any validators created via the HTTP will have keystore \ - passwords stored in the secrets-dir rather than the validator \ - definitions file.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("http-token-path") - .long("http-token-path") - .requires("http") - .value_name("HTTP_TOKEN_PATH") - .help( - "Path to file containing the HTTP API token for validator client authentication. \ - If not specified, defaults to {validators-dir}/api-token.txt." - ) - .action(ArgAction::Set) - .display_order(0) - ) - /* Prometheus metrics HTTP server related arguments */ - .arg( - Arg::new("metrics") - .long("metrics") - .help("Enable the Prometheus metrics HTTP server. Disabled by default.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("metrics-address") - .long("metrics-address") - .requires("metrics") - .value_name("ADDRESS") - .help("Set the listen address for the Prometheus metrics HTTP server.") - .default_value_if("metrics", ArgPredicate::IsPresent, "127.0.0.1") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("metrics-port") - .long("metrics-port") - .requires("metrics") - .value_name("PORT") - .help("Set the listen TCP port for the Prometheus metrics HTTP server.") - .default_value_if("metrics", ArgPredicate::IsPresent, "5064") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("metrics-allow-origin") - .long("metrics-allow-origin") - .requires("metrics") - .value_name("ORIGIN") - .help("Set the value of the Access-Control-Allow-Origin response HTTP header. \ - Use * to allow any origin (not recommended in production). \ - If no value is supplied, the CORS allowed origin is set to the listen \ - address of this server (e.g., http://localhost:5064).") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("enable-high-validator-count-metrics") - .long("enable-high-validator-count-metrics") - .help("Enable per validator metrics for > 64 validators. \ - Note: This flag is automatically enabled for <= 64 validators. \ - Enabling this metric for higher validator counts will lead to higher volume \ - of prometheus metrics being collected.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - /* - * Explorer metrics - */ - .arg( - Arg::new("monitoring-endpoint") - .long("monitoring-endpoint") - .value_name("ADDRESS") - .help("Enables the monitoring service for sending system metrics to a remote endpoint. \ + address of this server (e.g., http://localhost:5062).", + display_order = 0 + )] + pub http_allow_origin: Option, + + #[clap( + long, + requires = "http", + help = "If present, allow access to the DELETE /lighthouse/keystores HTTP \ + API method, which allows exporting keystores and passwords to HTTP API \ + consumers who have access to the API token. This method is useful for \ + exporting validators, however it should be used with caution since it \ + exposes private key data to authorized users.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub http_allow_keystore_export: bool, + + #[clap( + long, + requires = "http", + help = "If present, any validators created via the HTTP will have keystore \ + passwords stored in the secrets-dir rather than the validator \ + definitions file.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub http_store_passwords_in_secrets_dir: bool, + + #[clap( + long, + requires = "http", + help = "Path to file containing the HTTP API token for validator client authentication. \ + If not specified, defaults to {validators-dir}/api-token.txt.", + display_order = 0 + )] + pub http_token_path: Option, + + /* Prometheus metrics HTTP server related arguments */ + #[clap( + long, + help = "Enable the Prometheus metrics HTTP server. Disabled by default.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub metrics: bool, + + #[clap( + long, + value_name = "ADDRESS", + requires = "metrics", + default_value_if("metrics", ArgPredicate::IsPresent, "127.0.0.1"), + help = "Set the listen address for the Prometheus metrics HTTP server. [default: 127.0.0.1]", + display_order = 0 + )] + pub metrics_address: Option, + + #[clap( + long, + value_name = "PORT", + requires = "metrics", + default_value_t = 5064, + help = "Set the listen TCP port for the Prometheus metrics HTTP server.", + display_order = 0 + )] + pub metrics_port: u16, + + #[clap( + long, + value_name = "ORIGIN", + requires = "metrics", + help = "Set the value of the Access-Control-Allow-Origin response HTTP header. \ + Use * to allow any origin (not recommended in production). \ + If no value is supplied, the CORS allowed origin is set to the listen \ + address of this server (e.g., http://localhost:5064).", + display_order = 0 + )] + pub metrics_allow_origin: Option, + + #[clap( + long, + help = "Enable per validator metrics for > 64 validators. \ + Note: This flag is automatically enabled for <= 64 validators. \ + Enabling this metric for higher validator counts will lead to higher volume \ + of prometheus metrics being collected.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub enable_high_validator_count_metrics: bool, + + /* Explorer metrics */ + #[clap( + long, + value_name = "ADDRESS", + help = "Enables the monitoring service for sending system metrics to a remote endpoint. \ This can be used to monitor your setup on certain services (e.g. beaconcha.in). \ This flag sets the endpoint where the beacon node metrics will be sent. \ Note: This will send information to a remote sever which may identify and associate your \ validators, IP address and other personal information. Always use a HTTPS connection \ - and never provide an untrusted URL.") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("monitoring-endpoint-period") - .long("monitoring-endpoint-period") - .value_name("SECONDS") - .help("Defines how many seconds to wait between each message sent to \ - the monitoring-endpoint. Default: 60s") - .requires("monitoring-endpoint") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("enable-doppelganger-protection") - .long("enable-doppelganger-protection") - .value_name("ENABLE_DOPPELGANGER_PROTECTION") - .help("If this flag is set, Lighthouse will delay startup for three epochs and \ - monitor for messages on the network by any of the validators managed by this \ - client. This will result in three (possibly four) epochs worth of missed \ - attestations. If an attestation is detected during this period, it means it is \ - very likely that you are running a second validator client with the same keys. \ - This validator client will immediately shutdown if this is detected in order \ - to avoid potentially committing a slashable offense. Use this flag in order to \ - ENABLE this functionality, without this flag Lighthouse will begin attesting \ - immediately.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("builder-proposals") - .long("builder-proposals") - .alias("private-tx-proposals") - .help("If this flag is set, Lighthouse will query the Beacon Node for only block \ - headers during proposals and will sign over headers. Useful for outsourcing \ - execution payload construction during proposals.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("builder-registration-timestamp-override") - .long("builder-registration-timestamp-override") - .alias("builder-registration-timestamp-override") - .help("This flag takes a unix timestamp value that will be used to override the \ - timestamp used in the builder api registration") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("gas-limit") - .long("gas-limit") - .value_name("INTEGER") - .action(ArgAction::Set) - .help("The gas limit to be used in all builder proposals for all validators managed \ - by this validator client. Note this will not necessarily be used if the gas limit \ - set here moves too far from the previous block's gas limit. [default: 30,000,000]") - .requires("builder-proposals") - .display_order(0) - ) - .arg( - Arg::new("disable-latency-measurement-service") - .long("disable-latency-measurement-service") - .help("Disables the service that periodically attempts to measure latency to BNs.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("validator-registration-batch-size") - .long("validator-registration-batch-size") - .value_name("INTEGER") - .help("Defines the number of validators per \ - validator/register_validator request sent to the BN. This value \ - can be reduced to avoid timeouts from builders.") - .default_value("500") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("builder-boost-factor") - .long("builder-boost-factor") - .value_name("UINT64") - .help("Defines the boost factor, \ - a percentage multiplier to apply to the builder's payload value \ - when choosing between a builder payload header and payload from \ - the local execution node.") - .conflicts_with("prefer-builder-proposals") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("prefer-builder-proposals") - .long("prefer-builder-proposals") - .help("If this flag is set, Lighthouse will always prefer blocks \ - constructed by builders, regardless of payload value.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("beacon-nodes-sync-tolerances") - .long("beacon-nodes-sync-tolerances") - .value_name("SYNC_TOLERANCES") - .help("A comma-separated list of 3 values which sets the size of each sync distance range when \ - determining the health of each connected beacon node. \ - The first value determines the `Synced` range. \ - If a connected beacon node is synced to within this number of slots it is considered 'Synced'. \ - The second value determines the `Small` sync distance range. \ - This range starts immediately after the `Synced` range. \ - The third value determines the `Medium` sync distance range. \ - This range starts immediately after the `Small` range. \ - Any sync distance value beyond that is considered `Large`. \ - For example, a value of `8,8,48` would have ranges like the following: \ - `Synced`: 0..=8 \ - `Small`: 9..=16 \ - `Medium`: 17..=64 \ - `Large`: 65.. \ - These values are used to determine what ordering beacon node fallbacks are used in. \ - Generally, `Synced` nodes are preferred over `Small` and so on. \ - Nodes in the `Synced` range will tie-break based on their ordering in `--beacon-nodes`. \ - This ensures the primary beacon node is prioritised. \ - [default: 8,8,48]") - .action(ArgAction::Set) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("disable-slashing-protection-web3signer") - .long("disable-slashing-protection-web3signer") - .help("Disable Lighthouse's slashing protection for all web3signer keys. This can \ - reduce the I/O burden on the VC but is only safe if slashing protection \ - is enabled on the remote signer and is implemented correctly. DO NOT ENABLE \ - THIS FLAG UNLESS YOU ARE CERTAIN THAT SLASHING PROTECTION IS ENABLED ON \ - THE REMOTE SIGNER. YOU WILL GET SLASHED IF YOU USE THIS FLAG WITHOUT \ - ENABLING WEB3SIGNER'S SLASHING PROTECTION.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - /* - * Experimental/development options. - */ - .arg( - Arg::new("web3-signer-keep-alive-timeout") - .long("web3-signer-keep-alive-timeout") - .value_name("MILLIS") - .default_value("20000") - .help("Keep-alive timeout for each web3signer connection. Set to 'null' to never \ - timeout") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("web3-signer-max-idle-connections") - .long("web3-signer-max-idle-connections") - .value_name("COUNT") - .help("Maximum number of idle connections to maintain per web3signer host. Default \ - is unlimited.") - .action(ArgAction::Set) - .display_order(0) - ) + and never provide an untrusted URL.", + display_order = 0 + )] + pub monitoring_endpoint: Option, + + #[clap( + long, + value_name = "SECONDS", + requires = "monitoring_endpoint", + default_value_t = 60, + help = "Defines how many seconds to wait between each message sent to \ + the monitoring-endpoint.", + display_order = 0 + )] + pub monitoring_endpoint_period: u64, + + #[clap( + long, + value_name = "BOOLEAN", + help = "If this flag is set, Lighthouse will delay startup for three epochs and \ + monitor for messages on the network by any of the validators managed by this \ + client. This will result in three (possibly four) epochs worth of missed \ + attestations. If an attestation is detected during this period, it means it is \ + very likely that you are running a second validator client with the same keys. \ + This validator client will immediately shutdown if this is detected in order \ + to avoid potentially committing a slashable offense. Use this flag in order to \ + ENABLE this functionality, without this flag Lighthouse will begin attesting \ + immediately.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub enable_doppelganger_protection: bool, + + #[clap( + long, + alias = "private-tx-proposals", + help = "If this flag is set, Lighthouse will query the Beacon Node for only block \ + headers during proposals and will sign over headers. Useful for outsourcing \ + execution payload construction during proposals.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub builder_proposals: bool, + + #[clap( + long, + value_name = "UNIX-TIMESTAMP", + help = "This flag takes a unix timestamp value that will be used to override the \ + timestamp used in the builder api registration.", + display_order = 0 + )] + pub builder_registration_timestamp_override: Option, + + #[clap( + long, + value_name = "INTEGER", + default_value_t = 30_000_000, + requires = "builder_proposals", + help = "The gas limit to be used in all builder proposals for all validators managed \ + by this validator client. Note this will not necessarily be used if the gas limit \ + set here moves too far from the previous block's gas limit.", + display_order = 0 + )] + pub gas_limit: u64, + + #[clap( + long, + value_name = "BOOLEAN", + help = "Disables the service that periodically attempts to measure latency to BNs.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub disable_latency_measurement_service: bool, + + #[clap( + long, + value_name = "INTEGER", + default_value_t = 500, + help = "Defines the number of validators per \ + validator/register_validator request sent to the BN. This value \ + can be reduced to avoid timeouts from builders.", + display_order = 0 + )] + pub validator_registration_batch_size: usize, + + #[clap( + long, + value_name = "UINT64", + help = "Defines the boost factor, \ + a percentage multiplier to apply to the builder's payload value \ + when choosing between a builder payload header and payload from \ + the local execution node.", + conflicts_with = "prefer_builder_proposals", + display_order = 0 + )] + pub builder_boost_factor: Option, + + #[clap( + long, + help = "If this flag is set, Lighthouse will always prefer blocks \ + constructed by builders, regardless of payload value.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub prefer_builder_proposals: bool, + + #[clap( + long, + help = "A comma-separated list of 3 values which sets the size of each sync distance range when \ + determining the health of each connected beacon node. \ + The first value determines the `Synced` range. \ + If a connected beacon node is synced to within this number of slots it is considered 'Synced'. \ + The second value determines the `Small` sync distance range. \ + This range starts immediately after the `Synced` range. \ + The third value determines the `Medium` sync distance range. \ + This range starts immediately after the `Small` range. \ + Any sync distance value beyond that is considered `Large`. \ + For example, a value of `8,8,48` would have ranges like the following: \ + `Synced`: 0..=8 \ + `Small`: 9..=16 \ + `Medium`: 17..=64 \ + `Large`: 65.. \ + These values are used to determine what ordering beacon node fallbacks are used in. \ + Generally, `Synced` nodes are preferred over `Small` and so on. \ + Nodes in the `Synced` range will tie-break based on their ordering in `--beacon-nodes`. \ + This ensures the primary beacon node is prioritised.", + display_order = 0, + value_delimiter = ',', + default_value = "8,8,48", + help_heading = FLAG_HEADER, + value_name = "SYNC_TOLERANCES" + )] + pub beacon_nodes_sync_tolerances: Vec, + + #[clap( + long, + help = "Disable Lighthouse's slashing protection for all web3signer keys. This can \ + reduce the I/O burden on the VC but is only safe if slashing protection \ + is enabled on the remote signer and is implemented correctly. DO NOT ENABLE \ + THIS FLAG UNLESS YOU ARE CERTAIN THAT SLASHING PROTECTION IS ENABLED ON \ + THE REMOTE SIGNER. YOU WILL GET SLASHED IF YOU USE THIS FLAG WITHOUT \ + ENABLING WEB3SIGNER'S SLASHING PROTECTION.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub disable_slashing_protection_web3signer: bool, + + /* Experimental/development options */ + #[clap( + long, + value_name = "MILLIS", + default_value_t = 20000, + help = "Keep-alive timeout for each web3signer connection. Set to '0' to never \ + timeout.", + display_order = 0 + )] + pub web3_signer_keep_alive_timeout: u64, + + #[clap( + long, + value_name = "COUNT", + help = "Maximum number of idle connections to maintain per web3signer host. Default \ + is unlimited.", + display_order = 0 + )] + pub web3_signer_max_idle_connections: Option, } diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index bb72ef81c8..2a848e2022 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,6 +1,8 @@ -use beacon_node_fallback::{beacon_node_health::BeaconNodeSyncDistanceTiers, ApiTopic}; +use crate::cli::ValidatorClient; +use beacon_node_fallback::beacon_node_health::BeaconNodeSyncDistanceTiers; +use beacon_node_fallback::ApiTopic; use clap::ArgMatches; -use clap_utils::{flags::DISABLE_MALLOC_TUNING_FLAG, parse_optional, parse_required}; +use clap_utils::{flags::DISABLE_MALLOC_TUNING_FLAG, parse_required}; use directory::{ get_network_dir, DEFAULT_HARDCODED_NETWORK, DEFAULT_ROOT_DIR, DEFAULT_SECRET_DIR, DEFAULT_VALIDATOR_DIR, @@ -14,9 +16,8 @@ use slog::{info, warn, Logger}; use std::fs; use std::net::IpAddr; use std::path::PathBuf; -use std::str::FromStr; use std::time::Duration; -use types::{Address, GRAFFITI_BYTES_LEN}; +use types::GRAFFITI_BYTES_LEN; use validator_http_api::{self, PK_FILENAME}; use validator_http_metrics; use validator_store::Config as ValidatorStoreConfig; @@ -132,7 +133,11 @@ impl Default for Config { impl Config { /// Returns a `Default` implementation of `Self` with some parameters modified by the supplied /// `cli_args`. - pub fn from_cli(cli_args: &ArgMatches, log: &Logger) -> Result { + pub fn from_cli( + cli_args: &ArgMatches, + validator_client_config: &ValidatorClient, + log: &Logger, + ) -> Result { let mut config = Config::default(); let default_root_dir = dirs::home_dir() @@ -145,11 +150,12 @@ impl Config { validator_dir = Some(base_dir.join(DEFAULT_VALIDATOR_DIR)); secrets_dir = Some(base_dir.join(DEFAULT_SECRET_DIR)); } - if cli_args.get_one::("validators-dir").is_some() { - validator_dir = Some(parse_required(cli_args, "validators-dir")?); + + if let Some(validator_dir_path) = validator_client_config.validators_dir.as_ref() { + validator_dir = Some(validator_dir_path.clone()); } - if cli_args.get_one::("secrets-dir").is_some() { - secrets_dir = Some(parse_required(cli_args, "secrets-dir")?); + if let Some(secrets_dir_path) = validator_client_config.secrets_dir.as_ref() { + secrets_dir = Some(secrets_dir_path.clone()); } config.validator_dir = validator_dir.unwrap_or_else(|| { @@ -169,35 +175,36 @@ impl Config { .map_err(|e| format!("Failed to create {:?}: {:?}", config.validator_dir, e))?; } - if let Some(beacon_nodes) = parse_optional::(cli_args, "beacon-nodes")? { + if let Some(beacon_nodes) = validator_client_config.beacon_nodes.as_ref() { config.beacon_nodes = beacon_nodes - .split(',') - .map(SensitiveUrl::parse) + .iter() + .map(|s| SensitiveUrl::parse(s)) .collect::>() .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?; } - if let Some(proposer_nodes) = parse_optional::(cli_args, "proposer-nodes")? { + + if let Some(proposer_nodes) = validator_client_config.proposer_nodes.as_ref() { config.proposer_nodes = proposer_nodes - .split(',') - .map(SensitiveUrl::parse) + .iter() + .map(|s| SensitiveUrl::parse(s)) .collect::>() .map_err(|e| format!("Unable to parse proposer node URL: {:?}", e))?; } - config.disable_auto_discover = cli_args.get_flag("disable-auto-discover"); - config.init_slashing_protection = cli_args.get_flag("init-slashing-protection"); - config.use_long_timeouts = cli_args.get_flag("use-long-timeouts"); + config.disable_auto_discover = validator_client_config.disable_auto_discover; + config.init_slashing_protection = validator_client_config.init_slashing_protection; + config.use_long_timeouts = validator_client_config.use_long_timeouts; - if let Some(graffiti_file_path) = cli_args.get_one::("graffiti-file") { + if let Some(graffiti_file_path) = validator_client_config.graffiti_file.as_ref() { let mut graffiti_file = GraffitiFile::new(graffiti_file_path.into()); graffiti_file .read_graffiti_file() .map_err(|e| format!("Error reading graffiti file: {:?}", e))?; config.graffiti_file = Some(graffiti_file); - info!(log, "Successfully loaded graffiti file"; "path" => graffiti_file_path); + info!(log, "Successfully loaded graffiti file"; "path" => graffiti_file_path.to_str()); } - if let Some(input_graffiti) = cli_args.get_one::("graffiti") { + if let Some(input_graffiti) = validator_client_config.graffiti.as_ref() { let graffiti_bytes = input_graffiti.as_bytes(); if graffiti_bytes.len() > GRAFFITI_BYTES_LEN { return Err(format!( @@ -216,55 +223,40 @@ impl Config { } } - if let Some(input_fee_recipient) = - parse_optional::
(cli_args, "suggested-fee-recipient")? - { + if let Some(input_fee_recipient) = validator_client_config.suggested_fee_recipient { config.validator_store.fee_recipient = Some(input_fee_recipient); } - if let Some(tls_certs) = parse_optional::(cli_args, "beacon-nodes-tls-certs")? { - config.beacon_nodes_tls_certs = Some(tls_certs.split(',').map(PathBuf::from).collect()); + if let Some(tls_certs) = validator_client_config.beacon_nodes_tls_certs.as_ref() { + config.beacon_nodes_tls_certs = Some(tls_certs.iter().map(PathBuf::from).collect()); } - if cli_args.get_flag("distributed") { - config.distributed = true; - } + config.distributed = validator_client_config.distributed; - if let Some(broadcast_topics) = cli_args.get_one::("broadcast") { - config.broadcast_topics = broadcast_topics - .split(',') - .filter(|t| *t != "none") - .map(|t| { - t.trim() - .parse::() - .map_err(|_| format!("Unknown API topic to broadcast: {t}")) - }) - .collect::>()?; + if let Some(mut broadcast_topics) = validator_client_config.broadcast.clone() { + broadcast_topics.retain(|topic| *topic != ApiTopic::None); + config.broadcast_topics = broadcast_topics; } /* * Beacon node fallback */ - if let Some(sync_tolerance) = cli_args.get_one::("beacon-nodes-sync-tolerances") { - config.beacon_node_fallback.sync_tolerances = - BeaconNodeSyncDistanceTiers::from_str(sync_tolerance)?; - } else { - config.beacon_node_fallback.sync_tolerances = BeaconNodeSyncDistanceTiers::default(); - } + config.beacon_node_fallback.sync_tolerances = BeaconNodeSyncDistanceTiers::from_vec( + &validator_client_config.beacon_nodes_sync_tolerances, + )?; /* * Web3 signer */ - if let Some(s) = parse_optional::(cli_args, "web3-signer-keep-alive-timeout")? { - config.initialized_validators.web3_signer_keep_alive_timeout = if s == "null" { - None - } else { - Some(Duration::from_millis( - s.parse().map_err(|_| "invalid timeout value".to_string())?, - )) - } + if validator_client_config.web3_signer_keep_alive_timeout == 0 { + config.initialized_validators.web3_signer_keep_alive_timeout = None + } else { + config.initialized_validators.web3_signer_keep_alive_timeout = Some( + Duration::from_millis(validator_client_config.web3_signer_keep_alive_timeout), + ); } - if let Some(n) = parse_optional::(cli_args, "web3-signer-max-idle-connections")? { + + if let Some(n) = validator_client_config.web3_signer_max_idle_connections { config .initialized_validators .web3_signer_max_idle_connections = Some(n); @@ -274,12 +266,10 @@ impl Config { * Http API server */ - if cli_args.get_flag("http") { - config.http_api.enabled = true; - } + config.http_api.enabled = validator_client_config.http; - if let Some(address) = cli_args.get_one::("http-address") { - if cli_args.get_flag("unencrypted-http-transport") { + if let Some(address) = &validator_client_config.http_address { + if validator_client_config.unencrypted_http_transport { config.http_api.listen_addr = address .parse::() .map_err(|_| "http-address is not a valid IP address.")?; @@ -291,13 +281,9 @@ impl Config { } } - if let Some(port) = cli_args.get_one::("http-port") { - config.http_api.listen_port = port - .parse::() - .map_err(|_| "http-port is not a valid u16.")?; - } + config.http_api.listen_port = validator_client_config.http_port; - if let Some(allow_origin) = cli_args.get_one::("http-allow-origin") { + if let Some(allow_origin) = validator_client_config.http_allow_origin.as_ref() { // Pre-validate the config value to give feedback to the user on node startup, instead of // as late as when the first API response is produced. hyper::header::HeaderValue::from_str(allow_origin) @@ -306,15 +292,11 @@ impl Config { config.http_api.allow_origin = Some(allow_origin.to_string()); } - if cli_args.get_flag("http-allow-keystore-export") { - config.http_api.allow_keystore_export = true; - } + config.http_api.allow_keystore_export = validator_client_config.http_allow_keystore_export; + config.http_api.store_passwords_in_secrets_dir = + validator_client_config.http_store_passwords_in_secrets_dir; - if cli_args.get_flag("http-store-passwords-in-secrets-dir") { - config.http_api.store_passwords_in_secrets_dir = true; - } - - if let Some(http_token_path) = cli_args.get_one::("http-token-path") { + if let Some(http_token_path) = &validator_client_config.http_token_path { config.http_api.http_token_path = PathBuf::from(http_token_path); } else { // For backward compatibility, default to the path under the validator dir if not provided. @@ -325,27 +307,19 @@ impl Config { * Prometheus metrics HTTP server */ - if cli_args.get_flag("metrics") { - config.http_metrics.enabled = true; - } + config.http_metrics.enabled = validator_client_config.metrics; + config.enable_high_validator_count_metrics = + validator_client_config.enable_high_validator_count_metrics; - if cli_args.get_flag("enable-high-validator-count-metrics") { - config.enable_high_validator_count_metrics = true; - } - - if let Some(address) = cli_args.get_one::("metrics-address") { - config.http_metrics.listen_addr = address + if let Some(metrics_address) = &validator_client_config.metrics_address { + config.http_metrics.listen_addr = metrics_address .parse::() .map_err(|_| "metrics-address is not a valid IP address.")?; } - if let Some(port) = cli_args.get_one::("metrics-port") { - config.http_metrics.listen_port = port - .parse::() - .map_err(|_| "metrics-port is not a valid u16.")?; - } + config.http_metrics.listen_port = validator_client_config.metrics_port; - if let Some(allow_origin) = cli_args.get_one::("metrics-allow-origin") { + if let Some(allow_origin) = validator_client_config.metrics_allow_origin.as_ref() { // Pre-validate the config value to give feedback to the user on node startup, instead of // as late as when the first API response is produced. hyper::header::HeaderValue::from_str(allow_origin) @@ -361,9 +335,8 @@ impl Config { /* * Explorer metrics */ - if let Some(monitoring_endpoint) = cli_args.get_one::("monitoring-endpoint") { - let update_period_secs = - clap_utils::parse_optional(cli_args, "monitoring-endpoint-period")?; + if let Some(monitoring_endpoint) = validator_client_config.monitoring_endpoint.as_ref() { + let update_period_secs = Some(validator_client_config.monitoring_endpoint_period); config.monitoring_api = Some(monitoring_api::Config { db_path: None, freezer_db_path: None, @@ -372,56 +345,34 @@ impl Config { }); } - if cli_args.get_flag("enable-doppelganger-protection") { - config.enable_doppelganger_protection = true; - } + config.enable_doppelganger_protection = + validator_client_config.enable_doppelganger_protection; + config.validator_store.builder_proposals = validator_client_config.builder_proposals; + config.validator_store.prefer_builder_proposals = + validator_client_config.prefer_builder_proposals; + config.validator_store.gas_limit = Some(validator_client_config.gas_limit); - if cli_args.get_flag("builder-proposals") { - config.validator_store.builder_proposals = true; - } - - if cli_args.get_flag("prefer-builder-proposals") { - config.validator_store.prefer_builder_proposals = true; - } - - config.validator_store.gas_limit = cli_args - .get_one::("gas-limit") - .map(|gas_limit| { - gas_limit - .parse::() - .map_err(|_| "gas-limit is not a valid u64.") - }) - .transpose()?; - - if let Some(registration_timestamp_override) = - cli_args.get_one::("builder-registration-timestamp-override") - { - config.builder_registration_timestamp_override = Some( - registration_timestamp_override - .parse::() - .map_err(|_| "builder-registration-timestamp-override is not a valid u64.")?, - ); - } - - config.validator_store.builder_boost_factor = - parse_optional(cli_args, "builder-boost-factor")?; + config.builder_registration_timestamp_override = + validator_client_config.builder_registration_timestamp_override; + config.validator_store.builder_boost_factor = validator_client_config.builder_boost_factor; config.enable_latency_measurement_service = - !cli_args.get_flag("disable-latency-measurement-service"); + !validator_client_config.disable_latency_measurement_service; config.validator_registration_batch_size = - parse_required(cli_args, "validator-registration-batch-size")?; + validator_client_config.validator_registration_batch_size; + if config.validator_registration_batch_size == 0 { return Err("validator-registration-batch-size cannot be 0".to_string()); } config.validator_store.enable_web3signer_slashing_protection = - if cli_args.get_flag("disable-slashing-protection-web3signer") { + if validator_client_config.disable_slashing_protection_web3signer { warn!( log, "Slashing protection for remote keys disabled"; "info" => "ensure slashing protection on web3signer is enabled or you WILL \ - get slashed" + get slashed" ); false } else { diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index e566f632f1..341bb54154 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -1,9 +1,9 @@ -mod cli; +pub mod cli; pub mod config; mod latency; mod notifier; -pub use cli::cli_app; +use crate::cli::ValidatorClient; pub use config::Config; use initialized_validators::InitializedValidators; use metrics::set_gauge; @@ -11,11 +11,10 @@ use monitoring_api::{MonitoringHttpClient, ProcessType}; use sensitive_url::SensitiveUrl; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use account_utils::validator_definitions::ValidatorDefinitions; use beacon_node_fallback::{ start_fallback_updater_service, BeaconNodeFallback, CandidateBeaconNode, }; - -use account_utils::validator_definitions::ValidatorDefinitions; use clap::ArgMatches; use doppelganger_service::DoppelgangerService; use environment::RuntimeContext; @@ -100,8 +99,9 @@ impl ProductionValidatorClient { pub async fn new_from_cli( context: RuntimeContext, cli_args: &ArgMatches, + validator_client_config: &ValidatorClient, ) -> Result { - let config = Config::from_cli(cli_args, context.log()) + let config = Config::from_cli(cli_args, validator_client_config, context.log()) .map_err(|e| format!("Unable to initialize config: {}", e))?; Self::new(context, config).await } @@ -207,15 +207,15 @@ impl ProductionValidatorClient { config.initialized_validators.clone(), log.clone(), ) - .await - .map_err(|e| { - match e { - UnableToOpenVotingKeystore(err) => { - format!("Unable to initialize validators: {:?}. If you have recently moved the location of your data directory \ + .await + .map_err(|e| { + match e { + UnableToOpenVotingKeystore(err) => { + format!("Unable to initialize validators: {:?}. If you have recently moved the location of your data directory \ make sure to update the location of voting_keystore_path in your validator_definitions.yml", err) - }, - err => { - format!("Unable to initialize validators: {:?}", err)} + }, + err => { + format!("Unable to initialize validators: {:?}", err)} } })?; diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 8e43cd5977..9beccd3bde 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -1,5 +1,5 @@ -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::{get_color_style, FLAG_HEADER}; +use clap::{ArgMatches, Command}; +use clap_utils::get_color_style; use common::write_to_json_file; use environment::Environment; use serde::Serialize; @@ -46,16 +46,6 @@ pub fn cli_app() -> Command { .display_order(0) .styles(get_color_style()) .about("Utilities for managing a Lighthouse validator client via the HTTP API.") - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER) - .global(true), - ) .subcommand(create_validators::cli_app()) .subcommand(import_validators::cli_app()) .subcommand(move_validators::cli_app())