From 2639e67e908b45d2b23208ca59f0ebdbbdbaff97 Mon Sep 17 00:00:00 2001 From: Divma <26765164+divagant-martian@users.noreply.github.com> Date: Tue, 13 Jun 2023 01:25:05 +0000 Subject: [PATCH 01/25] Update discv5 to expand ipv6 support (#4319) Done in different PRs so that they can reviewed independently, as it's likely this won't be merged before I leave Includes resolution for #4080 - [ ] #4299 - [ ] #4318 - [ ] #4320 Co-authored-by: Diva M Co-authored-by: Age Manning --- Cargo.lock | 294 ++++++++++++++---- beacon_node/lighthouse_network/Cargo.toml | 4 +- beacon_node/lighthouse_network/src/config.rs | 30 +- .../src/discovery/enr_ext.rs | 28 +- .../lighthouse_network/src/discovery/mod.rs | 39 +-- beacon_node/src/cli.rs | 2 - book/src/advanced_networking.md | 78 ++++- boot_node/src/cli.rs | 37 ++- boot_node/src/config.rs | 57 ++-- boot_node/src/server.rs | 6 +- common/eth2_network_config/Cargo.toml | 2 +- lighthouse/tests/boot_node.rs | 18 +- 12 files changed, 425 insertions(+), 170 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f40e53c3..f53171aaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,6 +566,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -1377,6 +1383,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1472,11 +1490,12 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.1" +version = "4.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" dependencies = [ "cfg-if", + "digest 0.10.7", "fiat-crypto", "packed_simd_2", "platforms 3.0.2", @@ -1659,6 +1678,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der-parser" version = "7.0.0" @@ -1805,6 +1834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -1861,15 +1891,15 @@ dependencies = [ [[package]] name = "discv5" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b009a99b85b58900df46435307fc5c4c845af7e182582b1fbf869572fa9fce69" +checksum = "77f32d27968ba86689e3f0eccba0383414348a6fc5918b0a639c98dd81e20ed6" dependencies = [ "aes 0.7.5", "aes-gcm 0.9.4", "arrayvec", "delay_map", - "enr 0.7.0", + "enr 0.8.1", "fnv", "futures", "hashlink 0.7.0", @@ -1885,8 +1915,6 @@ dependencies = [ "smallvec", "socket2 0.4.9", "tokio", - "tokio-stream", - "tokio-util 0.6.10", "tracing", "tracing-subscriber", "uint", @@ -1922,10 +1950,24 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" +dependencies = [ + "der 0.7.6", + "digest 0.10.7", + "elliptic-curve 0.13.5", + "rfc6979 0.4.0", + "signature 2.1.0", + "spki 0.7.2", ] [[package]] @@ -1934,7 +1976,17 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ - "signature", + "signature 1.6.4", +] + +[[package]] +name = "ed25519" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.1.0", ] [[package]] @@ -1944,13 +1996,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", - "ed25519", + "ed25519 1.5.3", "rand 0.7.3", "serde", "sha2 0.9.9", "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a" +dependencies = [ + "curve25519-dalek 4.0.0-rc.2", + "ed25519 2.2.1", + "rand_core 0.6.4", + "serde", + "sha2 0.10.6", + "zeroize", +] + [[package]] name = "ef_tests" version = "0.2.0" @@ -1994,18 +2060,37 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", "digest 0.10.7", - "ff", + "ff 0.12.1", "generic-array", - "group", + "group 0.12.1", "hkdf", "pem-rfc7468", - "pkcs8", + "pkcs8 0.9.0", "rand_core 0.6.4", - "sec1", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.2", + "digest 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.2", "subtle", "zeroize", ] @@ -2029,7 +2114,7 @@ dependencies = [ "bs58", "bytes", "hex", - "k256", + "k256 0.11.6", "log", "rand 0.8.5", "rlp", @@ -2040,16 +2125,15 @@ dependencies = [ [[package]] name = "enr" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "492a7e5fc2504d5fdce8e124d3e263b244a68b283cac67a69eda0cd43e0aebad" +checksum = "cf56acd72bb22d2824e66ae8e9e5ada4d0de17a69c7fd35569dde2ada8ec9116" dependencies = [ "base64 0.13.1", - "bs58", "bytes", - "ed25519-dalek", + "ed25519-dalek 2.0.0-rc.2", "hex", - "k256", + "k256 0.13.1", "log", "rand 0.8.5", "rlp", @@ -2545,11 +2629,11 @@ dependencies = [ "bytes", "cargo_metadata", "chrono", - "elliptic-curve", + "elliptic-curve 0.12.3", "ethabi 18.0.0", "generic-array", "hex", - "k256", + "k256 0.11.6", "once_cell", "open-fastrlp", "rand 0.8.5", @@ -2731,6 +2815,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ffi-opaque" version = "2.0.1" @@ -3009,6 +3103,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -3118,7 +3213,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -3855,12 +3961,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", "sha2 0.10.6", "sha3 0.10.8", ] +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa 0.16.7", + "elliptic-curve 0.13.5", + "once_cell", + "sha2 0.10.6", + "signature 2.1.0", +] + [[package]] name = "keccak" version = "0.1.4" @@ -4059,7 +4179,7 @@ checksum = "b1fff5bd889c82a0aec668f2045edd066f559d4e5c40354e5a4c77ac00caac38" dependencies = [ "asn1_der", "bs58", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "either", "fnv", "futures", @@ -4094,7 +4214,7 @@ checksum = "b6a8fcd392ff67af6cc3f03b1426c41f7f26b6b9aff2dc632c1c56dd649e571f" dependencies = [ "asn1_der", "bs58", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "either", "fnv", "futures", @@ -4113,7 +4233,7 @@ dependencies = [ "prost-build", "rand 0.8.5", "rw-stream-sink", - "sec1", + "sec1 0.3.0", "sha2 0.10.6", "smallvec", "thiserror", @@ -4222,7 +4342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e2d584751cecb2aabaa56106be6be91338a60a0f4e420cf2af639204f596fc1" dependencies = [ "bs58", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "log", "multiaddr 0.17.1", "multihash 0.17.0", @@ -5654,8 +5774,8 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", "sha2 0.10.6", ] @@ -5665,8 +5785,8 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", "sha2 0.10.6", ] @@ -5922,8 +6042,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.6", + "spki 0.7.2", ] [[package]] @@ -6666,11 +6796,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac 0.12.1", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -7047,10 +7187,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.6", + "generic-array", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -7339,6 +7493,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simple_asn1" version = "0.6.2" @@ -7576,14 +7740,14 @@ checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" [[package]] name = "snow" -version = "0.9.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +checksum = "774d05a3edae07ce6d68ea6984f3c05e9bba8927e3dd591e3b479e5b03213d0d" dependencies = [ "aes-gcm 0.9.4", "blake2", "chacha20poly1305", - "curve25519-dalek 4.0.0-rc.1", + "curve25519-dalek 4.0.0-rc.2", "rand_core 0.6.4", "ring", "rustc_version 0.4.0", @@ -7640,7 +7804,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der 0.7.6", ] [[package]] @@ -9358,9 +9532,9 @@ dependencies = [ [[package]] name = "webrtc-dtls" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942be5bd85f072c3128396f6e5a9bfb93ca8c1939ded735d177b7bcba9a13d05" +checksum = "c4a00f4242f2db33307347bd5be53263c52a0331c96c14292118c9a6bb48d267" dependencies = [ "aes 0.6.0", "aes-gcm 0.10.2", @@ -9371,29 +9545,28 @@ dependencies = [ "ccm", "curve25519-dalek 3.2.0", "der-parser 8.2.0", - "elliptic-curve", + "elliptic-curve 0.12.3", "hkdf", "hmac 0.12.1", "log", - "oid-registry 0.6.1", "p256", "p384", "rand 0.8.5", "rand_core 0.6.4", - "rcgen 0.9.3", + "rcgen 0.10.0", "ring", "rustls 0.19.1", - "sec1", + "sec1 0.3.0", "serde", "sha1", "sha2 0.10.6", - "signature", + "signature 1.6.4", "subtle", "thiserror", "tokio", "webpki 0.21.4", "webrtc-util", - "x25519-dalek 2.0.0-pre.1", + "x25519-dalek 2.0.0-rc.2", "x509-parser 0.13.2", ] @@ -9836,12 +10009,13 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0-pre.1" +version = "2.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "fabd6e16dd08033932fc3265ad4510cc2eab24656058a6dcb107ffe274abcc95" dependencies = [ - "curve25519-dalek 3.2.0", + "curve25519-dalek 4.0.0-rc.2", "rand_core 0.6.4", + "serde", "zeroize", ] diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index c1b4d7217..ca15b5ef2 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Sigma Prime "] edition = "2021" [dependencies] -discv5 = { version = "0.2.2", features = ["libp2p"] } +discv5 = { version = "0.3.0", features = ["libp2p"]} unsigned-varint = { version = "0.6.0", features = ["codec"] } types = { path = "../../consensus/types" } ssz_types = "0.5.0" @@ -60,4 +60,4 @@ quickcheck = "0.9.2" quickcheck_macros = "0.9.1" [features] -libp2p-websocket = [] +libp2p-websocket = [] \ No newline at end of file diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 01bb8569d..946752645 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -163,7 +163,7 @@ impl Config { udp_port, tcp_port, }); - self.discv5_config.ip_mode = discv5::IpMode::Ip4; + self.discv5_config.listen_config = discv5::ListenConfig::from_ip(addr.into(), udp_port); self.discv5_config.table_filter = |enr| enr.ip4().as_ref().map_or(false, is_global_ipv4) } @@ -176,9 +176,8 @@ impl Config { udp_port, tcp_port, }); - self.discv5_config.ip_mode = discv5::IpMode::Ip6 { - enable_mapped_addresses: false, - }; + + self.discv5_config.listen_config = discv5::ListenConfig::from_ip(addr.into(), udp_port); self.discv5_config.table_filter = |enr| enr.ip6().as_ref().map_or(false, is_global_ipv6) } @@ -206,10 +205,10 @@ impl Config { tcp_port: tcp6_port, }, ); + self.discv5_config.listen_config = discv5::ListenConfig::default() + .with_ipv4(v4_addr, udp4_port) + .with_ipv6(v6_addr, udp6_port); - self.discv5_config.ip_mode = discv5::IpMode::Ip6 { - enable_mapped_addresses: true, - }; self.discv5_config.table_filter = |enr| match (&enr.ip4(), &enr.ip6()) { (None, None) => false, (None, Some(ip6)) => is_global_ipv6(ip6), @@ -279,9 +278,17 @@ impl Default for Config { .build() .expect("The total rate limit has been specified"), ); + let listen_addresses = ListenAddress::V4(ListenAddr { + addr: Ipv4Addr::UNSPECIFIED, + udp_port: 9000, + tcp_port: 9000, + }); + + let discv5_listen_config = + discv5::ListenConfig::from_ip(Ipv4Addr::UNSPECIFIED.into(), 9000); // discv5 configuration - let discv5_config = Discv5ConfigBuilder::new() + let discv5_config = Discv5ConfigBuilder::new(discv5_listen_config) .enable_packet_filter() .session_cache_capacity(5000) .request_timeout(Duration::from_secs(1)) @@ -304,12 +311,9 @@ impl Default for Config { // NOTE: Some of these get overridden by the corresponding CLI default values. Config { network_dir, - listen_addresses: ListenAddress::V4(ListenAddr { - addr: Ipv4Addr::UNSPECIFIED, - udp_port: 9000, - tcp_port: 9000, - }), + listen_addresses, enr_address: (None, None), + enr_udp4_port: None, enr_tcp4_port: None, enr_udp6_port: None, diff --git a/beacon_node/lighthouse_network/src/discovery/enr_ext.rs b/beacon_node/lighthouse_network/src/discovery/enr_ext.rs index e9cca6667..3df7f7c16 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr_ext.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr_ext.rs @@ -198,7 +198,7 @@ impl CombinedKeyPublicExt for CombinedPublicKey { fn as_peer_id(&self) -> PeerId { match self { Self::Secp256k1(pk) => { - let pk_bytes = pk.to_bytes(); + let pk_bytes = pk.to_sec1_bytes(); let libp2p_pk = libp2p::core::PublicKey::Secp256k1( libp2p::core::identity::secp256k1::PublicKey::decode(&pk_bytes) .expect("valid public key"), @@ -222,14 +222,16 @@ impl CombinedKeyExt for CombinedKey { match key { Keypair::Secp256k1(key) => { let secret = - discv5::enr::k256::ecdsa::SigningKey::from_bytes(&key.secret().to_bytes()) + discv5::enr::k256::ecdsa::SigningKey::from_slice(&key.secret().to_bytes()) .expect("libp2p key must be valid"); Ok(CombinedKey::Secp256k1(secret)) } Keypair::Ed25519(key) => { - let ed_keypair = - discv5::enr::ed25519_dalek::SecretKey::from_bytes(&key.encode()[..32]) - .expect("libp2p key must be valid"); + let ed_keypair = discv5::enr::ed25519_dalek::SigningKey::from_bytes( + &(key.encode()[..32]) + .try_into() + .expect("libp2p key must be valid"), + ); Ok(CombinedKey::from(ed_keypair)) } Keypair::Ecdsa(_) => Err("Ecdsa keypairs not supported"), @@ -281,7 +283,7 @@ mod tests { fn test_secp256k1_peer_id_conversion() { let sk_hex = "df94a73d528434ce2309abb19c16aedb535322797dbd59c157b1e04095900f48"; let sk_bytes = hex::decode(sk_hex).unwrap(); - let secret_key = discv5::enr::k256::ecdsa::SigningKey::from_bytes(&sk_bytes).unwrap(); + let secret_key = discv5::enr::k256::ecdsa::SigningKey::from_slice(&sk_bytes).unwrap(); let libp2p_sk = libp2p::identity::secp256k1::SecretKey::from_bytes(sk_bytes).unwrap(); let secp256k1_kp: libp2p::identity::secp256k1::Keypair = libp2p_sk.into(); @@ -300,16 +302,18 @@ mod tests { fn test_ed25519_peer_conversion() { let sk_hex = "4dea8a5072119927e9d243a7d953f2f4bc95b70f110978e2f9bc7a9000e4b261"; let sk_bytes = hex::decode(sk_hex).unwrap(); - let secret = discv5::enr::ed25519_dalek::SecretKey::from_bytes(&sk_bytes).unwrap(); - let public = discv5::enr::ed25519_dalek::PublicKey::from(&secret); - let keypair = discv5::enr::ed25519_dalek::Keypair { secret, public }; + let secret_key = discv5::enr::ed25519_dalek::SigningKey::from_bytes( + &sk_bytes.clone().try_into().unwrap(), + ); let libp2p_sk = libp2p::identity::ed25519::SecretKey::from_bytes(sk_bytes).unwrap(); - let ed25519_kp: libp2p::identity::ed25519::Keypair = libp2p_sk.into(); - let libp2p_kp = Keypair::Ed25519(ed25519_kp); + let secp256k1_kp: libp2p::identity::ed25519::Keypair = libp2p_sk.into(); + let libp2p_kp = Keypair::Ed25519(secp256k1_kp); let peer_id = libp2p_kp.public().to_peer_id(); - let enr = discv5::enr::EnrBuilder::new("v4").build(&keypair).unwrap(); + let enr = discv5::enr::EnrBuilder::new("v4") + .build(&secret_key) + .unwrap(); let node_id = peer_id_to_node_id(&peer_id).unwrap(); assert_eq!(enr.node_id(), node_id); diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 13fdf8ed5..3ee74ebf0 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -209,13 +209,6 @@ impl Discovery { info!(log, "ENR Initialised"; "enr" => local_enr.to_base64(), "seq" => local_enr.seq(), "id"=> %local_enr.node_id(), "ip4" => ?local_enr.ip4(), "udp4"=> ?local_enr.udp4(), "tcp4" => ?local_enr.tcp4(), "tcp6" => ?local_enr.tcp6(), "udp6" => ?local_enr.udp6() ); - let listen_socket = match config.listen_addrs() { - crate::listen_addr::ListenAddress::V4(v4_addr) => v4_addr.udp_socket_addr(), - crate::listen_addr::ListenAddress::V6(v6_addr) => v6_addr.udp_socket_addr(), - crate::listen_addr::ListenAddress::DualStack(_v4_addr, v6_addr) => { - v6_addr.udp_socket_addr() - } - }; // convert the keypair into an ENR key let enr_key: CombinedKey = CombinedKey::from_libp2p(local_key)?; @@ -251,10 +244,7 @@ impl Discovery { // Start the discv5 service and obtain an event stream let event_stream = if !config.disable_discovery { - discv5 - .start(listen_socket) - .map_err(|e| e.to_string()) - .await?; + discv5.start().map_err(|e| e.to_string()).await?; debug!(log, "Discovery service started"); EventStream::Awaiting(Box::pin(discv5.event_stream())) } else { @@ -413,7 +403,7 @@ impl Discovery { /// If the external address needs to be modified, use `update_enr_udp_socket. pub fn update_enr_tcp_port(&mut self, port: u16) -> Result<(), String> { self.discv5 - .enr_insert("tcp", &port.to_be_bytes()) + .enr_insert("tcp", &port) .map_err(|e| format!("{:?}", e))?; // replace the global version @@ -428,29 +418,12 @@ impl Discovery { /// This is with caution. Discovery should automatically maintain this. This should only be /// used when automatic discovery is disabled. pub fn update_enr_udp_socket(&mut self, socket_addr: SocketAddr) -> Result<(), String> { - match socket_addr { - SocketAddr::V4(socket) => { - self.discv5 - .enr_insert("ip", &socket.ip().octets()) - .map_err(|e| format!("{:?}", e))?; - self.discv5 - .enr_insert("udp", &socket.port().to_be_bytes()) - .map_err(|e| format!("{:?}", e))?; - } - SocketAddr::V6(socket) => { - self.discv5 - .enr_insert("ip6", &socket.ip().octets()) - .map_err(|e| format!("{:?}", e))?; - self.discv5 - .enr_insert("udp6", &socket.port().to_be_bytes()) - .map_err(|e| format!("{:?}", e))?; - } + const IS_TCP: bool = false; + if self.discv5.update_local_enr_socket(socket_addr, IS_TCP) { + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); } - - // replace the global version *self.network_globals.local_enr.write() = self.discv5.local_enr(); - // persist modified enr to disk - enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); Ok(()) } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 379eb8e33..e763d93f8 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -116,7 +116,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("PORT") .help("The UDP port that discovery will listen on over IpV6 if listening over \ both Ipv4 and IpV6. Defaults to `port6`") - .hidden(true) // TODO: implement dual stack via two sockets in discv5. .takes_value(true), ) .arg( @@ -198,7 +197,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { discovery. Set this only if you are sure other nodes can connect to your \ local node on this address. This will update the `ip4` or `ip6` ENR fields \ accordingly. To update both, set this flag twice with the different values.") - .requires("enr-udp-port") .multiple(true) .max_values(2) .takes_value(true), diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index 59f3da376..586503cb9 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -38,7 +38,6 @@ large peer count will not speed up sync. For these reasons, we recommend users do not modify the `--target-peers` count drastically and use the (recommended) default. - ### NAT Traversal (Port Forwarding) Lighthouse, by default, uses port 9000 for both TCP and UDP. Lighthouse will @@ -55,7 +54,7 @@ enabled, we recommend you to manually set up port mappings to both of Lighthouse TCP and UDP ports (9000 by default). > Note: Lighthouse needs to advertise its publicly accessible ports in -> order to inform its peers that it is contactable and how to connect to it. +> order to inform its peers that it is contactable and how to connect to it. > Lighthouse has an automated way of doing this for the UDP port. This means > Lighthouse can detect its external UDP port. There is no such mechanism for the > TCP port. As such, we assume that the external UDP and external TCP port is the @@ -107,3 +106,78 @@ Modifying the ENR settings can degrade the discovery of your node, making it harder for peers to find you or potentially making it harder for other peers to find each other. We recommend not touching these settings unless for a more advanced use case. + + +### IPv6 support + +As noted in the previous sections, two fundamental parts to ensure good +connectivity are: The parameters that configure the sockets over which +Lighthouse listens for connections, and the parameters used to tell other peers +how to connect to your node. This distinction is relevant and applies to most +nodes that do not run directly on a public network. + +#### Configuring Lighthouse to listen over IPv4/IPv6/Dual stack + +To listen over only IPv6 use the same parameters as done when listening over +IPv4 only: + +- `--listen-addresses :: --port 9909` will listen over IPv6 using port `9909` for +TCP and UDP. +- `--listen-addresses :: --port 9909 --discovery-port 9999` will listen over + IPv6 using port `9909` for TCP and port `9999` for UDP. + +To listen over both IPv4 and IPv6: +- Set two listening addresses using the `--listen-addresses` flag twice ensuring + the two addresses are one IPv4, and the other IPv6. When doing so, the + `--port` and `--discovery-port` flags will apply exclusively to IPv4. Note + that this behaviour differs from the Ipv6 only case described above. +- If necessary, set the `--port6` flag to configure the port used for TCP and + UDP over IPv6. This flag has no effect when listening over IPv6 only. +- If necessary, set the `--discovery-port6` flag to configure the IPv6 UDP + port. This will default to the value given to `--port6` if not set. This flag + has no effect when listening over IPv6 only. + +##### Configuration Examples + +- `--listen-addresses :: --listen-addresses 0.0.0.0 --port 9909` will listen + over IPv4 using port `9909` for TCP and UDP. It will also listen over IPv6 but + using the default value for `--port6` for UDP and TCP (`9090`). +- `--listen-addresses :: --listen-addresses --port 9909 --discovery-port6 9999` + will have the same configuration as before except for the IPv6 UDP socket, + which will use port `9999`. + +#### Configuring Lighthouse to advertise IPv6 reachable addresses +Lighthouse supports IPv6 to connect to other nodes both over IPv6 exclusively, +and dual stack using one socket for IPv6 and another socket for IPv6. In both +scenarios, the previous sections still apply. In summary: + +> Beacon nodes must advertise their publicly reachable socket address + +In order to do so, lighthouse provides the following CLI options/parameters. + +- `--enr-udp-port` Use this to advertise the port that is publicly reachable + over UDP with a publicly reachable IPv4 address. This might differ from the + IPv4 port used to listen. +- `--enr-udp6-port` Use this to advertise the port that is publicly reachable + over UDP with a publicly reachable IPv6 address. This might differ from the + IPv6 port used to listen. +- `--enr-tcp-port` Use this to advertise the port that is publicly reachable + over TCP with a publicly reachable IPv4 address. This might differ from the + IPv4 port used to listen. +- `--enr-tcp6-port` Use this to advertise the port that is publicly reachable + over TCP with a publicly reachable IPv6 address. This might differ from the + IPv6 port used to listen. +- `--enr-addresses` Use this to advertise publicly reachable addresses. Takes at + most two values, one for IPv4 and one for IPv6. Note that a beacon node that + advertises some address, must be + reachable both over UDP and TCP. + +In the general case, an user will not require to set these explicitly. Update +these options only if you can guarantee your node is reachable with these +values. + +#### Known caveats + +IPv6 link local addresses are likely to have poor connectivity if used in +topologies with more than one interface. Use global addresses for the general +case. diff --git a/boot_node/src/cli.rs b/boot_node/src/cli.rs index c3d7ac48a..b13f47f75 100644 --- a/boot_node/src/cli.rs +++ b/boot_node/src/cli.rs @@ -13,13 +13,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .settings(&[clap::AppSettings::ColoredHelp]) .arg( Arg::with_name("enr-address") - .value_name("IP-ADDRESS") - .help("The external IP address/ DNS address to broadcast to other peers on how to reach this node. \ - If a DNS address is provided, the enr-address is set to the IP address it resolves to and \ - does not auto-update based on PONG responses in discovery.") + .long("enr-address") + .value_name("ADDRESS") + .help("The IP address/ DNS address to broadcast to other peers on how to reach \ + this node. If a DNS address is provided, the enr-address is set to the IP \ + address it resolves to and does not auto-update based on PONG responses in \ + discovery. Set this only if you are sure other nodes can connect to your \ + local node on this address. This will update the `ip4` or `ip6` ENR fields \ + accordingly. To update both, set this flag twice with the different values.") + .multiple(true) + .max_values(2) .required(true) - .takes_value(true) .conflicts_with("network-dir") + .takes_value(true), ) .arg( Arg::with_name("port") @@ -29,11 +35,29 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("9000") .takes_value(true) ) + .arg( + Arg::with_name("port6") + .long("port6") + .value_name("PORT") + .help("The UDP port to listen on over IpV6 when listening over both Ipv4 and \ + Ipv6. Defaults to 9090 when required.") + .default_value("9090") + .takes_value(true), + ) .arg( Arg::with_name("listen-address") .long("listen-address") .value_name("ADDRESS") - .help("The address the bootnode will listen for UDP connections.") + .help("The address the bootnode will listen for UDP communications. To listen \ + 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(true) + .max_values(2) .default_value("0.0.0.0") .takes_value(true) ) @@ -59,6 +83,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("PORT") .help("The UDP6 port of the local ENR. Set this only if you are sure other nodes \ can connect to your local node on this port over IpV6.") + .conflicts_with("network-dir") .takes_value(true), ) .arg( diff --git a/boot_node/src/config.rs b/boot_node/src/config.rs index d3ee58a90..c4e36022a 100644 --- a/boot_node/src/config.rs +++ b/boot_node/src/config.rs @@ -2,7 +2,6 @@ use beacon_node::{get_data_dir, set_network_config}; use clap::ArgMatches; use eth2_network_config::Eth2NetworkConfig; use lighthouse_network::discovery::create_enr_builder_from_config; -use lighthouse_network::discv5::IpMode; use lighthouse_network::discv5::{enr::CombinedKey, Discv5Config, Enr}; use lighthouse_network::{ discovery::{load_enr_from_disk, use_or_load_enr}, @@ -10,13 +9,12 @@ use lighthouse_network::{ }; use serde_derive::{Deserialize, Serialize}; use ssz::Encode; -use std::net::SocketAddr; +use std::net::{SocketAddrV4, SocketAddrV6}; use std::{marker::PhantomData, path::PathBuf}; use types::EthSpec; /// A set of configuration parameters for the bootnode, established from CLI arguments. pub struct BootNodeConfig { - pub listen_socket: SocketAddr, // TODO: Generalise to multiaddr pub boot_nodes: Vec, pub local_enr: Enr, @@ -81,31 +79,6 @@ impl BootNodeConfig { network_config.discv5_config.enr_update = false; } - // the address to listen on - let listen_socket = match network_config.listen_addrs().clone() { - lighthouse_network::ListenAddress::V4(v4_addr) => { - // Set explicitly as ipv4 otherwise - network_config.discv5_config.ip_mode = IpMode::Ip4; - v4_addr.udp_socket_addr() - } - lighthouse_network::ListenAddress::V6(v6_addr) => { - // create ipv6 sockets and enable ipv4 mapped addresses. - network_config.discv5_config.ip_mode = IpMode::Ip6 { - enable_mapped_addresses: false, - }; - - v6_addr.udp_socket_addr() - } - lighthouse_network::ListenAddress::DualStack(_v4_addr, v6_addr) => { - // create ipv6 sockets and enable ipv4 mapped addresses. - network_config.discv5_config.ip_mode = IpMode::Ip6 { - enable_mapped_addresses: true, - }; - - v6_addr.udp_socket_addr() - } - }; - let private_key = load_private_key(&network_config, &logger); let local_key = CombinedKey::from_libp2p(&private_key)?; @@ -143,7 +116,7 @@ impl BootNodeConfig { let mut builder = create_enr_builder_from_config(&network_config, enable_tcp); // If we know of the ENR field, add it to the initial construction if let Some(enr_fork_bytes) = enr_fork { - builder.add_value("eth2", enr_fork_bytes.as_slice()); + builder.add_value("eth2", &enr_fork_bytes); } builder .build(&local_key) @@ -155,7 +128,6 @@ impl BootNodeConfig { }; Ok(BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key, @@ -170,7 +142,8 @@ impl BootNodeConfig { /// Its fields are a subset of the fields of `BootNodeConfig`, some of them are copied from `Discv5Config`. #[derive(Serialize, Deserialize)] pub struct BootNodeConfigSerialization { - pub listen_socket: SocketAddr, + pub ipv4_listen_socket: Option, + pub ipv6_listen_socket: Option, // TODO: Generalise to multiaddr pub boot_nodes: Vec, pub local_enr: Enr, @@ -183,7 +156,6 @@ impl BootNodeConfigSerialization { /// relevant fields of `config` pub fn from_config_ref(config: &BootNodeConfig) -> Self { let BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key: _, @@ -191,8 +163,27 @@ impl BootNodeConfigSerialization { phantom: _, } = config; + let (ipv4_listen_socket, ipv6_listen_socket) = match discv5_config.listen_config { + lighthouse_network::discv5::ListenConfig::Ipv4 { ip, port } => { + (Some(SocketAddrV4::new(ip, port)), None) + } + lighthouse_network::discv5::ListenConfig::Ipv6 { ip, port } => { + (None, Some(SocketAddrV6::new(ip, port, 0, 0))) + } + lighthouse_network::discv5::ListenConfig::DualStack { + ipv4, + ipv4_port, + ipv6, + ipv6_port, + } => ( + Some(SocketAddrV4::new(ipv4, ipv4_port)), + Some(SocketAddrV6::new(ipv6, ipv6_port, 0, 0)), + ), + }; + BootNodeConfigSerialization { - listen_socket: *listen_socket, + ipv4_listen_socket, + ipv6_listen_socket, boot_nodes: boot_nodes.clone(), local_enr: local_enr.clone(), disable_packet_filter: !discv5_config.enable_packet_filter, diff --git a/boot_node/src/server.rs b/boot_node/src/server.rs index 3f5419c2c..3823b2872 100644 --- a/boot_node/src/server.rs +++ b/boot_node/src/server.rs @@ -10,7 +10,6 @@ use types::EthSpec; pub async fn run(config: BootNodeConfig, log: slog::Logger) { let BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key, @@ -31,7 +30,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { let pretty_v6_socket = enr_v6_socket.as_ref().map(|addr| addr.to_string()); info!( log, "Configuration parameters"; - "listening_address" => %listen_socket, + "listening_address" => ?discv5_config.listen_config, "advertised_v4_address" => ?pretty_v4_socket, "advertised_v6_address" => ?pretty_v6_socket, "eth2" => eth2_field @@ -41,6 +40,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { // build the contactable multiaddr list, adding the p2p protocol info!(log, "Contact information"; "enr" => local_enr.to_base64()); + info!(log, "Enr details"; "enr" => ?local_enr); info!(log, "Contact information"; "multiaddrs" => ?local_enr.multiaddr_p2p()); // construct the discv5 server @@ -64,7 +64,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { } // start the server - if let Err(e) = discv5.start(listen_socket).await { + if let Err(e) = discv5.start().await { slog::crit!(log, "Could not start discv5 server"; "error" => %e); return; } diff --git a/common/eth2_network_config/Cargo.toml b/common/eth2_network_config/Cargo.toml index f8382c95d..296d43b1a 100644 --- a/common/eth2_network_config/Cargo.toml +++ b/common/eth2_network_config/Cargo.toml @@ -18,4 +18,4 @@ serde_yaml = "0.8.13" types = { path = "../../consensus/types"} ethereum_ssz = "0.5.0" eth2_config = { path = "../eth2_config"} -discv5 = "0.2.2" +discv5 = "0.3.0" \ No newline at end of file diff --git a/lighthouse/tests/boot_node.rs b/lighthouse/tests/boot_node.rs index 4dd5ad95d..659dea468 100644 --- a/lighthouse/tests/boot_node.rs +++ b/lighthouse/tests/boot_node.rs @@ -39,7 +39,7 @@ impl CommandLineTest { } fn run_with_ip(&mut self) -> CompletedTest { - self.cmd.arg(IP_ADDRESS); + self.cmd.arg("--enr-address").arg(IP_ADDRESS); self.run() } } @@ -67,7 +67,13 @@ fn port_flag() { .flag("port", Some(port.to_string().as_str())) .run_with_ip() .with_config(|config| { - assert_eq!(config.listen_socket.port(), port); + assert_eq!( + config + .ipv4_listen_socket + .expect("Bootnode should be listening on IPv4") + .port(), + port + ); }) } @@ -78,7 +84,13 @@ fn listen_address_flag() { .flag("listen-address", Some("127.0.0.2")) .run_with_ip() .with_config(|config| { - assert_eq!(config.listen_socket.ip(), addr); + assert_eq!( + config + .ipv4_listen_socket + .expect("Bootnode should be listening on IPv4") + .ip(), + &addr + ); }); } From a227ee7478a124ae4fe0d88e8f070ed51a26063c Mon Sep 17 00:00:00 2001 From: AMIT SINGH Date: Tue, 13 Jun 2023 10:25:27 +0000 Subject: [PATCH 02/25] Use MediaType accept header parser (#4216) ## Issue Addressed #3510 ## Proposed Changes - Replace mime with MediaTypeList - Remove parse_accept fn as MediaTypeList does it built-in - Get the supported media type of the highest q-factor in a single list iteration without sorting ## Additional Info I have addressed the suggested changes in previous [PR](https://github.com/sigp/lighthouse/pull/3520#discussion_r959048633) --- Cargo.lock | 8 +++- common/eth2/Cargo.toml | 12 ++++-- common/eth2/src/types.rs | 83 ++++++++++++++++++++++++++-------------- 3 files changed, 71 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f53171aaf..18276b3ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2290,7 +2290,7 @@ dependencies = [ "futures-util", "libsecp256k1", "lighthouse_network", - "mime", + "mediatype", "procinfo", "proto_array", "psutil", @@ -4962,6 +4962,12 @@ dependencies = [ "libc", ] +[[package]] +name = "mediatype" +version = "0.19.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea6e62614ab2fc0faa58bb15102a0382d368f896a9fa4776592589ab55c4de7" + [[package]] name = "memchr" version = "2.5.0" diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 2c5e7060b..4eabd3ff8 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" serde = { version = "1.0.116", features = ["derive"] } serde_json = "1.0.58" types = { path = "../../consensus/types" } -reqwest = { version = "0.11.0", features = ["json","stream"] } +reqwest = { version = "0.11.0", features = ["json", "stream"] } lighthouse_network = { path = "../../beacon_node/lighthouse_network" } proto_array = { path = "../../consensus/proto_array", optional = true } ethereum_serde_utils = "0.5.0" @@ -26,7 +26,7 @@ futures-util = "0.3.8" futures = "0.3.8" store = { path = "../../beacon_node/store", optional = true } slashing_protection = { path = "../../validator_client/slashing_protection", optional = true } -mime = "0.3.16" +mediatype = "0.19.13" [target.'cfg(target_os = "linux")'.dependencies] psutil = { version = "3.2.2", optional = true } @@ -34,4 +34,10 @@ procinfo = { version = "0.4.2", optional = true } [features] default = ["lighthouse"] -lighthouse = ["proto_array", "psutil", "procinfo", "store", "slashing_protection"] +lighthouse = [ + "proto_array", + "psutil", + "procinfo", + "store", + "slashing_protection", +] diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index d7150bff7..55759a2e1 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -3,9 +3,8 @@ use crate::Error as ServerError; use lighthouse_network::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStatus}; -use mime::{Mime, APPLICATION, JSON, OCTET_STREAM, STAR}; +use mediatype::{names, MediaType, MediaTypeList}; use serde::{Deserialize, Serialize}; -use std::cmp::Reverse; use std::convert::TryFrom; use std::fmt; use std::str::{from_utf8, FromStr}; @@ -1172,35 +1171,58 @@ impl FromStr for Accept { type Err = String; fn from_str(s: &str) -> Result { - let mut mimes = parse_accept(s)?; + let media_type_list = MediaTypeList::new(s); // [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 // find the highest q-factor supported accept type - mimes.sort_by_key(|m| { - Reverse(m.get_param("q").map_or(1000_u16, |n| { - (n.as_ref().parse::().unwrap_or(0_f32) * 1000_f32) as u16 - })) - }); - mimes - .into_iter() - .find_map(|m| match (m.type_(), m.subtype()) { - (APPLICATION, OCTET_STREAM) => Some(Accept::Ssz), - (APPLICATION, JSON) => Some(Accept::Json), - (STAR, STAR) => Some(Accept::Any), - _ => None, - }) - .ok_or_else(|| "accept header is not supported".to_string()) - } -} + let mut highest_q = 0_u16; + let mut accept_type = None; -fn parse_accept(accept: &str) -> Result, String> { - accept - .split(',') - .map(|part| { - part.parse() - .map_err(|e| format!("error parsing Accept header: {}", e)) - }) - .collect() + const APPLICATION: &str = names::APPLICATION.as_str(); + const OCTET_STREAM: &str = names::OCTET_STREAM.as_str(); + const JSON: &str = names::JSON.as_str(); + const STAR: &str = names::_STAR.as_str(); + const Q: &str = names::Q.as_str(); + + media_type_list.into_iter().for_each(|item| { + if let Ok(MediaType { + ty, + subty, + suffix: _, + params, + }) = item + { + let q_accept = match (ty.as_str(), subty.as_str()) { + (APPLICATION, OCTET_STREAM) => Some(Accept::Ssz), + (APPLICATION, JSON) => Some(Accept::Json), + (STAR, STAR) => Some(Accept::Any), + _ => None, + } + .map(|item_accept_type| { + let q_val = params + .iter() + .find_map(|(n, v)| match n.as_str() { + Q => { + Some((v.as_str().parse::().unwrap_or(0_f32) * 1000_f32) as u16) + } + _ => None, + }) + .or(Some(1000_u16)); + + (q_val.unwrap(), item_accept_type) + }); + + match q_accept { + Some((q, accept)) if q > highest_q => { + highest_q = q; + accept_type = Some(accept); + } + _ => (), + } + } + }); + accept_type.ok_or_else(|| "accept header is not supported".to_string()) + } } #[derive(Debug, Serialize, Deserialize)] @@ -1268,6 +1290,11 @@ mod tests { assert_eq!( Accept::from_str("text/plain"), Err("accept header is not supported".to_string()) - ) + ); + + assert_eq!( + Accept::from_str("application/json;message=\"Hello, world!\";q=0.3,*/*;q=0.6").unwrap(), + Accept::Any + ); } } From 2548be3e661f8f930b46a796c1707fc1bf48c06b Mon Sep 17 00:00:00 2001 From: chonghe Date: Tue, 13 Jun 2023 13:12:56 +0000 Subject: [PATCH 03/25] Minor revision in Lighthouse book (#4385) ## Proposed Changes Correct some typos in the book, also update information about withdrawals since the Mainnet will be having 700K validators in about a month --- book/src/LaTeX/full-withdrawal.tex | 2 +- book/src/LaTeX/partial-withdrawal.tex | 2 +- book/src/SUMMARY.md | 2 +- book/src/advanced_database.md | 2 +- book/src/builders.md | 3 ++- book/src/imgs/full-withdrawal.png | Bin 263209 -> 263064 bytes book/src/imgs/partial-withdrawal.png | Bin 175823 -> 175937 bytes book/src/partial-withdrawal.md | 2 +- book/src/voluntary-exit.md | 20 +++++++++++++++++++- 9 files changed, 26 insertions(+), 7 deletions(-) diff --git a/book/src/LaTeX/full-withdrawal.tex b/book/src/LaTeX/full-withdrawal.tex index 2447ba097..a4b384872 100644 --- a/book/src/LaTeX/full-withdrawal.tex +++ b/book/src/LaTeX/full-withdrawal.tex @@ -37,7 +37,7 @@ \rput[bl](9.0,-3.49){27.3 hours} \rput[bl](8.8,-5.49){Varying time} \rput[bl](8.7,-5.99){validator sweep} - \rput[bl](8.9,-6.59){up to 5 days} + \rput[bl](8.9,-6.59){up to \textit{n} days} \psframe[linecolor=black, linewidth=0.04, dimen=outer](11.6,-2.19)(8.0,-3.89) \psframe[linecolor=black, linewidth=0.04, dimen=outer](11.7,-4.79)(7.9,-6.89) \psframe[linecolor=black, linewidth=0.04, dimen=outer](3.7,-2.49)(0.0,-4.29) diff --git a/book/src/LaTeX/partial-withdrawal.tex b/book/src/LaTeX/partial-withdrawal.tex index 05db3b688..4d1d0b5f0 100644 --- a/book/src/LaTeX/partial-withdrawal.tex +++ b/book/src/LaTeX/partial-withdrawal.tex @@ -31,7 +31,7 @@ \rput[bl](0.9,-1.59){Beacon chain} \psframe[linecolor=black, linewidth=0.04, dimen=outer](10.7,-3.29)(6.8,-5.09) \rput[bl](7.6,-3.99){validator sweep} - \rput[bl](7.5,-4.69){$\sim$ every 5 days} + \rput[bl](7.82,-4.73){every \textit{n} days} \psframe[linecolor=black, linewidth=0.04, dimen=outer](3.7,-3.29)(0.0,-5.09) \rput[bl](1.3,-4.09){BLS to} \rput[bl](0.5,-4.69){execution change} diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8fc2c2f83..7431d2238 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -47,7 +47,7 @@ * [Running a Slasher](./slasher.md) * [Redundancy](./redundancy.md) * [Release Candidates](./advanced-release-candidates.md) - * [Maximal Extractable Value (MEV)](./builders.md) + * [MEV](./builders.md) * [Merge Migration](./merge-migration.md) * [Late Block Re-orgs](./late-block-re-orgs.md) * [Contributing](./contributing.md) diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index f9996ec65..d95110405 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -28,7 +28,7 @@ some example values. | Research | 32 | 3.4 TB | 155 ms | | Block explorer/analysis | 128 | 851 GB | 620 ms | | Enthusiast (prev. default) | 2048 | 53.6 GB | 10.2 s | -| EHobbyist | 4096 | 26.8 GB | 20.5 s | +| Hobbyist | 4096 | 26.8 GB | 20.5 s | | Validator only (default) | 8192 | 8.1 GB | 41 s | *Last update: May 2023. diff --git a/book/src/builders.md b/book/src/builders.md index 8d727ef2c..6db360d70 100644 --- a/book/src/builders.md +++ b/book/src/builders.md @@ -108,13 +108,14 @@ Command: ```bash DATADIR=/var/lib/lighthouse curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ --H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)" \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ -H "Content-Type: application/json" \ -d '{ "builder_proposals": true, "gas_limit": 30000001 }' | jq ``` +If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"` #### Example Response Body diff --git a/book/src/imgs/full-withdrawal.png b/book/src/imgs/full-withdrawal.png index 6fa2db6a9137da077a72c658bb8f8ca514bcf0f6..c16d8352696cc399d6368bf4e79ac42bb0f41dea 100644 GIT binary patch delta 47018 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

Z$lE6fdjEdCpu|t-G7ONz(3T& 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 E35HTH8~^|S diff --git a/book/src/imgs/partial-withdrawal.png b/book/src/imgs/partial-withdrawal.png index 0bf90b91db0270dc8d7a0618905f21301728a5d3..5d318b4e62e0663a51718fe8070ca15d1111efb2 100644 GIT binary patch delta 61109 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

hUK8mSz& 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 delta 60770 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

  • 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 3. Do I have to do anything to get my rewards after I update the withdrawal credentials to type `0x01`? - No. The "validator sweep" occurs automatically and you can expect to receive the rewards every few days. + No. The "validator sweep" occurs automatically and you can expect to receive the rewards every *n* days, [more information here](./voluntary-exit.md#4-when-will-i-get-my-staked-fund-after-voluntary-exit-if-my-validator-is-of-type-0x01). Figure below summarizes partial withdrawals. diff --git a/book/src/voluntary-exit.md b/book/src/voluntary-exit.md index d90395c07..d298d13f2 100644 --- a/book/src/voluntary-exit.md +++ b/book/src/voluntary-exit.md @@ -97,7 +97,25 @@ There are two types of withdrawal credentials, `0x00` and `0x01`. To check which - A fixed waiting period of 256 epochs (27.3 hours) for the validator's status to become withdrawable. - - A varying time of "validator sweep" that can take up to 5 days (at the time of writing with ~560,000 validators on the mainnet). The "validator sweep" is the process of skimming through all validators by index number for eligible withdrawals (those with type `0x01` and balance above 32ETH). Once the "validator sweep" reaches your validator's index, your staked fund will be fully withdrawn to the withdrawal address set. + - A varying time of "validator sweep" that can take up to *n* days with *n* listed in the table below. The "validator sweep" is the process of skimming through all eligible validators by index number for withdrawals (those with type `0x01` and balance above 32ETH). Once the "validator sweep" reaches your validator's index, your staked fund will be fully withdrawn to the withdrawal address set. + +
    + + | Number of eligible validators | Ideal scenario *n* | Practical scenario *n* | + |-------------------------------|--------------------| ---------------------- | + | 300000 | 2.60 | 2.63 | + | 400000 | 3.47 | 3.51 | + | 500000 | 4.34 | 4.38 | + | 600000 | 5.21 | 5.26 | + | 700000 | 6.08 | 6.14 | + | 800000 | 6.94 | 7.01 | + | 900000 | 7.81 | 7.89 | + | 1000000 | 8.68 | 8.77 | +
    + +> Note: Ideal scenario assumes no block proposals are missed. This means a total of withdrawals of 7200 blocks/day * 16 withdrawals/block = 115200 withdrawals/day. Practical scenario assumes 1% of blocks are missed per day. As an example, if there are 700000 eligible validators, one would expect a waiting time of slightly more than 6 days. + + The total time taken is the summation of the above 3 waiting periods. After these waiting periods, you will receive the staked funds in your withdrawal address. From 0caaad4c0346f8c1235fbfe43886f4a1a836ef1d Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 14 Jun 2023 02:29:51 +0000 Subject: [PATCH 04/25] Re-enable maxperf for Windows releases (#4371) ## Issue Addressed Closes #3964 ## Proposed Changes Use the `maxperf` profile to build Windows binaries, now that Rust 1.70.0 fixed the underlying issue. --- .github/workflows/release.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6d79bd5e..814218441 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,17 +134,11 @@ jobs: - name: Build Lighthouse for Windows portable if: matrix.arch == 'x86_64-windows-portable' - # NOTE: profile set to release until this rustc issue is fixed: - # - # https://github.com/rust-lang/rust/issues/107781 - # - # tracked at: https://github.com/sigp/lighthouse/issues/3964 - run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile release + run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} - name: Build Lighthouse for Windows modern if: matrix.arch == 'x86_64-windows' - # NOTE: profile set to release (see above) - run: cargo install --path lighthouse --force --locked --features modern,gnosis --profile release + run: cargo install --path lighthouse --force --locked --features modern,gnosis --profile ${{ matrix.profile }} - name: Configure GPG and create artifacts if: startsWith(matrix.arch, 'x86_64-windows') != true From 0ecca1dcb086a440ac4663e3d7aceb74e4101959 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 14 Jun 2023 05:08:50 +0000 Subject: [PATCH 05/25] Rework internal rpc protocol handling (#4290) ## Issue Addressed Resolves #3980. Builds on work by @GeemoCandama in #4084 ## Proposed Changes Extends the `SupportedProtocol` abstraction added in Geemo's PR and attempts to fix internal versioning of requests that are mentioned in this comment https://github.com/sigp/lighthouse/pull/4084#issuecomment-1496380033 Co-authored-by: geemo --- .../lighthouse_network/src/rpc/codec/base.rs | 9 +- .../src/rpc/codec/ssz_snappy.rs | 591 +++++++----------- .../lighthouse_network/src/rpc/handler.rs | 14 +- .../lighthouse_network/src/rpc/methods.rs | 125 +++- beacon_node/lighthouse_network/src/rpc/mod.rs | 4 +- .../lighthouse_network/src/rpc/outbound.rs | 68 +- .../lighthouse_network/src/rpc/protocol.rs | 187 +++--- .../src/rpc/rate_limiter.rs | 4 +- .../src/rpc/self_limiter.rs | 8 +- .../src/service/api_types.rs | 28 +- .../lighthouse_network/src/service/mod.rs | 49 +- .../lighthouse_network/src/service/utils.rs | 8 +- .../lighthouse_network/tests/rpc_tests.rs | 34 +- .../beacon_processor/worker/rpc_methods.rs | 54 +- .../sync/block_lookups/single_block_lookup.rs | 4 +- .../network/src/sync/network_context.rs | 8 +- .../network/src/sync/range_sync/batch.rs | 8 +- 17 files changed, 619 insertions(+), 584 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/codec/base.rs b/beacon_node/lighthouse_network/src/rpc/codec/base.rs index 6c6ce2da3..d568f2789 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/base.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/base.rs @@ -214,8 +214,7 @@ mod tests { let mut buf = BytesMut::new(); buf.extend_from_slice(&message); - let snappy_protocol_id = - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(SupportedProtocol::StatusV1, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(ForkName::Base)); let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( @@ -249,8 +248,7 @@ mod tests { // Insert length-prefix uvi_codec.encode(len, &mut dst).unwrap(); - let snappy_protocol_id = - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(SupportedProtocol::StatusV1, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(ForkName::Base)); let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( @@ -277,8 +275,7 @@ mod tests { dst } - let protocol_id = - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy); + let protocol_id = ProtocolId::new(SupportedProtocol::BlocksByRangeV1, Encoding::SSZSnappy); // Response limits let fork_context = Arc::new(fork_context(ForkName::Base)); diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index 28fea40a2..39cf8b3eb 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -1,9 +1,9 @@ +use crate::rpc::methods::*; use crate::rpc::{ codec::base::OutboundCodec, - protocol::{Encoding, Protocol, ProtocolId, RPCError, Version, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, + protocol::{Encoding, ProtocolId, RPCError, SupportedProtocol, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, }; use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse, RPCResponse}; -use crate::{rpc::methods::*, EnrSyncCommitteeBitfield}; use libp2p::bytes::BytesMut; use snap::read::FrameDecoder; use snap::write::FrameEncoder; @@ -76,27 +76,14 @@ impl Encoder> for SSZSnappyInboundCodec< RPCResponse::MetaData(res) => // Encode the correct version of the MetaData response based on the negotiated version. { - match self.protocol.version { - Version::V1 => MetaData::::V1(MetaDataV1 { - seq_number: *res.seq_number(), - attnets: res.attnets().clone(), - }) - .as_ssz_bytes(), - Version::V2 => { - // `res` is of type MetaDataV2, return the ssz bytes - if res.syncnets().is_ok() { - res.as_ssz_bytes() - } else { - // `res` is of type MetaDataV1, create a MetaDataV2 by adding a default syncnets field - // Note: This code path is redundant as `res` would be always of type MetaDataV2 - MetaData::::V2(MetaDataV2 { - seq_number: *res.seq_number(), - attnets: res.attnets().clone(), - syncnets: EnrSyncCommitteeBitfield::::default(), - }) - .as_ssz_bytes() - } - } + match self.protocol.versioned_protocol { + SupportedProtocol::MetaDataV1 => res.metadata_v1().as_ssz_bytes(), + // We always send V2 metadata responses from the behaviour + // No change required. + SupportedProtocol::MetaDataV2 => res.metadata_v2().as_ssz_bytes(), + _ => unreachable!( + "We only send metadata responses on negotiating metadata requests" + ), } } }, @@ -139,8 +126,11 @@ impl Decoder for SSZSnappyInboundCodec { type Error = RPCError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if self.protocol.message_name == Protocol::MetaData { - return Ok(Some(InboundRequest::MetaData(PhantomData))); + if self.protocol.versioned_protocol == SupportedProtocol::MetaDataV1 { + return Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v1()))); + } + if self.protocol.versioned_protocol == SupportedProtocol::MetaDataV2 { + return Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v2()))); } let length = match handle_length(&mut self.inner, &mut self.len, src)? { Some(len) => len, @@ -152,8 +142,8 @@ impl Decoder for SSZSnappyInboundCodec { let ssz_limits = self.protocol.rpc_request_limits(); if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { return Err(RPCError::InvalidData(format!( - "RPC request length is out of bounds, length {}", - length + "RPC request length for protocol {:?} is out of bounds, length {}", + self.protocol.versioned_protocol, length ))); } // Calculate worst case compression length for given uncompressed length @@ -170,11 +160,7 @@ impl Decoder for SSZSnappyInboundCodec { let n = reader.get_ref().get_ref().position(); self.len = None; let _read_bytes = src.split_to(n as usize); - - match self.protocol.version { - Version::V1 => handle_v1_request(self.protocol.message_name, &decoded_buffer), - Version::V2 => handle_v2_request(self.protocol.message_name, &decoded_buffer), - } + handle_rpc_request(self.protocol.versioned_protocol, &decoded_buffer) } Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), } @@ -228,11 +214,16 @@ impl Encoder> for SSZSnappyOutboundCodec< let bytes = match item { OutboundRequest::Status(req) => req.as_ssz_bytes(), OutboundRequest::Goodbye(req) => req.as_ssz_bytes(), - OutboundRequest::BlocksByRange(req) => req.as_ssz_bytes(), - OutboundRequest::BlocksByRoot(req) => req.block_roots.as_ssz_bytes(), + OutboundRequest::BlocksByRange(r) => match r { + OldBlocksByRangeRequest::V1(req) => req.as_ssz_bytes(), + OldBlocksByRangeRequest::V2(req) => req.as_ssz_bytes(), + }, + OutboundRequest::BlocksByRoot(r) => match r { + BlocksByRootRequest::V1(req) => req.block_roots.as_ssz_bytes(), + BlocksByRootRequest::V2(req) => req.block_roots.as_ssz_bytes(), + }, OutboundRequest::Ping(req) => req.as_ssz_bytes(), OutboundRequest::MetaData(_) => return Ok(()), // no metadata to encode - OutboundRequest::LightClientBootstrap(req) => req.as_ssz_bytes(), }; // SSZ encoded bytes should be within `max_packet_size` if bytes.len() > self.max_packet_size { @@ -311,15 +302,10 @@ impl Decoder for SSZSnappyOutboundCodec { let n = reader.get_ref().get_ref().position(); self.len = None; let _read_bytes = src.split_to(n as usize); - - match self.protocol.version { - Version::V1 => handle_v1_response(self.protocol.message_name, &decoded_buffer), - Version::V2 => handle_v2_response( - self.protocol.message_name, - &decoded_buffer, - &mut self.fork_name, - ), - } + // Safe to `take` from `self.fork_name` as we have all the bytes we need to + // decode an ssz object at this point. + let fork_name = self.fork_name.take(); + handle_rpc_response(self.protocol.versioned_protocol, &decoded_buffer, fork_name) } Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), } @@ -456,181 +442,150 @@ fn handle_length( } } -/// Decodes a `Version::V1` `InboundRequest` from the byte stream. +/// Decodes an `InboundRequest` from the byte stream. /// `decoded_buffer` should be an ssz-encoded bytestream with // length = length-prefix received in the beginning of the stream. -fn handle_v1_request( - protocol: Protocol, +fn handle_rpc_request( + versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], ) -> Result>, RPCError> { - match protocol { - Protocol::Status => Ok(Some(InboundRequest::Status(StatusMessage::from_ssz_bytes( - decoded_buffer, - )?))), - Protocol::Goodbye => Ok(Some(InboundRequest::Goodbye( + match versioned_protocol { + SupportedProtocol::StatusV1 => Ok(Some(InboundRequest::Status( + StatusMessage::from_ssz_bytes(decoded_buffer)?, + ))), + SupportedProtocol::GoodbyeV1 => Ok(Some(InboundRequest::Goodbye( GoodbyeReason::from_ssz_bytes(decoded_buffer)?, ))), - Protocol::BlocksByRange => Ok(Some(InboundRequest::BlocksByRange( - OldBlocksByRangeRequest::from_ssz_bytes(decoded_buffer)?, + SupportedProtocol::BlocksByRangeV2 => Ok(Some(InboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V2(OldBlocksByRangeRequestV2::from_ssz_bytes(decoded_buffer)?), ))), - Protocol::BlocksByRoot => Ok(Some(InboundRequest::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, - }))), - Protocol::Ping => Ok(Some(InboundRequest::Ping(Ping { + SupportedProtocol::BlocksByRangeV1 => Ok(Some(InboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V1(OldBlocksByRangeRequestV1::from_ssz_bytes(decoded_buffer)?), + ))), + SupportedProtocol::BlocksByRootV2 => Ok(Some(InboundRequest::BlocksByRoot( + BlocksByRootRequest::V2(BlocksByRootRequestV2 { + block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, + }), + ))), + SupportedProtocol::BlocksByRootV1 => Ok(Some(InboundRequest::BlocksByRoot( + BlocksByRootRequest::V1(BlocksByRootRequestV1 { + block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, + }), + ))), + SupportedProtocol::PingV1 => Ok(Some(InboundRequest::Ping(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), - Protocol::LightClientBootstrap => Ok(Some(InboundRequest::LightClientBootstrap( - LightClientBootstrapRequest { + SupportedProtocol::LightClientBootstrapV1 => Ok(Some( + InboundRequest::LightClientBootstrap(LightClientBootstrapRequest { root: Hash256::from_ssz_bytes(decoded_buffer)?, - }, - ))), + }), + )), // MetaData requests return early from InboundUpgrade and do not reach the decoder. // Handle this case just for completeness. - Protocol::MetaData => { + SupportedProtocol::MetaDataV2 => { if !decoded_buffer.is_empty() { Err(RPCError::InternalError( "Metadata requests shouldn't reach decoder", )) } else { - Ok(Some(InboundRequest::MetaData(PhantomData))) + Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v2()))) } } - } -} - -/// Decodes a `Version::V2` `InboundRequest` from the byte stream. -/// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. -fn handle_v2_request( - protocol: Protocol, - decoded_buffer: &[u8], -) -> Result>, RPCError> { - match protocol { - Protocol::BlocksByRange => Ok(Some(InboundRequest::BlocksByRange( - OldBlocksByRangeRequest::from_ssz_bytes(decoded_buffer)?, - ))), - Protocol::BlocksByRoot => Ok(Some(InboundRequest::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, - }))), - // MetaData requests return early from InboundUpgrade and do not reach the decoder. - // Handle this case just for completeness. - Protocol::MetaData => { + SupportedProtocol::MetaDataV1 => { if !decoded_buffer.is_empty() { Err(RPCError::InvalidData("Metadata request".to_string())) } else { - Ok(Some(InboundRequest::MetaData(PhantomData))) + Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v1()))) } } - _ => Err(RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - format!("{} does not support version 2", protocol), - )), } } -/// Decodes a `Version::V1` `RPCResponse` from the byte stream. +/// Decodes a `RPCResponse` from the byte stream. /// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. -fn handle_v1_response( - protocol: Protocol, - decoded_buffer: &[u8], -) -> Result>, RPCError> { - match protocol { - Protocol::Status => Ok(Some(RPCResponse::Status(StatusMessage::from_ssz_bytes( - decoded_buffer, - )?))), - // This case should be unreachable as `Goodbye` has no response. - Protocol::Goodbye => Err(RPCError::InvalidData( - "Goodbye RPC message has no valid response".to_string(), - )), - Protocol::BlocksByRange => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - Protocol::BlocksByRoot => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - Protocol::Ping => Ok(Some(RPCResponse::Pong(Ping { - data: u64::from_ssz_bytes(decoded_buffer)?, - }))), - Protocol::MetaData => Ok(Some(RPCResponse::MetaData(MetaData::V1( - MetaDataV1::from_ssz_bytes(decoded_buffer)?, - )))), - Protocol::LightClientBootstrap => Ok(Some(RPCResponse::LightClientBootstrap( - LightClientBootstrap::from_ssz_bytes(decoded_buffer)?, - ))), - } -} - -/// Decodes a `Version::V2` `RPCResponse` from the byte stream. -/// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. +/// length = length-prefix received in the beginning of the stream. /// /// For BlocksByRange/BlocksByRoot reponses, decodes the appropriate response /// according to the received `ForkName`. -fn handle_v2_response( - protocol: Protocol, +fn handle_rpc_response( + versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], - fork_name: &mut Option, + fork_name: Option, ) -> Result>, RPCError> { - // MetaData does not contain context_bytes - if let Protocol::MetaData = protocol { - Ok(Some(RPCResponse::MetaData(MetaData::V2( + match versioned_protocol { + SupportedProtocol::StatusV1 => Ok(Some(RPCResponse::Status( + StatusMessage::from_ssz_bytes(decoded_buffer)?, + ))), + // This case should be unreachable as `Goodbye` has no response. + SupportedProtocol::GoodbyeV1 => Err(RPCError::InvalidData( + "Goodbye RPC message has no valid response".to_string(), + )), + SupportedProtocol::BlocksByRangeV1 => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + SupportedProtocol::BlocksByRootV1 => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + SupportedProtocol::PingV1 => Ok(Some(RPCResponse::Pong(Ping { + data: u64::from_ssz_bytes(decoded_buffer)?, + }))), + SupportedProtocol::MetaDataV1 => Ok(Some(RPCResponse::MetaData(MetaData::V1( + MetaDataV1::from_ssz_bytes(decoded_buffer)?, + )))), + SupportedProtocol::LightClientBootstrapV1 => Ok(Some(RPCResponse::LightClientBootstrap( + LightClientBootstrap::from_ssz_bytes(decoded_buffer)?, + ))), + // MetaData V2 responses have no context bytes, so behave similarly to V1 responses + SupportedProtocol::MetaDataV2 => Ok(Some(RPCResponse::MetaData(MetaData::V2( MetaDataV2::from_ssz_bytes(decoded_buffer)?, - )))) - } else { - let fork_name = fork_name.take().ok_or_else(|| { - RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - format!("No context bytes provided for {} response", protocol), - ) - })?; - match protocol { - Protocol::BlocksByRange => match fork_name { - ForkName::Altair => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes( - decoded_buffer, - )?), - )))), + )))), + SupportedProtocol::BlocksByRangeV2 => match fork_name { + Some(ForkName::Altair) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes(decoded_buffer)?), + )))), - ForkName::Base => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - ForkName::Merge => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Capella => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( - decoded_buffer, - )?), - )))), - }, - Protocol::BlocksByRoot => match fork_name { - ForkName::Altair => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Base => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - ForkName::Merge => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Capella => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( - decoded_buffer, - )?), - )))), - }, - _ => Err(RPCError::ErrorResponse( + Some(ForkName::Base) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Merge) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Capella) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( + decoded_buffer, + )?), + )))), + None => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, - "Invalid v2 request".to_string(), + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), )), - } + }, + SupportedProtocol::BlocksByRootV2 => match fork_name { + Some(ForkName::Altair) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Base) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Merge) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Capella) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( + decoded_buffer, + )?), + )))), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, } } @@ -742,18 +697,20 @@ mod tests { } } - fn bbrange_request() -> OldBlocksByRangeRequest { - OldBlocksByRangeRequest { - start_slot: 0, - count: 10, - step: 1, - } + fn bbrange_request_v1() -> OldBlocksByRangeRequest { + OldBlocksByRangeRequest::new_v1(0, 10, 1) } - fn bbroot_request() -> BlocksByRootRequest { - BlocksByRootRequest { - block_roots: VariableList::from(vec![Hash256::zero()]), - } + fn bbrange_request_v2() -> OldBlocksByRangeRequest { + OldBlocksByRangeRequest::new(0, 10, 1) + } + + fn bbroot_request_v1() -> BlocksByRootRequest { + BlocksByRootRequest::new_v1(vec![Hash256::zero()].into()) + } + + fn bbroot_request_v2() -> BlocksByRootRequest { + BlocksByRootRequest::new(vec![Hash256::zero()].into()) } fn ping_message() -> Ping { @@ -777,12 +734,11 @@ mod tests { /// Encodes the given protocol response as bytes. fn encode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: RPCCodedResponse, fork_name: ForkName, ) -> Result { - let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(fork_name)); let max_packet_size = max_rpc_size(&fork_context); @@ -824,12 +780,11 @@ mod tests { /// Attempts to decode the given protocol bytes as an rpc response fn decode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: &mut BytesMut, fork_name: ForkName, ) -> Result>, RPCError> { - let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(fork_name)); let max_packet_size = max_rpc_size(&fork_context); let mut snappy_outbound_codec = @@ -840,63 +795,55 @@ mod tests { /// Encodes the provided protocol message as bytes and tries to decode the encoding bytes. fn encode_then_decode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: RPCCodedResponse, fork_name: ForkName, ) -> Result>, RPCError> { - let mut encoded = encode_response(protocol, version.clone(), message, fork_name)?; - decode_response(protocol, version, &mut encoded, fork_name) + let mut encoded = encode_response(protocol, message, fork_name)?; + decode_response(protocol, &mut encoded, fork_name) } /// Verifies that requests we send are encoded in a way that we would correctly decode too. fn encode_then_decode_request(req: OutboundRequest, fork_name: ForkName) { let fork_context = Arc::new(fork_context(fork_name)); let max_packet_size = max_rpc_size(&fork_context); - for protocol in req.supported_protocols() { - // Encode a request we send - let mut buf = BytesMut::new(); - let mut outbound_codec = SSZSnappyOutboundCodec::::new( - protocol.clone(), - max_packet_size, - fork_context.clone(), - ); - outbound_codec.encode(req.clone(), &mut buf).unwrap(); + let protocol = ProtocolId::new(req.versioned_protocol(), Encoding::SSZSnappy); + // Encode a request we send + let mut buf = BytesMut::new(); + let mut outbound_codec = SSZSnappyOutboundCodec::::new( + protocol.clone(), + max_packet_size, + fork_context.clone(), + ); + outbound_codec.encode(req.clone(), &mut buf).unwrap(); - let mut inbound_codec = SSZSnappyInboundCodec::::new( - protocol.clone(), - max_packet_size, - fork_context.clone(), - ); + let mut inbound_codec = + SSZSnappyInboundCodec::::new(protocol.clone(), max_packet_size, fork_context); - let decoded = inbound_codec.decode(&mut buf).unwrap().unwrap_or_else(|| { - panic!( - "Should correctly decode the request {} over protocol {:?} and fork {}", - req, protocol, fork_name - ) - }); - match req.clone() { - OutboundRequest::Status(status) => { - assert_eq!(decoded, InboundRequest::Status(status)) - } - OutboundRequest::Goodbye(goodbye) => { - assert_eq!(decoded, InboundRequest::Goodbye(goodbye)) - } - OutboundRequest::BlocksByRange(bbrange) => { - assert_eq!(decoded, InboundRequest::BlocksByRange(bbrange)) - } - OutboundRequest::BlocksByRoot(bbroot) => { - assert_eq!(decoded, InboundRequest::BlocksByRoot(bbroot)) - } - OutboundRequest::Ping(ping) => { - assert_eq!(decoded, InboundRequest::Ping(ping)) - } - OutboundRequest::MetaData(metadata) => { - assert_eq!(decoded, InboundRequest::MetaData(metadata)) - } - OutboundRequest::LightClientBootstrap(bootstrap) => { - assert_eq!(decoded, InboundRequest::LightClientBootstrap(bootstrap)) - } + let decoded = inbound_codec.decode(&mut buf).unwrap().unwrap_or_else(|| { + panic!( + "Should correctly decode the request {} over protocol {:?} and fork {}", + req, protocol, fork_name + ) + }); + match req { + OutboundRequest::Status(status) => { + assert_eq!(decoded, InboundRequest::Status(status)) + } + OutboundRequest::Goodbye(goodbye) => { + assert_eq!(decoded, InboundRequest::Goodbye(goodbye)) + } + OutboundRequest::BlocksByRange(bbrange) => { + assert_eq!(decoded, InboundRequest::BlocksByRange(bbrange)) + } + OutboundRequest::BlocksByRoot(bbroot) => { + assert_eq!(decoded, InboundRequest::BlocksByRoot(bbroot)) + } + OutboundRequest::Ping(ping) => { + assert_eq!(decoded, InboundRequest::Ping(ping)) + } + OutboundRequest::MetaData(metadata) => { + assert_eq!(decoded, InboundRequest::MetaData(metadata)) } } } @@ -906,8 +853,7 @@ mod tests { fn test_encode_then_decode_v1() { assert_eq!( encode_then_decode_response( - Protocol::Status, - Version::V1, + SupportedProtocol::StatusV1, RPCCodedResponse::Success(RPCResponse::Status(status_message())), ForkName::Base, ), @@ -916,8 +862,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::Ping, - Version::V1, + SupportedProtocol::PingV1, RPCCodedResponse::Success(RPCResponse::Pong(ping_message())), ForkName::Base, ), @@ -926,8 +871,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V1, + SupportedProtocol::BlocksByRangeV1, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, ), @@ -939,8 +883,7 @@ mod tests { assert!( matches!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V1, + SupportedProtocol::BlocksByRangeV1, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(altair_block()))), ForkName::Altair, ) @@ -952,8 +895,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V1, + SupportedProtocol::BlocksByRootV1, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, ), @@ -965,8 +907,7 @@ mod tests { assert!( matches!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V1, + SupportedProtocol::BlocksByRootV1, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, ) @@ -978,18 +919,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V1, - RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), - ForkName::Base, - ), - Ok(Some(RPCResponse::MetaData(metadata()))), - ); - - assert_eq!( - encode_then_decode_response( - Protocol::MetaData, - Version::V1, + SupportedProtocol::MetaDataV1, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Base, ), @@ -999,8 +929,7 @@ mod tests { // A MetaDataV2 still encodes as a MetaDataV1 since version is Version::V1 assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V1, + SupportedProtocol::MetaDataV1, RPCCodedResponse::Success(RPCResponse::MetaData(metadata_v2())), ForkName::Base, ), @@ -1011,38 +940,9 @@ mod tests { // Test RPCResponse encoding/decoding for V1 messages #[test] fn test_encode_then_decode_v2() { - assert!( - matches!( - encode_then_decode_response( - Protocol::Status, - Version::V2, - RPCCodedResponse::Success(RPCResponse::Status(status_message())), - ForkName::Base, - ) - .unwrap_err(), - RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), - ), - "status does not have V2 message" - ); - - assert!( - matches!( - encode_then_decode_response( - Protocol::Ping, - Version::V2, - RPCCodedResponse::Success(RPCResponse::Pong(ping_message())), - ForkName::Base, - ) - .unwrap_err(), - RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), - ), - "ping does not have V2 message" - ); - assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, ), @@ -1056,8 +956,7 @@ mod tests { // the current_fork's rpc limit assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Altair, ), @@ -1068,8 +967,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(altair_block()))), ForkName::Altair, ), @@ -1081,8 +979,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new( merge_block_small.clone() ))), @@ -1100,8 +997,7 @@ mod tests { assert!( matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded, ForkName::Merge, ) @@ -1113,8 +1009,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, ), @@ -1128,8 +1023,7 @@ mod tests { // the current_fork's rpc limit assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, ), @@ -1140,8 +1034,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, ), @@ -1150,8 +1043,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new( merge_block_small.clone() ))), @@ -1167,8 +1059,7 @@ mod tests { assert!( matches!( decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, &mut encoded, ForkName::Merge, ) @@ -1181,8 +1072,7 @@ mod tests { // A MetaDataV1 still encodes as a MetaDataV2 since version is Version::V2 assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Base, ), @@ -1191,8 +1081,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata_v2())), ForkName::Altair, ), @@ -1207,8 +1096,7 @@ mod tests { // Removing context bytes for v2 messages should error let mut encoded_bytes = encode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, ) @@ -1218,8 +1106,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded_bytes, ForkName::Base ) @@ -1228,8 +1115,7 @@ mod tests { )); let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, ) @@ -1239,8 +1125,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded_bytes, ForkName::Base ) @@ -1250,8 +1135,7 @@ mod tests { // Trying to decode a base block with altair context bytes should give ssz decoding error let mut encoded_bytes = encode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Altair, ) @@ -1264,8 +1148,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, ForkName::Altair ) @@ -1275,8 +1158,7 @@ mod tests { // Trying to decode an altair block with base context bytes should give ssz decoding error let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, ) @@ -1288,8 +1170,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, ForkName::Altair ) @@ -1302,8 +1183,7 @@ mod tests { encoded_bytes.extend_from_slice(&fork_context.to_context_bytes(ForkName::Altair).unwrap()); encoded_bytes.extend_from_slice( &encode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Altair, ) @@ -1311,8 +1191,7 @@ mod tests { ); assert!(decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, &mut encoded_bytes, ForkName::Altair ) @@ -1320,8 +1199,7 @@ mod tests { // Sending context bytes which do not correspond to any fork should return an error let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, ) @@ -1333,8 +1211,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, ForkName::Altair ) @@ -1344,8 +1221,7 @@ mod tests { // Sending bytes less than context bytes length should wait for more bytes by returning `Ok(None)` let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, ) @@ -1355,8 +1231,7 @@ mod tests { assert_eq!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut part, ForkName::Altair ), @@ -1370,9 +1245,12 @@ mod tests { OutboundRequest::Ping(ping_message()), OutboundRequest::Status(status_message()), OutboundRequest::Goodbye(GoodbyeReason::Fault), - OutboundRequest::BlocksByRange(bbrange_request()), - OutboundRequest::BlocksByRoot(bbroot_request()), - OutboundRequest::MetaData(PhantomData::), + OutboundRequest::BlocksByRange(bbrange_request_v1()), + OutboundRequest::BlocksByRange(bbrange_request_v2()), + OutboundRequest::BlocksByRoot(bbroot_request_v1()), + OutboundRequest::BlocksByRoot(bbroot_request_v2()), + OutboundRequest::MetaData(MetadataRequest::new_v1()), + OutboundRequest::MetaData(MetadataRequest::new_v2()), ]; for req in requests.iter() { for fork_name in ForkName::list_all() { @@ -1432,7 +1310,7 @@ mod tests { // 10 (for stream identifier) + 80 + 42 = 132 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. assert!(matches!( - decode_response(Protocol::Status, Version::V1, &mut dst, ForkName::Base).unwrap_err(), + decode_response(SupportedProtocol::StatusV1, &mut dst, ForkName::Base).unwrap_err(), RPCError::InvalidData(_) )); } @@ -1490,8 +1368,7 @@ mod tests { // 10 (for stream identifier) + 176156 + 8103 = 184269 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut dst, ForkName::Altair ) @@ -1534,7 +1411,7 @@ mod tests { dst.extend_from_slice(writer.get_ref()); assert!(matches!( - decode_response(Protocol::Status, Version::V1, &mut dst, ForkName::Base).unwrap_err(), + decode_response(SupportedProtocol::StatusV1, &mut dst, ForkName::Base).unwrap_err(), RPCError::InvalidData(_) )); } diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index a1743c15f..8199bee2a 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -245,7 +245,7 @@ where while let Some((id, req)) = self.dial_queue.pop() { self.events_out.push(Err(HandlerErr::Outbound { error: RPCError::Disconnected, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), id, })); } @@ -269,7 +269,7 @@ where } _ => self.events_out.push(Err(HandlerErr::Outbound { error: RPCError::Disconnected, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), id, })), } @@ -334,7 +334,7 @@ where ) { self.dial_negotiated -= 1; let (id, request) = request_info; - let proto = request.protocol(); + let proto = request.versioned_protocol().protocol(); // accept outbound connections only if the handler is not deactivated if matches!(self.state, HandlerState::Deactivated) { @@ -414,7 +414,7 @@ where 128, ) as usize), delay_key: Some(delay_key), - protocol: req.protocol(), + protocol: req.versioned_protocol().protocol(), request_start_time: Instant::now(), remaining_chunks: expected_responses, }, @@ -422,7 +422,7 @@ where } else { self.events_out.push(Err(HandlerErr::Inbound { id: self.current_inbound_substream_id, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), error: RPCError::HandlerRejected, })); return self.shutdown(None); @@ -498,7 +498,7 @@ where }; self.events_out.push(Err(HandlerErr::Outbound { error, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), id, })); } @@ -895,7 +895,7 @@ where // else we return an error, stream should not have closed early. let outbound_err = HandlerErr::Outbound { id: request_id, - proto: request.protocol(), + proto: request.versioned_protocol().protocol(), error: RPCError::IncompleteStream, }; return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 5da595c3d..af0ba2510 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -3,11 +3,13 @@ use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use regex::bytes::Regex; use serde::Serialize; +use ssz::Encode; use ssz_derive::{Decode, Encode}; use ssz_types::{ typenum::{U1024, U256}, VariableList, }; +use std::marker::PhantomData; use std::ops::Deref; use std::sync::Arc; use strum::IntoStaticStr; @@ -85,6 +87,30 @@ pub struct Ping { pub data: u64, } +/// The METADATA request structure. +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Clone, Debug, PartialEq, Serialize),) +)] +#[derive(Clone, Debug, PartialEq)] +pub struct MetadataRequest { + _phantom_data: PhantomData, +} + +impl MetadataRequest { + pub fn new_v1() -> Self { + Self::V1(MetadataRequestV1 { + _phantom_data: PhantomData, + }) + } + + pub fn new_v2() -> Self { + Self::V2(MetadataRequestV2 { + _phantom_data: PhantomData, + }) + } +} + /// The METADATA response structure. #[superstruct( variants(V1, V2), @@ -93,9 +119,8 @@ pub struct Ping { serde(bound = "T: EthSpec", deny_unknown_fields), ) )] -#[derive(Clone, Debug, PartialEq, Serialize, Encode)] +#[derive(Clone, Debug, PartialEq, Serialize)] #[serde(bound = "T: EthSpec")] -#[ssz(enum_behaviour = "transparent")] pub struct MetaData { /// A sequential counter indicating when data gets modified. pub seq_number: u64, @@ -106,6 +131,38 @@ pub struct MetaData { pub syncnets: EnrSyncCommitteeBitfield, } +impl MetaData { + /// Returns a V1 MetaData response from self. + pub fn metadata_v1(&self) -> Self { + match self { + md @ MetaData::V1(_) => md.clone(), + MetaData::V2(metadata) => MetaData::V1(MetaDataV1 { + seq_number: metadata.seq_number, + attnets: metadata.attnets.clone(), + }), + } + } + + /// Returns a V2 MetaData response from self by filling unavailable fields with default. + pub fn metadata_v2(&self) -> Self { + match self { + MetaData::V1(metadata) => MetaData::V2(MetaDataV2 { + seq_number: metadata.seq_number, + attnets: metadata.attnets.clone(), + syncnets: Default::default(), + }), + md @ MetaData::V2(_) => md.clone(), + } + } + + pub fn as_ssz_bytes(&self) -> Vec { + match self { + MetaData::V1(md) => md.as_ssz_bytes(), + MetaData::V2(md) => md.as_ssz_bytes(), + } + } +} + /// The reason given for a `Goodbye` message. /// /// Note: any unknown `u64::into(n)` will resolve to `Goodbye::Unknown` for any unknown `n`, @@ -197,7 +254,11 @@ impl ssz::Decode for GoodbyeReason { } /// Request a number of beacon block roots from a peer. -#[derive(Encode, Decode, Clone, Debug, PartialEq)] +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Encode, Decode, Clone, Debug, PartialEq)) +)] +#[derive(Clone, Debug, PartialEq)] pub struct BlocksByRangeRequest { /// The starting slot to request blocks. pub start_slot: u64, @@ -206,8 +267,23 @@ pub struct BlocksByRangeRequest { pub count: u64, } +impl BlocksByRangeRequest { + /// The default request is V2 + pub fn new(start_slot: u64, count: u64) -> Self { + Self::V2(BlocksByRangeRequestV2 { start_slot, count }) + } + + pub fn new_v1(start_slot: u64, count: u64) -> Self { + Self::V1(BlocksByRangeRequestV1 { start_slot, count }) + } +} + /// Request a number of beacon block roots from a peer. -#[derive(Encode, Decode, Clone, Debug, PartialEq)] +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Encode, Decode, Clone, Debug, PartialEq)) +)] +#[derive(Clone, Debug, PartialEq)] pub struct OldBlocksByRangeRequest { /// The starting slot to request blocks. pub start_slot: u64, @@ -223,13 +299,43 @@ pub struct OldBlocksByRangeRequest { pub step: u64, } +impl OldBlocksByRangeRequest { + /// The default request is V2 + pub fn new(start_slot: u64, count: u64, step: u64) -> Self { + Self::V2(OldBlocksByRangeRequestV2 { + start_slot, + count, + step, + }) + } + + pub fn new_v1(start_slot: u64, count: u64, step: u64) -> Self { + Self::V1(OldBlocksByRangeRequestV1 { + start_slot, + count, + step, + }) + } +} + /// Request a number of beacon block bodies from a peer. +#[superstruct(variants(V1, V2), variant_attributes(derive(Clone, Debug, PartialEq)))] #[derive(Clone, Debug, PartialEq)] pub struct BlocksByRootRequest { /// The list of beacon block bodies being requested. pub block_roots: VariableList, } +impl BlocksByRootRequest { + pub fn new(block_roots: VariableList) -> Self { + Self::V2(BlocksByRootRequestV2 { block_roots }) + } + + pub fn new_v1(block_roots: VariableList) -> Self { + Self::V1(BlocksByRootRequestV1 { block_roots }) + } +} + /* RPC Handling and Grouping */ // Collection of enums and structs used by the Codecs to encode/decode RPC messages @@ -438,7 +544,12 @@ impl std::fmt::Display for GoodbyeReason { impl std::fmt::Display for BlocksByRangeRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Start Slot: {}, Count: {}", self.start_slot, self.count) + write!( + f, + "Start Slot: {}, Count: {}", + self.start_slot(), + self.count() + ) } } @@ -447,7 +558,9 @@ impl std::fmt::Display for OldBlocksByRangeRequest { write!( f, "Start Slot: {}, Count: {}, Step: {}", - self.start_slot, self.count, self.step + self.start_slot(), + self.count(), + self.step() ) } } diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 4f7af95cf..ffdc193bb 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -247,7 +247,7 @@ where } Err(RateLimitedErr::TooLarge) => { // we set the batch sizes, so this is a coding/config err for most protocols - let protocol = req.protocol(); + let protocol = req.versioned_protocol().protocol(); if matches!(protocol, Protocol::BlocksByRange) { debug!(self.log, "Blocks by range request will never be processed"; "request" => %req); } else { @@ -335,7 +335,7 @@ where serializer.emit_arguments("peer_id", &format_args!("{}", self.peer_id))?; let (msg_kind, protocol) = match &self.event { Ok(received) => match received { - RPCReceived::Request(_, req) => ("request", req.protocol()), + RPCReceived::Request(_, req) => ("request", req.versioned_protocol().protocol()), RPCReceived::Response(_, res) => ("response", res.protocol()), RPCReceived::EndOfStream(_, end) => ( "end_of_stream", diff --git a/beacon_node/lighthouse_network/src/rpc/outbound.rs b/beacon_node/lighthouse_network/src/rpc/outbound.rs index 774303800..d12f36686 100644 --- a/beacon_node/lighthouse_network/src/rpc/outbound.rs +++ b/beacon_node/lighthouse_network/src/rpc/outbound.rs @@ -1,11 +1,8 @@ -use std::marker::PhantomData; - use super::methods::*; -use super::protocol::Protocol; use super::protocol::ProtocolId; +use super::protocol::SupportedProtocol; use super::RPCError; use crate::rpc::protocol::Encoding; -use crate::rpc::protocol::Version; use crate::rpc::{ codec::{base::BaseOutboundCodec, ssz_snappy::SSZSnappyOutboundCodec, OutboundCodec}, methods::ResponseTermination, @@ -38,9 +35,8 @@ pub enum OutboundRequest { Goodbye(GoodbyeReason), BlocksByRange(OldBlocksByRangeRequest), BlocksByRoot(BlocksByRootRequest), - LightClientBootstrap(LightClientBootstrapRequest), Ping(Ping), - MetaData(PhantomData), + MetaData(MetadataRequest), } impl UpgradeInfo for OutboundRequestContainer { @@ -59,36 +55,29 @@ impl OutboundRequest { match self { // add more protocols when versions/encodings are supported OutboundRequest::Status(_) => vec![ProtocolId::new( - Protocol::Status, - Version::V1, + SupportedProtocol::StatusV1, Encoding::SSZSnappy, )], OutboundRequest::Goodbye(_) => vec![ProtocolId::new( - Protocol::Goodbye, - Version::V1, + SupportedProtocol::GoodbyeV1, Encoding::SSZSnappy, )], OutboundRequest::BlocksByRange(_) => vec![ - ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRangeV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRangeV1, Encoding::SSZSnappy), ], OutboundRequest::BlocksByRoot(_) => vec![ - ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRootV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRootV1, Encoding::SSZSnappy), ], OutboundRequest::Ping(_) => vec![ProtocolId::new( - Protocol::Ping, - Version::V1, + SupportedProtocol::PingV1, Encoding::SSZSnappy, )], OutboundRequest::MetaData(_) => vec![ - ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::MetaDataV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::MetaDataV1, Encoding::SSZSnappy), ], - // Note: This match arm is technically unreachable as we only respond to light client requests - // that we generate from the beacon state. - // We do not make light client rpc requests from the beacon node - OutboundRequest::LightClientBootstrap(_) => vec![], } } /* These functions are used in the handler for stream management */ @@ -98,24 +87,31 @@ impl OutboundRequest { match self { OutboundRequest::Status(_) => 1, OutboundRequest::Goodbye(_) => 0, - OutboundRequest::BlocksByRange(req) => req.count, - OutboundRequest::BlocksByRoot(req) => req.block_roots.len() as u64, + OutboundRequest::BlocksByRange(req) => *req.count(), + OutboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, OutboundRequest::Ping(_) => 1, OutboundRequest::MetaData(_) => 1, - OutboundRequest::LightClientBootstrap(_) => 1, } } - /// Gives the corresponding `Protocol` to this request. - pub fn protocol(&self) -> Protocol { + /// Gives the corresponding `SupportedProtocol` to this request. + pub fn versioned_protocol(&self) -> SupportedProtocol { match self { - OutboundRequest::Status(_) => Protocol::Status, - OutboundRequest::Goodbye(_) => Protocol::Goodbye, - OutboundRequest::BlocksByRange(_) => Protocol::BlocksByRange, - OutboundRequest::BlocksByRoot(_) => Protocol::BlocksByRoot, - OutboundRequest::Ping(_) => Protocol::Ping, - OutboundRequest::MetaData(_) => Protocol::MetaData, - OutboundRequest::LightClientBootstrap(_) => Protocol::LightClientBootstrap, + OutboundRequest::Status(_) => SupportedProtocol::StatusV1, + OutboundRequest::Goodbye(_) => SupportedProtocol::GoodbyeV1, + OutboundRequest::BlocksByRange(req) => match req { + OldBlocksByRangeRequest::V1(_) => SupportedProtocol::BlocksByRangeV1, + OldBlocksByRangeRequest::V2(_) => SupportedProtocol::BlocksByRangeV2, + }, + OutboundRequest::BlocksByRoot(req) => match req { + BlocksByRootRequest::V1(_) => SupportedProtocol::BlocksByRootV1, + BlocksByRootRequest::V2(_) => SupportedProtocol::BlocksByRootV2, + }, + OutboundRequest::Ping(_) => SupportedProtocol::PingV1, + OutboundRequest::MetaData(req) => match req { + MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, + MetadataRequest::V2(_) => SupportedProtocol::MetaDataV2, + }, } } @@ -127,7 +123,6 @@ impl OutboundRequest { // variants that have `multiple_responses()` can have values. OutboundRequest::BlocksByRange(_) => ResponseTermination::BlocksByRange, OutboundRequest::BlocksByRoot(_) => ResponseTermination::BlocksByRoot, - OutboundRequest::LightClientBootstrap(_) => unreachable!(), OutboundRequest::Status(_) => unreachable!(), OutboundRequest::Goodbye(_) => unreachable!(), OutboundRequest::Ping(_) => unreachable!(), @@ -185,9 +180,6 @@ impl std::fmt::Display for OutboundRequest { OutboundRequest::BlocksByRoot(req) => write!(f, "Blocks by root: {:?}", req), OutboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), OutboundRequest::MetaData(_) => write!(f, "MetaData request"), - OutboundRequest::LightClientBootstrap(bootstrap) => { - write!(f, "Lightclient Bootstrap: {}", bootstrap.root) - } } } } diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index a8423e47b..ea39c1423 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -179,21 +179,74 @@ pub enum Protocol { LightClientBootstrap, } -/// RPC Versions -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Version { - /// Version 1 of RPC - V1, - /// Version 2 of RPC - V2, -} - /// RPC Encondings supported. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Encoding { SSZSnappy, } +/// All valid protocol name and version combinations. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SupportedProtocol { + StatusV1, + GoodbyeV1, + BlocksByRangeV1, + BlocksByRangeV2, + BlocksByRootV1, + BlocksByRootV2, + PingV1, + MetaDataV1, + MetaDataV2, + LightClientBootstrapV1, +} + +impl SupportedProtocol { + pub fn version_string(&self) -> &'static str { + match self { + SupportedProtocol::StatusV1 => "1", + SupportedProtocol::GoodbyeV1 => "1", + SupportedProtocol::BlocksByRangeV1 => "1", + SupportedProtocol::BlocksByRangeV2 => "2", + SupportedProtocol::BlocksByRootV1 => "1", + SupportedProtocol::BlocksByRootV2 => "2", + SupportedProtocol::PingV1 => "1", + SupportedProtocol::MetaDataV1 => "1", + SupportedProtocol::MetaDataV2 => "2", + SupportedProtocol::LightClientBootstrapV1 => "1", + } + } + + pub fn protocol(&self) -> Protocol { + match self { + SupportedProtocol::StatusV1 => Protocol::Status, + SupportedProtocol::GoodbyeV1 => Protocol::Goodbye, + SupportedProtocol::BlocksByRangeV1 => Protocol::BlocksByRange, + SupportedProtocol::BlocksByRangeV2 => Protocol::BlocksByRange, + SupportedProtocol::BlocksByRootV1 => Protocol::BlocksByRoot, + SupportedProtocol::BlocksByRootV2 => Protocol::BlocksByRoot, + SupportedProtocol::PingV1 => Protocol::Ping, + SupportedProtocol::MetaDataV1 => Protocol::MetaData, + SupportedProtocol::MetaDataV2 => Protocol::MetaData, + SupportedProtocol::LightClientBootstrapV1 => Protocol::LightClientBootstrap, + } + } + + fn currently_supported() -> Vec { + vec![ + ProtocolId::new(Self::StatusV1, Encoding::SSZSnappy), + ProtocolId::new(Self::GoodbyeV1, Encoding::SSZSnappy), + // V2 variants have higher preference then V1 + ProtocolId::new(Self::BlocksByRangeV2, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRangeV1, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRootV2, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRootV1, Encoding::SSZSnappy), + ProtocolId::new(Self::PingV1, Encoding::SSZSnappy), + ProtocolId::new(Self::MetaDataV2, Encoding::SSZSnappy), + ProtocolId::new(Self::MetaDataV1, Encoding::SSZSnappy), + ] + } +} + impl std::fmt::Display for Encoding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let repr = match self { @@ -203,16 +256,6 @@ impl std::fmt::Display for Encoding { } } -impl std::fmt::Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let repr = match self { - Version::V1 => "1", - Version::V2 => "2", - }; - f.write_str(repr) - } -} - #[derive(Debug, Clone)] pub struct RPCProtocol { pub fork_context: Arc, @@ -227,22 +270,10 @@ impl UpgradeInfo for RPCProtocol { /// The list of supported RPC protocols for Lighthouse. fn protocol_info(&self) -> Self::InfoIter { - let mut supported_protocols = vec![ - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::Goodbye, Version::V1, Encoding::SSZSnappy), - // V2 variants have higher preference then V1 - ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::Ping, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy), - ]; + let mut supported_protocols = SupportedProtocol::currently_supported(); if self.enable_light_client_server { supported_protocols.push(ProtocolId::new( - Protocol::LightClientBootstrap, - Version::V1, + SupportedProtocol::LightClientBootstrapV1, Encoding::SSZSnappy, )); } @@ -272,11 +303,8 @@ impl RpcLimits { /// Tracks the types in a protocol id. #[derive(Clone, Debug)] pub struct ProtocolId { - /// The RPC message type/name. - pub message_name: Protocol, - - /// The version of the RPC. - pub version: Version, + /// The protocol name and version + pub versioned_protocol: SupportedProtocol, /// The encoding of the RPC. pub encoding: Encoding, @@ -288,7 +316,7 @@ pub struct ProtocolId { impl ProtocolId { /// Returns min and max size for messages of given protocol id requests. pub fn rpc_request_limits(&self) -> RpcLimits { - match self.message_name { + match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -297,9 +325,10 @@ impl ProtocolId { ::ssz_fixed_len(), ::ssz_fixed_len(), ), + // V1 and V2 requests are the same Protocol::BlocksByRange => RpcLimits::new( - ::ssz_fixed_len(), - ::ssz_fixed_len(), + ::ssz_fixed_len(), + ::ssz_fixed_len(), ), Protocol::BlocksByRoot => { RpcLimits::new(*BLOCKS_BY_ROOT_REQUEST_MIN, *BLOCKS_BY_ROOT_REQUEST_MAX) @@ -318,7 +347,7 @@ impl ProtocolId { /// Returns min and max size for messages of given protocol id responses. pub fn rpc_response_limits(&self, fork_context: &ForkContext) -> RpcLimits { - match self.message_name { + match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -344,30 +373,34 @@ impl ProtocolId { /// Returns `true` if the given `ProtocolId` should expect `context_bytes` in the /// beginning of the stream, else returns `false`. pub fn has_context_bytes(&self) -> bool { - match self.message_name { - Protocol::BlocksByRange | Protocol::BlocksByRoot => match self.version { - Version::V2 => true, - Version::V1 => false, - }, - Protocol::LightClientBootstrap => match self.version { - Version::V2 | Version::V1 => true, - }, - Protocol::Goodbye | Protocol::Ping | Protocol::Status | Protocol::MetaData => false, + match self.versioned_protocol { + SupportedProtocol::BlocksByRangeV2 + | SupportedProtocol::BlocksByRootV2 + | SupportedProtocol::LightClientBootstrapV1 => true, + SupportedProtocol::StatusV1 + | SupportedProtocol::BlocksByRootV1 + | SupportedProtocol::BlocksByRangeV1 + | SupportedProtocol::PingV1 + | SupportedProtocol::MetaDataV1 + | SupportedProtocol::MetaDataV2 + | SupportedProtocol::GoodbyeV1 => false, } } } /// An RPC protocol ID. impl ProtocolId { - pub fn new(message_name: Protocol, version: Version, encoding: Encoding) -> Self { + pub fn new(versioned_protocol: SupportedProtocol, encoding: Encoding) -> Self { let protocol_id = format!( "{}/{}/{}/{}", - PROTOCOL_PREFIX, message_name, version, encoding + PROTOCOL_PREFIX, + versioned_protocol.protocol(), + versioned_protocol.version_string(), + encoding ); ProtocolId { - message_name, - version, + versioned_protocol, encoding, protocol_id, } @@ -400,7 +433,7 @@ where fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future { async move { - let protocol_name = protocol.message_name; + let versioned_protocol = protocol.versioned_protocol; // convert the socket to tokio compatible socket let socket = socket.compat(); let codec = match protocol.encoding { @@ -419,8 +452,13 @@ where let socket = Framed::new(Box::pin(timed_socket), codec); // MetaData requests should be empty, return the stream - match protocol_name { - Protocol::MetaData => Ok((InboundRequest::MetaData(PhantomData), socket)), + match versioned_protocol { + SupportedProtocol::MetaDataV1 => { + Ok((InboundRequest::MetaData(MetadataRequest::new_v1()), socket)) + } + SupportedProtocol::MetaDataV2 => { + Ok((InboundRequest::MetaData(MetadataRequest::new_v2()), socket)) + } _ => { match tokio::time::timeout( Duration::from_secs(REQUEST_TIMEOUT), @@ -448,7 +486,7 @@ pub enum InboundRequest { BlocksByRoot(BlocksByRootRequest), LightClientBootstrap(LightClientBootstrapRequest), Ping(Ping), - MetaData(PhantomData), + MetaData(MetadataRequest), } /// Implements the encoding per supported protocol for `RPCRequest`. @@ -460,24 +498,33 @@ impl InboundRequest { match self { InboundRequest::Status(_) => 1, InboundRequest::Goodbye(_) => 0, - InboundRequest::BlocksByRange(req) => req.count, - InboundRequest::BlocksByRoot(req) => req.block_roots.len() as u64, + InboundRequest::BlocksByRange(req) => *req.count(), + InboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, InboundRequest::Ping(_) => 1, InboundRequest::MetaData(_) => 1, InboundRequest::LightClientBootstrap(_) => 1, } } - /// Gives the corresponding `Protocol` to this request. - pub fn protocol(&self) -> Protocol { + /// Gives the corresponding `SupportedProtocol` to this request. + pub fn versioned_protocol(&self) -> SupportedProtocol { match self { - InboundRequest::Status(_) => Protocol::Status, - InboundRequest::Goodbye(_) => Protocol::Goodbye, - InboundRequest::BlocksByRange(_) => Protocol::BlocksByRange, - InboundRequest::BlocksByRoot(_) => Protocol::BlocksByRoot, - InboundRequest::Ping(_) => Protocol::Ping, - InboundRequest::MetaData(_) => Protocol::MetaData, - InboundRequest::LightClientBootstrap(_) => Protocol::LightClientBootstrap, + InboundRequest::Status(_) => SupportedProtocol::StatusV1, + InboundRequest::Goodbye(_) => SupportedProtocol::GoodbyeV1, + InboundRequest::BlocksByRange(req) => match req { + OldBlocksByRangeRequest::V1(_) => SupportedProtocol::BlocksByRangeV1, + OldBlocksByRangeRequest::V2(_) => SupportedProtocol::BlocksByRangeV2, + }, + InboundRequest::BlocksByRoot(req) => match req { + BlocksByRootRequest::V1(_) => SupportedProtocol::BlocksByRootV1, + BlocksByRootRequest::V2(_) => SupportedProtocol::BlocksByRootV2, + }, + InboundRequest::Ping(_) => SupportedProtocol::PingV1, + InboundRequest::MetaData(req) => match req { + MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, + MetadataRequest::V2(_) => SupportedProtocol::MetaDataV2, + }, + InboundRequest::LightClientBootstrap(_) => SupportedProtocol::LightClientBootstrapV1, } } diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index 1fdc6cce3..e1634d711 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -192,7 +192,7 @@ pub trait RateLimiterItem { impl RateLimiterItem for super::InboundRequest { fn protocol(&self) -> Protocol { - self.protocol() + self.versioned_protocol().protocol() } fn expected_responses(&self) -> u64 { @@ -202,7 +202,7 @@ impl RateLimiterItem for super::InboundRequest { impl RateLimiterItem for super::OutboundRequest { fn protocol(&self) -> Protocol { - self.protocol() + self.versioned_protocol().protocol() } fn expected_responses(&self) -> u64 { diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index 6748a1947..626917d6a 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -72,7 +72,7 @@ impl SelfRateLimiter { request_id: Id, req: OutboundRequest, ) -> Result, Error> { - let protocol = req.protocol(); + 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)) { queued_requests.push_back(QueuedRequest { req, request_id }); @@ -111,7 +111,7 @@ impl SelfRateLimiter { event: RPCSend::Request(request_id, req), }), Err(e) => { - let protocol = req.protocol(); + let protocol = req.versioned_protocol(); match e { RateLimitedErr::TooLarge => { // this should never happen with default parameters. Let's just send the request. @@ -119,7 +119,7 @@ impl SelfRateLimiter { crit!( log, "Self rate limiting error for a batch that will never fit. Sending request anyway. Check configuration parameters."; - "protocol" => %req.protocol() + "protocol" => %req.versioned_protocol().protocol() ); Ok(BehaviourAction::NotifyHandler { peer_id, @@ -128,7 +128,7 @@ impl SelfRateLimiter { }) } RateLimitedErr::TooSoon(wait_time) => { - debug!(log, "Self rate limiting"; "protocol" => %protocol, "wait_time_ms" => wait_time.as_millis(), "peer_id" => %peer_id); + debug!(log, "Self rate limiting"; "protocol" => %protocol.protocol(), "wait_time_ms" => wait_time.as_millis(), "peer_id" => %peer_id); Err((QueuedRequest { req, request_id }, wait_time)) } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index bd3df7976..5ab89fee5 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -7,7 +7,8 @@ use types::{EthSpec, SignedBeaconBlock}; use crate::rpc::{ methods::{ BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, - OldBlocksByRangeRequest, RPCCodedResponse, RPCResponse, ResponseTermination, StatusMessage, + OldBlocksByRangeRequest, OldBlocksByRangeRequestV1, OldBlocksByRangeRequestV2, + RPCCodedResponse, RPCResponse, ResponseTermination, StatusMessage, }, OutboundRequest, SubstreamId, }; @@ -43,14 +44,25 @@ impl std::convert::From for OutboundRequest { fn from(req: Request) -> OutboundRequest { match req { Request::BlocksByRoot(r) => OutboundRequest::BlocksByRoot(r), - Request::BlocksByRange(BlocksByRangeRequest { start_slot, count }) => { - OutboundRequest::BlocksByRange(OldBlocksByRangeRequest { - start_slot, - count, - step: 1, - }) + Request::BlocksByRange(r) => match r { + BlocksByRangeRequest::V1(req) => OutboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V1(OldBlocksByRangeRequestV1 { + start_slot: req.start_slot, + count: req.count, + step: 1, + }), + ), + BlocksByRangeRequest::V2(req) => OutboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V2(OldBlocksByRangeRequestV2 { + start_slot: req.start_slot, + count: req.count, + step: 1, + }), + ), + }, + Request::LightClientBootstrap(_) => { + unreachable!("Lighthouse never makes an outbound light client request") } - Request::LightClientBootstrap(b) => OutboundRequest::LightClientBootstrap(b), Request::Status(s) => OutboundRequest::Status(s), } } diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 34d5a5631..129a4da25 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -9,6 +9,7 @@ use crate::peer_manager::{ ConnectionDirection, PeerManager, PeerManagerEvent, }; use crate::peer_manager::{MIN_OUTBOUND_ONLY_FACTOR, PEER_EXCESS_FACTOR, PRIORITY_PEER_EXCESS}; +use crate::rpc::methods::MetadataRequest; use crate::rpc::*; use crate::service::behaviour::BehaviourEvent; pub use crate::service::behaviour::Gossipsub; @@ -37,7 +38,6 @@ use slog::{crit, debug, info, o, trace, warn}; use std::path::PathBuf; use std::pin::Pin; use std::{ - marker::PhantomData, sync::Arc, task::{Context, Poll}, }; @@ -944,16 +944,25 @@ impl Network { /// Sends a METADATA request to a peer. fn send_meta_data_request(&mut self, peer_id: PeerId) { - let event = OutboundRequest::MetaData(PhantomData); + // We always prefer sending V2 requests + let event = OutboundRequest::MetaData(MetadataRequest::new_v2()); self.eth2_rpc_mut() .send_request(peer_id, RequestId::Internal, event); } /// Sends a METADATA response to a peer. - fn send_meta_data_response(&mut self, id: PeerRequestId, peer_id: PeerId) { - let event = RPCCodedResponse::Success(RPCResponse::MetaData( - self.network_globals.local_metadata.read().clone(), - )); + fn send_meta_data_response( + &mut self, + req: MetadataRequest, + id: PeerRequestId, + peer_id: PeerId, + ) { + let metadata = self.network_globals.local_metadata.read().clone(); + let metadata = match req { + MetadataRequest::V1(_) => metadata.metadata_v1(), + MetadataRequest::V2(_) => metadata, + }; + let event = RPCCodedResponse::Success(RPCResponse::MetaData(metadata)); self.eth2_rpc_mut().send_response(peer_id, id, event); } @@ -1196,9 +1205,9 @@ impl Network { self.pong(peer_request_id, peer_id); None } - InboundRequest::MetaData(_) => { + InboundRequest::MetaData(req) => { // send the requested meta-data - self.send_meta_data_response((handler_id, id), peer_id); + self.send_meta_data_response(req, (handler_id, id), peer_id); None } InboundRequest::Goodbye(reason) => { @@ -1225,13 +1234,9 @@ impl Network { Some(event) } InboundRequest::BlocksByRange(req) => { - let methods::OldBlocksByRangeRequest { - start_slot, - mut count, - step, - } = req; // Still disconnect the peer if the request is naughty. - if step == 0 { + let mut count = *req.count(); + if *req.step() == 0 { self.peer_manager_mut().handle_rpc_error( &peer_id, Protocol::BlocksByRange, @@ -1243,14 +1248,18 @@ impl Network { return None; } // return just one block in case the step parameter is used. https://github.com/ethereum/consensus-specs/pull/2856 - if step > 1 { + if *req.step() > 1 { count = 1; } - let event = self.build_request( - peer_request_id, - peer_id, - Request::BlocksByRange(BlocksByRangeRequest { start_slot, count }), - ); + let request = match req { + methods::OldBlocksByRangeRequest::V1(req) => Request::BlocksByRange( + BlocksByRangeRequest::new_v1(req.start_slot, count), + ), + methods::OldBlocksByRangeRequest::V2(req) => Request::BlocksByRange( + BlocksByRangeRequest::new(req.start_slot, count), + ), + }; + let event = self.build_request(peer_request_id, peer_id, request); Some(event) } InboundRequest::BlocksByRoot(req) => { diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 625df65ee..ac0dc57d7 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -272,9 +272,11 @@ pub(crate) fn save_metadata_to_disk( log: &slog::Logger, ) { let _ = std::fs::create_dir_all(dir); - match File::create(dir.join(METADATA_FILENAME)) - .and_then(|mut f| f.write_all(&metadata.as_ssz_bytes())) - { + let metadata_bytes = match metadata { + MetaData::V1(md) => md.as_ssz_bytes(), + MetaData::V2(md) => md.as_ssz_bytes(), + }; + match File::create(dir.join(METADATA_FILENAME)).and_then(|mut f| f.write_all(&metadata_bytes)) { Ok(_) => { debug!(log, "Metadata written to disk"); } diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index ebdbb6742..656df0c4a 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -155,10 +155,7 @@ fn test_blocks_by_range_chunked_rpc() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Merge).await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); let spec = E::default_spec(); @@ -282,10 +279,7 @@ fn test_blocks_by_range_over_limit() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Merge).await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); // BlocksByRange Response let full_block = merge_block_large(&common::fork_context(ForkName::Merge)); @@ -367,10 +361,7 @@ fn test_blocks_by_range_chunked_rpc_terminates_correctly() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); // BlocksByRange Response let spec = E::default_spec(); @@ -490,10 +481,7 @@ fn test_blocks_by_range_single_empty_rpc() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: 10, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, 10)); // BlocksByRange Response let spec = E::default_spec(); @@ -594,16 +582,15 @@ fn test_blocks_by_root_chunked_rpc() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Merge).await; // BlocksByRoot Request - let rpc_request = Request::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from(vec![ + let rpc_request = + Request::BlocksByRoot(BlocksByRootRequest::new(VariableList::from(vec![ Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), - ]), - }); + ]))); // BlocksByRoot Response let full_block = BeaconBlock::Base(BeaconBlockBase::::full(&spec)); @@ -722,8 +709,8 @@ fn test_blocks_by_root_chunked_rpc_terminates_correctly() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; // BlocksByRoot Request - let rpc_request = Request::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from(vec![ + let rpc_request = + Request::BlocksByRoot(BlocksByRootRequest::new(VariableList::from(vec![ Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), @@ -734,8 +721,7 @@ fn test_blocks_by_root_chunked_rpc_terminates_correctly() { Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), - ]), - }); + ]))); // BlocksByRoot Response let full_block = BeaconBlock::Base(BeaconBlockBase::::full(&spec)); diff --git a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs b/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs index 81b163bf7..83baa0417 100644 --- a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs @@ -131,10 +131,10 @@ impl Worker { request_id: PeerRequestId, request: BlocksByRootRequest, ) { - let requested_blocks = request.block_roots.len(); + let requested_blocks = request.block_roots().len(); let mut block_stream = match self .chain - .get_blocks_checking_early_attester_cache(request.block_roots.into(), &executor) + .get_blocks_checking_early_attester_cache(request.block_roots().to_vec(), &executor) { Ok(block_stream) => block_stream, Err(e) => return error!(self.log, "Error getting block stream"; "error" => ?e), @@ -292,18 +292,18 @@ impl Worker { ) { debug!(self.log, "Received BlocksByRange Request"; "peer_id" => %peer_id, - "count" => req.count, - "start_slot" => req.start_slot, + "count" => req.count(), + "start_slot" => req.start_slot(), ); // Should not send more than max request blocks - if req.count > MAX_REQUEST_BLOCKS { - req.count = MAX_REQUEST_BLOCKS; + if *req.count() > MAX_REQUEST_BLOCKS { + *req.count_mut() = MAX_REQUEST_BLOCKS; } let forwards_block_root_iter = match self .chain - .forwards_iter_block_roots(Slot::from(req.start_slot)) + .forwards_iter_block_roots(Slot::from(*req.start_slot())) { Ok(iter) => iter, Err(BeaconChainError::HistoricalBlockError( @@ -326,18 +326,20 @@ impl Worker { // Pick out the required blocks, ignoring skip-slots. let mut last_block_root = None; let maybe_block_roots = process_results(forwards_block_root_iter, |iter| { - iter.take_while(|(_, slot)| slot.as_u64() < req.start_slot.saturating_add(req.count)) - // map skip slots to None - .map(|(root, _)| { - let result = if Some(root) == last_block_root { - None - } else { - Some(root) - }; - last_block_root = Some(root); - result - }) - .collect::>>() + iter.take_while(|(_, slot)| { + slot.as_u64() < req.start_slot().saturating_add(*req.count()) + }) + // map skip slots to None + .map(|(root, _)| { + let result = if Some(root) == last_block_root { + None + } else { + Some(root) + }; + last_block_root = Some(root); + result + }) + .collect::>>() }); let block_roots = match maybe_block_roots { @@ -364,8 +366,8 @@ impl Worker { Ok(Some(block)) => { // Due to skip slots, blocks could be out of the range, we ensure they // are in the range before sending - if block.slot() >= req.start_slot - && block.slot() < req.start_slot + req.count + if block.slot() >= *req.start_slot() + && block.slot() < req.start_slot() + req.count() { blocks_sent += 1; self.send_network_message(NetworkMessage::SendResponse { @@ -440,15 +442,15 @@ impl Worker { .slot() .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); - if blocks_sent < (req.count as usize) { + if blocks_sent < (*req.count() as usize) { debug!( self.log, "BlocksByRange outgoing response processed"; "peer" => %peer_id, "msg" => "Failed to return all requested blocks", - "start_slot" => req.start_slot, + "start_slot" => req.start_slot(), "current_slot" => current_slot, - "requested" => req.count, + "requested" => req.count(), "returned" => blocks_sent ); } else { @@ -456,9 +458,9 @@ impl Worker { self.log, "BlocksByRange outgoing response processed"; "peer" => %peer_id, - "start_slot" => req.start_slot, + "start_slot" => req.start_slot(), "current_slot" => current_slot, - "requested" => req.count, + "requested" => req.count(), "returned" => blocks_sent ); } diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 256a2b429..62ca68e7b 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -156,9 +156,7 @@ impl SingleBlockRequest { cannot_process: self.failed_processing >= self.failed_downloading, }) } else if let Some(&peer_id) = self.available_peers.iter().choose(&mut rand::thread_rng()) { - let request = BlocksByRootRequest { - block_roots: VariableList::from(vec![self.hash]), - }; + let request = BlocksByRootRequest::new(VariableList::from(vec![self.hash])); self.state = State::Downloading { peer_id }; self.used_peers.insert(peer_id); Ok((peer_id, request)) diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index c81fed244..23d42002f 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -112,7 +112,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRange Request"; "method" => "BlocksByRange", - "count" => request.count, + "count" => request.count(), "peer" => %peer_id, ); let request = Request::BlocksByRange(request); @@ -138,7 +138,7 @@ impl SyncNetworkContext { self.log, "Sending backfill BlocksByRange Request"; "method" => "BlocksByRange", - "count" => request.count, + "count" => request.count(), "peer" => %peer_id, ); let request = Request::BlocksByRange(request); @@ -185,7 +185,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRoot Request"; "method" => "BlocksByRoot", - "count" => request.block_roots.len(), + "count" => request.block_roots().len(), "peer" => %peer_id ); let request = Request::BlocksByRoot(request); @@ -209,7 +209,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRoot Request"; "method" => "BlocksByRoot", - "count" => request.block_roots.len(), + "count" => request.block_roots().len(), "peer" => %peer_id ); let request = Request::BlocksByRoot(request); diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 3eee7223d..723ea9b59 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -202,10 +202,10 @@ impl BatchInfo { /// Returns a BlocksByRange request associated with the batch. pub fn to_blocks_by_range_request(&self) -> BlocksByRangeRequest { - BlocksByRangeRequest { - start_slot: self.start_slot.into(), - count: self.end_slot.sub(self.start_slot).into(), - } + BlocksByRangeRequest::new( + self.start_slot.into(), + self.end_slot.sub(self.start_slot).into(), + ) } /// After different operations over a batch, this could be in a state that allows it to From 77fc5111706c95b3ed55633355a0616f9a2d2073 Mon Sep 17 00:00:00 2001 From: ethDreamer Date: Thu, 15 Jun 2023 08:42:20 +0000 Subject: [PATCH 06/25] Use JSON by default for Deposit Snapshot Sync (#4397) Checkpointz now supports deposit snapshot but [they only support returning them in JSON](https://github.com/ethpandaops/checkpointz/issues/74) so I've modified lighthouse to request them in JSON by default. There's also `get_opt` & `get_opt_with_timeout` methods which seem to expect responses in JSON but were not adding `Accept: application/json` to the request headers so I fixed that as well. Also the beacon API puts quantities in quotes so I fixed that in the snapshot JSON serialization --- common/eth2/src/lib.rs | 16 +++++++++------- consensus/types/src/deposit_tree_snapshot.rs | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e03cc2e9b..e871efbc2 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -218,7 +218,11 @@ impl BeaconNodeHttpClient { /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_opt(&self, url: U) -> Result, Error> { - match self.get_response(url, |b| b).await.optional()? { + match self + .get_response(url, |b| b.accept(Accept::Json)) + .await + .optional()? + { Some(response) => Ok(Some(response.json().await?)), None => Ok(None), } @@ -231,7 +235,7 @@ impl BeaconNodeHttpClient { timeout: Duration, ) -> Result, Error> { let opt_response = self - .get_response(url, |b| b.timeout(timeout)) + .get_response(url, |b| b.timeout(timeout).accept(Accept::Json)) .await .optional()?; match opt_response { @@ -982,16 +986,14 @@ impl BeaconNodeHttpClient { /// `GET beacon/deposit_snapshot` pub async fn get_deposit_snapshot(&self) -> Result, Error> { - use ssz::Decode; let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("deposit_snapshot"); - self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_deposit_snapshot) - .await? - .map(|bytes| DepositTreeSnapshot::from_ssz_bytes(&bytes).map_err(Error::InvalidSsz)) - .transpose() + self.get_opt_with_timeout::, _>(path, self.timeouts.get_deposit_snapshot) + .await + .map(|opt| opt.map(|r| r.data)) } /// `POST beacon/rewards/sync_committee` diff --git a/consensus/types/src/deposit_tree_snapshot.rs b/consensus/types/src/deposit_tree_snapshot.rs index aea4677f2..12e81d002 100644 --- a/consensus/types/src/deposit_tree_snapshot.rs +++ b/consensus/types/src/deposit_tree_snapshot.rs @@ -30,8 +30,10 @@ impl From<&DepositTreeSnapshot> for FinalizedExecutionBlock { pub struct DepositTreeSnapshot { pub finalized: Vec, pub deposit_root: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub deposit_count: u64, pub execution_block_hash: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub execution_block_height: u64, } From affea585f44c037c1db77fb69a64c543f1721739 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 16 Jun 2023 06:44:31 +0000 Subject: [PATCH 07/25] Remove `CountUnrealized` (#4357) ## Issue Addressed Closes #4332 ## Proposed Changes Remove the `CountUnrealized` type, defaulting unrealized justification to _on_. This fixes the #4332 issue by ensuring that importing the same block to fork choice always results in the same outcome. Finalized sync speed may be slightly impacted by this change, but that is deemed an acceptable trade-off until the optimisation from #4118 is implemented. TODO: - [x] Also check that the block isn't a duplicate before importing --- beacon_node/beacon_chain/src/beacon_chain.rs | 12 +- beacon_node/beacon_chain/src/builder.rs | 3 +- beacon_node/beacon_chain/src/fork_revert.rs | 16 +- beacon_node/beacon_chain/src/lib.rs | 5 +- beacon_node/beacon_chain/src/test_utils.rs | 9 +- .../beacon_chain/tests/block_verification.rs | 161 +++++++++++----- .../tests/payload_invalidation.rs | 18 +- beacon_node/beacon_chain/tests/store_tests.rs | 2 - beacon_node/beacon_chain/tests/tests.rs | 2 - beacon_node/http_api/src/publish_blocks.rs | 11 +- .../beacon_processor/worker/gossip_methods.rs | 11 +- .../beacon_processor/worker/sync_methods.rs | 27 +-- beacon_node/network/src/sync/manager.rs | 2 +- .../network/src/sync/range_sync/chain.rs | 13 +- .../src/sync/range_sync/chain_collection.rs | 7 +- consensus/fork_choice/src/fork_choice.rs | 181 ++++++++---------- consensus/fork_choice/src/lib.rs | 6 +- consensus/fork_choice/tests/tests.rs | 5 +- testing/ef_tests/src/cases/fork_choice.rs | 4 +- 19 files changed, 229 insertions(+), 266 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2fa04304f..ceda7222e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -63,7 +63,6 @@ use execution_layer::{ BlockProposalContents, BuilderParams, ChainHealth, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus, }; -pub use fork_choice::CountUnrealized; use fork_choice::{ AttestationFromBlock, ExecutionStatus, ForkChoice, ForkchoiceUpdateParameters, InvalidationOperation, PayloadVerificationStatus, ResetPayloadStatuses, @@ -2510,7 +2509,6 @@ impl BeaconChain { pub async fn process_chain_segment( self: &Arc, chain_segment: Vec>>, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, ) -> ChainSegmentResult { let mut imported_blocks = 0; @@ -2579,7 +2577,6 @@ impl BeaconChain { .process_block( signature_verified_block.block_root(), signature_verified_block, - count_unrealized, notify_execution_layer, ) .await @@ -2668,7 +2665,6 @@ impl BeaconChain { self: &Arc, block_root: Hash256, unverified_block: B, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, ) -> Result> { // Start the Prometheus timer. @@ -2689,7 +2685,7 @@ impl BeaconChain { notify_execution_layer, )?; chain - .import_execution_pending_block(execution_pending, count_unrealized) + .import_execution_pending_block(execution_pending) .await }; @@ -2744,10 +2740,9 @@ impl BeaconChain { /// /// An error is returned if the block was unable to be imported. It may be partially imported /// (i.e., this function is not atomic). - async fn import_execution_pending_block( + pub async fn import_execution_pending_block( self: Arc, execution_pending_block: ExecutionPendingBlock, - count_unrealized: CountUnrealized, ) -> Result> { let ExecutionPendingBlock { block, @@ -2808,7 +2803,6 @@ impl BeaconChain { state, confirmed_state_roots, payload_verification_status, - count_unrealized, parent_block, parent_eth1_finalization_data, consensus_context, @@ -2834,7 +2828,6 @@ impl BeaconChain { mut state: BeaconState, confirmed_state_roots: Vec, payload_verification_status: PayloadVerificationStatus, - count_unrealized: CountUnrealized, parent_block: SignedBlindedBeaconBlock, parent_eth1_finalization_data: Eth1FinalizationData, mut consensus_context: ConsensusContext, @@ -2903,7 +2896,6 @@ impl BeaconChain { &state, payload_verification_status, &self.spec, - count_unrealized, ) .map_err(|e| BlockError::BeaconChainError(e.into()))?; } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index b0f0015b9..84148fbfb 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -18,7 +18,7 @@ use crate::{ }; use eth1::Config as Eth1Config; use execution_layer::ExecutionLayer; -use fork_choice::{CountUnrealized, ForkChoice, ResetPayloadStatuses}; +use fork_choice::{ForkChoice, ResetPayloadStatuses}; use futures::channel::mpsc::Sender; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::RwLock; @@ -687,7 +687,6 @@ where store.clone(), Some(current_slot), &self.spec, - CountUnrealized::True, )?; } diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index ccd17af24..084ae95e0 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -1,5 +1,5 @@ use crate::{BeaconForkChoiceStore, BeaconSnapshot}; -use fork_choice::{CountUnrealized, ForkChoice, PayloadVerificationStatus}; +use fork_choice::{ForkChoice, PayloadVerificationStatus}; use itertools::process_results; use slog::{info, warn, Logger}; use state_processing::state_advance::complete_state_advance; @@ -100,7 +100,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It store: Arc>, current_slot: Option, spec: &ChainSpec, - count_unrealized_config: CountUnrealized, ) -> Result, E>, String> { // Fetch finalized block. let finalized_checkpoint = head_state.finalized_checkpoint(); @@ -166,8 +165,7 @@ pub fn reset_fork_choice_to_finalization, Cold: It .map_err(|e| format!("Error loading blocks to replay for fork choice: {:?}", e))?; let mut state = finalized_snapshot.beacon_state; - let blocks_len = blocks.len(); - for (i, block) in blocks.into_iter().enumerate() { + for block in blocks { complete_state_advance(&mut state, None, block.slot(), spec) .map_err(|e| format!("State advance failed: {:?}", e))?; @@ -190,15 +188,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It // This scenario is so rare that it seems OK to double-verify some blocks. let payload_verification_status = PayloadVerificationStatus::Optimistic; - // Because we are replaying a single chain of blocks, we only need to calculate unrealized - // justification for the last block in the chain. - let is_last_block = i + 1 == blocks_len; - let count_unrealized = if is_last_block { - count_unrealized_config - } else { - CountUnrealized::False - }; - fork_choice .on_block( block.slot(), @@ -209,7 +198,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It &state, payload_verification_status, spec, - count_unrealized, ) .map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?; } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index be1522a3b..d672c1682 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -52,8 +52,8 @@ pub mod validator_pubkey_cache; pub use self::beacon_chain::{ AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BeaconStore, ChainSegmentResult, - CountUnrealized, ForkChoiceError, OverrideForkchoiceUpdate, ProduceBlockVerification, - StateSkipConfig, WhenSlotSkipped, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, + ForkChoiceError, OverrideForkchoiceUpdate, ProduceBlockVerification, StateSkipConfig, + WhenSlotSkipped, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, MAXIMUM_GOSSIP_CLOCK_DISPARITY, }; pub use self::beacon_snapshot::BeaconSnapshot; @@ -64,6 +64,7 @@ pub use attestation_verification::Error as AttestationError; pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError}; pub use block_verification::{ get_block_root, BlockError, ExecutionPayloadError, GossipVerifiedBlock, + IntoExecutionPendingBlock, }; pub use canonical_head::{CachedHead, CanonicalHead, CanonicalHeadRwLock}; pub use eth1_chain::{Eth1Chain, Eth1ChainBackend}; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index c5615b618..55ea016fb 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -22,7 +22,6 @@ use execution_layer::{ }, ExecutionLayer, }; -use fork_choice::CountUnrealized; use futures::channel::mpsc::Receiver; pub use genesis::{interop_genesis_state_with_eth1, DEFAULT_ETH1_BLOCK_HASH}; use int_to_bytes::int_to_bytes32; @@ -1693,12 +1692,7 @@ where self.set_current_slot(slot); let block_hash: SignedBeaconBlockHash = self .chain - .process_block( - block_root, - Arc::new(block), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, Arc::new(block), NotifyExecutionLayer::Yes) .await? .into(); self.chain.recompute_head_at_current_slot().await; @@ -1714,7 +1708,6 @@ where .process_block( block.canonical_root(), Arc::new(block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await? diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index c66ed60a9..a88931367 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -3,8 +3,9 @@ use beacon_chain::test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, }; -use beacon_chain::{BeaconSnapshot, BlockError, ChainSegmentResult, NotifyExecutionLayer}; -use fork_choice::CountUnrealized; +use beacon_chain::{ + BeaconSnapshot, BlockError, ChainSegmentResult, IntoExecutionPendingBlock, NotifyExecutionLayer, +}; use lazy_static::lazy_static; use logging::test_logger; use slasher::{Config as SlasherConfig, Slasher}; @@ -148,18 +149,14 @@ async fn chain_segment_full_segment() { // Sneak in a little check to ensure we can process empty chain segments. harness .chain - .process_chain_segment(vec![], CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(vec![], NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import empty chain segment"); harness .chain - .process_chain_segment( - blocks.clone(), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(blocks.clone(), NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import chain segment"); @@ -188,11 +185,7 @@ async fn chain_segment_varying_chunk_size() { for chunk in blocks.chunks(*chunk_size) { harness .chain - .process_chain_segment( - chunk.to_vec(), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(chunk.to_vec(), NotifyExecutionLayer::Yes) .await .into_block_error() .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); @@ -228,7 +221,7 @@ async fn chain_segment_non_linear_parent_roots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearParentRoots) @@ -248,7 +241,7 @@ async fn chain_segment_non_linear_parent_roots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearParentRoots) @@ -279,7 +272,7 @@ async fn chain_segment_non_linear_slots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearSlots) @@ -300,7 +293,7 @@ async fn chain_segment_non_linear_slots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearSlots) @@ -326,7 +319,7 @@ async fn assert_invalid_signature( matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -348,11 +341,7 @@ async fn assert_invalid_signature( // imported prior to this test. let _ = harness .chain - .process_chain_segment( - ancestor_blocks, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) .await; harness.chain.recompute_head_at_current_slot().await; @@ -361,7 +350,6 @@ async fn assert_invalid_signature( .process_block( snapshots[block_index].beacon_block.canonical_root(), snapshots[block_index].beacon_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await; @@ -414,11 +402,7 @@ async fn invalid_signature_gossip_block() { .collect(); harness .chain - .process_chain_segment( - ancestor_blocks, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import all blocks prior to the one being tested"); @@ -430,7 +414,6 @@ async fn invalid_signature_gossip_block() { .process_block( signed_block.canonical_root(), Arc::new(signed_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await, @@ -465,7 +448,7 @@ async fn invalid_signature_block_proposal() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -663,7 +646,7 @@ async fn invalid_signature_deposit() { !matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -743,7 +726,6 @@ async fn block_gossip_verification() { .process_block( gossip_verified.block_root, gossip_verified, - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1015,7 +997,6 @@ async fn verify_block_for_gossip_slashing_detection() { .process_block( verified_block.block_root, verified_block, - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1055,7 +1036,6 @@ async fn verify_block_for_gossip_doppelganger_detection() { .process_block( verified_block.block_root, verified_block, - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1203,7 +1183,6 @@ async fn add_base_block_to_altair_chain() { .process_block( base_block.canonical_root(), Arc::new(base_block.clone()), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1219,11 +1198,7 @@ async fn add_base_block_to_altair_chain() { assert!(matches!( harness .chain - .process_chain_segment( - vec![Arc::new(base_block)], - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(vec![Arc::new(base_block)], NotifyExecutionLayer::Yes,) .await, ChainSegmentResult::Failed { imported_blocks: 0, @@ -1342,7 +1317,6 @@ async fn add_altair_block_to_base_chain() { .process_block( altair_block.canonical_root(), Arc::new(altair_block.clone()), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1358,11 +1332,7 @@ async fn add_altair_block_to_base_chain() { assert!(matches!( harness .chain - .process_chain_segment( - vec![Arc::new(altair_block)], - CountUnrealized::True, - NotifyExecutionLayer::Yes - ) + .process_chain_segment(vec![Arc::new(altair_block)], NotifyExecutionLayer::Yes) .await, ChainSegmentResult::Failed { imported_blocks: 0, @@ -1373,3 +1343,100 @@ async fn add_altair_block_to_base_chain() { } )); } + +#[tokio::test] +async fn import_duplicate_block_unrealized_justification() { + let spec = MainnetEthSpec::default_spec(); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + let chain = &harness.chain; + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build the chain out to the first justification opportunity 2/3rds of the way through epoch 2. + let num_slots = E::slots_per_epoch() as usize * 8 / 3; + harness + .extend_chain( + num_slots, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // The store's justified checkpoint must still be at epoch 0, while unrealized justification + // must be at epoch 1. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!(fc.unrealized_justified_checkpoint().epoch, 1); + drop(fc); + + // Produce a block to justify epoch 2. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (block, _) = harness.make_block(state.clone(), slot).await; + let block = Arc::new(block); + let block_root = block.canonical_root(); + + // Create two verified variants of the block, representing the same block being processed in + // parallel. + let notify_execution_layer = NotifyExecutionLayer::Yes; + let verified_block1 = block + .clone() + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + let verified_block2 = block + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + + // Import the first block, simulating a block processed via a finalized chain segment. + chain + .clone() + .import_execution_pending_block(verified_block1) + .await + .unwrap(); + + // Unrealized justification should NOT have updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + let unrealized_justification = fc.unrealized_justified_checkpoint(); + assert_eq!(unrealized_justification.epoch, 2); + + // The fork choice node for the block should have unrealized justification. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); + drop(fc); + + // Import the second verified block, simulating a block processed via RPC. + chain + .clone() + .import_execution_pending_block(verified_block2) + .await + .unwrap(); + + // Unrealized justification should still be updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!( + fc.unrealized_justified_checkpoint(), + unrealized_justification + ); + + // The fork choice node for the block should still have the unrealized justified checkpoint. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); +} diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index f88c2ee6f..c39bdeaf3 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -17,9 +17,7 @@ use execution_layer::{ test_utils::ExecutionBlockGenerator, ExecutionLayer, ForkchoiceState, PayloadAttributes, }; -use fork_choice::{ - CountUnrealized, Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus, -}; +use fork_choice::{Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus}; use logging::test_logger; use proto_array::{Error as ProtoArrayError, ExecutionStatus}; use slot_clock::SlotClock; @@ -698,7 +696,6 @@ async fn invalidates_all_descendants() { .process_block( fork_block.canonical_root(), Arc::new(fork_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -795,7 +792,6 @@ async fn switches_heads() { .process_block( fork_block.canonical_root(), Arc::new(fork_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1050,7 +1046,7 @@ async fn invalid_parent() { // Ensure the block built atop an invalid payload is invalid for import. assert!(matches!( - rig.harness.chain.process_block(block.canonical_root(), block.clone(), CountUnrealized::True, NotifyExecutionLayer::Yes).await, + rig.harness.chain.process_block(block.canonical_root(), block.clone(), NotifyExecutionLayer::Yes).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) if invalid_root == parent_root )); @@ -1065,7 +1061,7 @@ async fn invalid_parent() { &state, PayloadVerificationStatus::Optimistic, &rig.harness.chain.spec, - CountUnrealized::True, + ), Err(ForkChoiceError::ProtoArrayStringError(message)) if message.contains(&format!( @@ -1336,12 +1332,7 @@ async fn build_optimistic_chain( for block in blocks { rig.harness .chain - .process_block( - block.canonical_root(), - block, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block.canonical_root(), block, NotifyExecutionLayer::Yes) .await .unwrap(); } @@ -1900,7 +1891,6 @@ async fn recover_from_invalid_head_by_importing_blocks() { .process_block( fork_block.canonical_root(), fork_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 2f40443b9..0bc7798a7 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -12,7 +12,6 @@ use beacon_chain::{ BeaconChainError, BeaconChainTypes, BeaconSnapshot, ChainConfig, NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped, }; -use fork_choice::CountUnrealized; use lazy_static::lazy_static; use logging::test_logger; use maplit::hashset; @@ -2151,7 +2150,6 @@ async fn weak_subjectivity_sync() { .process_block( full_block.canonical_root(), Arc::new(full_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index b4eabc809..f97f7069d 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -8,7 +8,6 @@ use beacon_chain::{ }, BeaconChain, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, }; -use fork_choice::CountUnrealized; use lazy_static::lazy_static; use operation_pool::PersistedOperationPool; use state_processing::{ @@ -687,7 +686,6 @@ async fn run_skip_slot_test(skip_slots: u64) { .process_block( harness_a.chain.head_snapshot().beacon_block_root, harness_a.chain.head_snapshot().beacon_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 1a5d5175b..8bcad6ba4 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -1,8 +1,6 @@ use crate::metrics; use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now}; -use beacon_chain::{ - BeaconChain, BeaconChainTypes, BlockError, CountUnrealized, NotifyExecutionLayer, -}; +use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer}; use execution_layer::ProvenancedPayload; use lighthouse_network::PubsubMessage; use network::NetworkMessage; @@ -56,12 +54,7 @@ pub async fn publish_block( let block_root = block_root.unwrap_or_else(|| block.canonical_root()); match chain - .process_block( - block_root, - block.clone(), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, block.clone(), NotifyExecutionLayer::Yes) .await { Ok(root) => { diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index cb4533f5a..121a27fec 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -8,8 +8,8 @@ use beacon_chain::{ observed_operations::ObservationOutcome, sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::get_block_delay_ms, - BeaconChainError, BeaconChainTypes, BlockError, CountUnrealized, ForkChoiceError, - GossipVerifiedBlock, NotifyExecutionLayer, + BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, GossipVerifiedBlock, + NotifyExecutionLayer, }; use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; use operation_pool::ReceivedPreCapella; @@ -949,12 +949,7 @@ impl Worker { let result = self .chain - .process_block( - block_root, - verified_block, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, verified_block, NotifyExecutionLayer::Yes) .await; match &result { diff --git a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs index 2dbb5a346..7e8fce356 100644 --- a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs @@ -7,7 +7,6 @@ use crate::beacon_processor::DuplicateCache; use crate::metrics; use crate::sync::manager::{BlockProcessType, SyncMessage}; use crate::sync::{BatchProcessResult, ChainId}; -use beacon_chain::CountUnrealized; use beacon_chain::{ observed_block_producers::Error as ObserveError, validator_monitor::get_block_delay_ms, BeaconChainError, BeaconChainTypes, BlockError, ChainSegmentResult, HistoricalBlockError, @@ -25,7 +24,7 @@ use types::{Epoch, Hash256, SignedBeaconBlock}; #[derive(Clone, Debug, PartialEq)] pub enum ChainSegmentProcessId { /// Processing Id of a range syncing batch. - RangeBatchId(ChainId, Epoch, CountUnrealized), + RangeBatchId(ChainId, Epoch), /// Processing ID for a backfill syncing batch. BackSyncBatchId(Epoch), /// Processing Id of the parent lookup of a block. @@ -166,12 +165,7 @@ impl Worker { let parent_root = block.message().parent_root(); let result = self .chain - .process_block( - block_root, - block, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, block, NotifyExecutionLayer::Yes) .await; metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); @@ -220,17 +214,13 @@ impl Worker { ) { let result = match sync_type { // this a request from the range sync - ChainSegmentProcessId::RangeBatchId(chain_id, epoch, count_unrealized) => { + ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64()); let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64()); let sent_blocks = downloaded_blocks.len(); match self - .process_blocks( - downloaded_blocks.iter(), - count_unrealized, - notify_execution_layer, - ) + .process_blocks(downloaded_blocks.iter(), notify_execution_layer) .await { (_, Ok(_)) => { @@ -309,11 +299,7 @@ impl Worker { // parent blocks are ordered from highest slot to lowest, so we need to process in // reverse match self - .process_blocks( - downloaded_blocks.iter().rev(), - CountUnrealized::True, - notify_execution_layer, - ) + .process_blocks(downloaded_blocks.iter().rev(), notify_execution_layer) .await { (imported_blocks, Err(e)) => { @@ -343,13 +329,12 @@ impl Worker { async fn process_blocks<'a>( &self, downloaded_blocks: impl Iterator>>, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, ) -> (usize, Result<(), ChainSegmentFailed>) { let blocks: Vec> = downloaded_blocks.cloned().collect(); match self .chain - .process_chain_segment(blocks, count_unrealized, notify_execution_layer) + .process_chain_segment(blocks, notify_execution_layer) .await { ChainSegmentResult::Successful { imported_blocks } => { diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 230c883a9..37b63cdba 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -556,7 +556,7 @@ impl SyncManager { .parent_block_processed(chain_hash, result, &mut self.network), }, SyncMessage::BatchProcessed { sync_type, result } => match sync_type { - ChainSegmentProcessId::RangeBatchId(chain_id, epoch, _) => { + ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { self.range_sync.handle_block_process_result( &mut self.network, chain_id, diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 4226b600f..51ca9e2b0 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -3,7 +3,7 @@ use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent as BeaconWorkEven use crate::sync::{ manager::Id, network_context::SyncNetworkContext, BatchOperationOutcome, BatchProcessResult, }; -use beacon_chain::{BeaconChainTypes, CountUnrealized}; +use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::{PeerAction, PeerId}; use rand::seq::SliceRandom; @@ -101,8 +101,6 @@ pub struct SyncingChain { /// Batches validated by this chain. validated_batches: u64, - is_finalized_segment: bool, - /// The chain's log. log: slog::Logger, } @@ -128,7 +126,6 @@ impl SyncingChain { target_head_slot: Slot, target_head_root: Hash256, peer_id: PeerId, - is_finalized_segment: bool, log: &slog::Logger, ) -> Self { let mut peers = FnvHashMap::default(); @@ -150,7 +147,6 @@ impl SyncingChain { state: ChainSyncingState::Stopped, current_processing_batch: None, validated_batches: 0, - is_finalized_segment, log: log.new(o!("chain" => id)), } } @@ -318,12 +314,7 @@ impl SyncingChain { // for removing chains and checking completion is in the callback. let blocks = batch.start_processing()?; - let count_unrealized = if self.is_finalized_segment { - CountUnrealized::False - } else { - CountUnrealized::True - }; - let process_id = ChainSegmentProcessId::RangeBatchId(self.id, batch_id, count_unrealized); + let process_id = ChainSegmentProcessId::RangeBatchId(self.id, batch_id); self.current_processing_batch = Some(batch_id); if let Err(e) = 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 37a3f13e7..65ddcefe8 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -465,10 +465,10 @@ impl ChainCollection { network: &mut SyncNetworkContext, ) { let id = SyncingChain::::id(&target_head_root, &target_head_slot); - let (collection, is_finalized) = if let RangeSyncType::Finalized = sync_type { - (&mut self.finalized_chains, true) + let collection = if let RangeSyncType::Finalized = sync_type { + &mut self.finalized_chains } else { - (&mut self.head_chains, false) + &mut self.head_chains }; match collection.entry(id) { Entry::Occupied(mut entry) => { @@ -493,7 +493,6 @@ impl ChainCollection { target_head_slot, target_head_root, peer, - is_finalized, &self.log, ); debug_assert_eq!(new_chain.get_id(), id); diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index e6c46e83e..5d86f99f1 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -174,21 +174,6 @@ impl From for Error { } } -/// Indicates whether the unrealized justification of a block should be calculated and tracked. -/// If a block has been finalized, this can be set to false. This is useful when syncing finalized -/// portions of the chain. Otherwise this should always be set to true. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum CountUnrealized { - True, - False, -} - -impl CountUnrealized { - pub fn is_true(&self) -> bool { - matches!(self, CountUnrealized::True) - } -} - /// Indicates if a block has been verified by an execution payload. /// /// There is no variant for "invalid", since such a block should never be added to fork choice. @@ -659,8 +644,14 @@ where state: &BeaconState, payload_verification_status: PayloadVerificationStatus, spec: &ChainSpec, - count_unrealized: CountUnrealized, ) -> Result<(), Error> { + // If this block has already been processed we do not need to reprocess it. + // We check this immediately in case re-processing the block mutates some property of the + // global fork choice store, e.g. the justified checkpoints or the proposer boost root. + if self.proto_array.contains_block(&block_root) { + return Ok(()); + } + // Provide the slot (as per the system clock) to the `fc_store` and then return its view of // the current slot. The `fc_store` will ensure that the `current_slot` is never // decreasing, a property which we must maintain. @@ -726,96 +717,84 @@ where )?; // Update unrealized justified/finalized checkpoints. - let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = if count_unrealized - .is_true() - { - let block_epoch = block.slot().epoch(E::slots_per_epoch()); + let block_epoch = block.slot().epoch(E::slots_per_epoch()); - // If the parent checkpoints are already at the same epoch as the block being imported, - // it's impossible for the unrealized checkpoints to differ from the parent's. This - // holds true because: - // - // 1. A child block cannot have lower FFG checkpoints than its parent. - // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`. - // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`. - // - // This is an optimization. It should reduce the amount of times we run - // `process_justification_and_finalization` by approximately 1/3rd when the chain is - // performing optimally. - let parent_checkpoints = parent_block - .unrealized_justified_checkpoint - .zip(parent_block.unrealized_finalized_checkpoint) - .filter(|(parent_justified, parent_finalized)| { - parent_justified.epoch == block_epoch - && parent_finalized.epoch + 1 >= block_epoch - }); + // If the parent checkpoints are already at the same epoch as the block being imported, + // it's impossible for the unrealized checkpoints to differ from the parent's. This + // holds true because: + // + // 1. A child block cannot have lower FFG checkpoints than its parent. + // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`. + // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`. + // + // This is an optimization. It should reduce the amount of times we run + // `process_justification_and_finalization` by approximately 1/3rd when the chain is + // performing optimally. + let parent_checkpoints = parent_block + .unrealized_justified_checkpoint + .zip(parent_block.unrealized_finalized_checkpoint) + .filter(|(parent_justified, parent_finalized)| { + parent_justified.epoch == block_epoch && parent_finalized.epoch + 1 >= block_epoch + }); - let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = - if let Some((parent_justified, parent_finalized)) = parent_checkpoints { - (parent_justified, parent_finalized) - } else { - let justification_and_finalization_state = match block { - BeaconBlockRef::Capella(_) - | BeaconBlockRef::Merge(_) - | BeaconBlockRef::Altair(_) => { - let participation_cache = - per_epoch_processing::altair::ParticipationCache::new(state, spec) - .map_err(Error::ParticipationCacheBuild)?; - per_epoch_processing::altair::process_justification_and_finalization( - state, - &participation_cache, - )? - } - BeaconBlockRef::Base(_) => { - let mut validator_statuses = - per_epoch_processing::base::ValidatorStatuses::new(state, spec) - .map_err(Error::ValidatorStatuses)?; - validator_statuses - .process_attestations(state) + let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = + if let Some((parent_justified, parent_finalized)) = parent_checkpoints { + (parent_justified, parent_finalized) + } else { + let justification_and_finalization_state = match block { + BeaconBlockRef::Capella(_) + | BeaconBlockRef::Merge(_) + | BeaconBlockRef::Altair(_) => { + let participation_cache = + per_epoch_processing::altair::ParticipationCache::new(state, spec) + .map_err(Error::ParticipationCacheBuild)?; + per_epoch_processing::altair::process_justification_and_finalization( + state, + &participation_cache, + )? + } + BeaconBlockRef::Base(_) => { + let mut validator_statuses = + per_epoch_processing::base::ValidatorStatuses::new(state, spec) .map_err(Error::ValidatorStatuses)?; - per_epoch_processing::base::process_justification_and_finalization( - state, - &validator_statuses.total_balances, - spec, - )? - } - }; - - ( - justification_and_finalization_state.current_justified_checkpoint(), - justification_and_finalization_state.finalized_checkpoint(), - ) + validator_statuses + .process_attestations(state) + .map_err(Error::ValidatorStatuses)?; + per_epoch_processing::base::process_justification_and_finalization( + state, + &validator_statuses.total_balances, + spec, + )? + } }; - // Update best known unrealized justified & finalized checkpoints - if unrealized_justified_checkpoint.epoch - > self.fc_store.unrealized_justified_checkpoint().epoch - { - self.fc_store - .set_unrealized_justified_checkpoint(unrealized_justified_checkpoint); - } - if unrealized_finalized_checkpoint.epoch - > self.fc_store.unrealized_finalized_checkpoint().epoch - { - self.fc_store - .set_unrealized_finalized_checkpoint(unrealized_finalized_checkpoint); - } + ( + justification_and_finalization_state.current_justified_checkpoint(), + justification_and_finalization_state.finalized_checkpoint(), + ) + }; - // If block is from past epochs, try to update store's justified & finalized checkpoints right away - if block.slot().epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) { - self.pull_up_store_checkpoints( - unrealized_justified_checkpoint, - unrealized_finalized_checkpoint, - )?; - } + // Update best known unrealized justified & finalized checkpoints + if unrealized_justified_checkpoint.epoch + > self.fc_store.unrealized_justified_checkpoint().epoch + { + self.fc_store + .set_unrealized_justified_checkpoint(unrealized_justified_checkpoint); + } + if unrealized_finalized_checkpoint.epoch + > self.fc_store.unrealized_finalized_checkpoint().epoch + { + self.fc_store + .set_unrealized_finalized_checkpoint(unrealized_finalized_checkpoint); + } - ( - Some(unrealized_justified_checkpoint), - Some(unrealized_finalized_checkpoint), - ) - } else { - (None, None) - }; + // If block is from past epochs, try to update store's justified & finalized checkpoints right away + if block.slot().epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) { + self.pull_up_store_checkpoints( + unrealized_justified_checkpoint, + unrealized_finalized_checkpoint, + )?; + } let target_slot = block .slot() @@ -886,8 +865,8 @@ where justified_checkpoint: state.current_justified_checkpoint(), finalized_checkpoint: state.finalized_checkpoint(), execution_status, - unrealized_justified_checkpoint, - unrealized_finalized_checkpoint, + unrealized_justified_checkpoint: Some(unrealized_justified_checkpoint), + unrealized_finalized_checkpoint: Some(unrealized_finalized_checkpoint), }, current_slot, )?; diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index 397a2ff89..e7ca84efb 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -2,9 +2,9 @@ mod fork_choice; mod fork_choice_store; pub use crate::fork_choice::{ - AttestationFromBlock, CountUnrealized, Error, ForkChoice, ForkChoiceView, - ForkchoiceUpdateParameters, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, - PersistedForkChoice, QueuedAttestation, ResetPayloadStatuses, + AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, + InvalidAttestation, InvalidBlock, PayloadVerificationStatus, PersistedForkChoice, + QueuedAttestation, ResetPayloadStatuses, }; pub use fork_choice_store::ForkChoiceStore; pub use proto_array::{Block as ProtoBlock, ExecutionStatus, InvalidationOperation}; diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 82bf642f1..ef262b58c 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -12,8 +12,7 @@ use beacon_chain::{ StateSkipConfig, WhenSlotSkipped, }; use fork_choice::{ - CountUnrealized, ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, - QueuedAttestation, + ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, QueuedAttestation, }; use store::MemoryStore; use types::{ @@ -288,7 +287,6 @@ impl ForkChoiceTest { &state, PayloadVerificationStatus::Verified, &self.harness.chain.spec, - CountUnrealized::True, ) .unwrap(); self @@ -331,7 +329,6 @@ impl ForkChoiceTest { &state, PayloadVerificationStatus::Verified, &self.harness.chain.spec, - CountUnrealized::True, ) .err() .expect("on_block did not return an error"); diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 4f5d99830..e0f4043ac 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -7,7 +7,7 @@ use beacon_chain::{ obtain_indexed_attestation_and_committees_per_slot, VerifiedAttestation, }, test_utils::{BeaconChainHarness, EphemeralHarnessType}, - BeaconChainTypes, CachedHead, CountUnrealized, NotifyExecutionLayer, + BeaconChainTypes, CachedHead, NotifyExecutionLayer, }; use execution_layer::{json_structures::JsonPayloadStatusV1Status, PayloadStatusV1}; use serde::Deserialize; @@ -381,7 +381,6 @@ impl Tester { let result = self.block_on_dangerous(self.harness.chain.process_block( block_root, block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, ))?; if result.is_ok() != valid { @@ -441,7 +440,6 @@ impl Tester { &state, PayloadVerificationStatus::Irrelevant, &self.harness.chain.spec, - CountUnrealized::True, ); if result.is_ok() { From 2bb62b7f7dc636f00018144a09873c1c4ba83ec8 Mon Sep 17 00:00:00 2001 From: chonghe Date: Fri, 16 Jun 2023 06:44:32 +0000 Subject: [PATCH 08/25] Correct table formatting in Lighthouse book (#4407) This is only a minor correction to the table not properly showing up in Lighthouse book. The changes solves the formatting issue. Another change is on the link to do port-forwarding. --- book/src/faq.md | 2 +- book/src/voluntary-exit.md | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/book/src/faq.md b/book/src/faq.md index 404ae2667..5651f108a 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -386,7 +386,7 @@ For these reasons, we recommend that you make your node publicly accessible. Lighthouse supports UPnP. If you are behind a NAT with a router that supports UPnP, you can simply ensure UPnP is enabled (Lighthouse will inform you in its -initial logs if a route has been established). You can also manually [set up port mappings](./advanced_networking.md) in your router to your local Lighthouse instance. By default, +initial logs if a route has been established). You can also manually [set up port mappings/port forwarding](./advanced_networking.md/#how-to-open-ports) in your router to your local Lighthouse instance. By default, Lighthouse uses port 9000 for both TCP and UDP. Opening both these ports will make your Lighthouse node maximally contactable. diff --git a/book/src/voluntary-exit.md b/book/src/voluntary-exit.md index d298d13f2..8d61c1770 100644 --- a/book/src/voluntary-exit.md +++ b/book/src/voluntary-exit.md @@ -101,16 +101,17 @@ There are two types of withdrawal credentials, `0x00` and `0x01`. To check which
    - | Number of eligible validators | Ideal scenario *n* | Practical scenario *n* | - |-------------------------------|--------------------| ---------------------- | - | 300000 | 2.60 | 2.63 | - | 400000 | 3.47 | 3.51 | - | 500000 | 4.34 | 4.38 | - | 600000 | 5.21 | 5.26 | - | 700000 | 6.08 | 6.14 | - | 800000 | 6.94 | 7.01 | - | 900000 | 7.81 | 7.89 | - | 1000000 | 8.68 | 8.77 | +| Number of eligible validators | Ideal scenario *n* | Practical scenario *n* | +|:----------------:|:---------------------:|:----:| +| 300000 | 2.60 | 2.63 | +| 400000 | 3.47 | 3.51 | +| 500000 | 4.34 | 4.38 | +| 600000 | 5.21 | 5.26 | +| 700000 | 6.08 | 6.14 | +| 800000 | 6.94 | 7.01 | +| 900000 | 7.81 | 7.89 | +| 1000000 | 8.68 | 8.77 | +
    > Note: Ideal scenario assumes no block proposals are missed. This means a total of withdrawals of 7200 blocks/day * 16 withdrawals/block = 115200 withdrawals/day. Practical scenario assumes 1% of blocks are missed per day. As an example, if there are 700000 eligible validators, one would expect a waiting time of slightly more than 6 days. From 6621e1d0c5fc8a310bec51a19a7a721f9fb91bb9 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 19 Jun 2023 23:53:25 +0000 Subject: [PATCH 09/25] Improve ENR logic for ipv6 (#4395) Currently, the ENR of the node may not be correctly updated when specifying ipv6 fields through the CLI if an ENR exists on disk. This remedies a bug where we were not checking for ipv6 fields when comparing whether to use an on-disk ENR or updating based on CLI configuration parameters. --- beacon_node/lighthouse_network/src/discovery/enr.rs | 6 +++++- boot_node/src/cli.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 938e7cfa2..f85c4b3e5 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -213,13 +213,17 @@ pub fn build_enr( fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { // take preference over disk_enr address if one is not specified (local_enr.ip4().is_none() || local_enr.ip4() == disk_enr.ip4()) + && + (local_enr.ip6().is_none() || local_enr.ip6() == disk_enr.ip6()) // tcp ports must match && local_enr.tcp4() == disk_enr.tcp4() + && local_enr.tcp6() == disk_enr.tcp6() // must match on the same fork && local_enr.get(ETH2_ENR_KEY) == disk_enr.get(ETH2_ENR_KEY) // take preference over disk udp port if one is not specified && (local_enr.udp4().is_none() || local_enr.udp4() == disk_enr.udp4()) - // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match, + && (local_enr.udp6().is_none() || local_enr.udp6() == disk_enr.udp6()) + // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match, // otherwise we use a new ENR. This will likely only be true for non-validating nodes && local_enr.get(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get(ATTESTATION_BITFIELD_ENR_KEY) && local_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) diff --git a/boot_node/src/cli.rs b/boot_node/src/cli.rs index b13f47f75..d7ea5ab0b 100644 --- a/boot_node/src/cli.rs +++ b/boot_node/src/cli.rs @@ -102,7 +102,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("network-dir") .value_name("NETWORK_DIR") .long("network-dir") - .help("The directory which contains the enr and it's assoicated private key") + .help("The directory which contains the enr and it's associated private key") .takes_value(true) ) } From c76afc6630740216aa8f73eecd2e56f9a833719c Mon Sep 17 00:00:00 2001 From: Mac L Date: Tue, 20 Jun 2023 05:20:36 +0000 Subject: [PATCH 10/25] Remove legacy `max-skip-slots` checks (#4403) ## Proposed Changes Remove `max-skip-slots` checks when processing blocks. This was legacy code which was previously used in the Medalla testnet to sync to the correct fork. With the addition of checkpoint sync which allows us to sync to any arbitrary fork, this is no longer a necessary feature, so it has been removed for simplicity. ## Additional Notes The CLI flag and checks for attestation processing have been retained as it still may have uses in DoS protection. --- .../beacon_chain/src/block_verification.rs | 35 ------------------- beacon_node/beacon_chain/src/chain_config.rs | 3 +- .../beacon_processor/worker/gossip_methods.rs | 1 - beacon_node/src/cli.rs | 2 +- 4 files changed, 2 insertions(+), 39 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index dba38af9b..3cb8fbdb5 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -141,8 +141,6 @@ pub enum BlockError { /// It's unclear if this block is valid, but it cannot be processed without already knowing /// its parent. ParentUnknown(Arc>), - /// The block skips too many slots and is a DoS risk. - TooManySkippedSlots { parent_slot: Slot, block_slot: Slot }, /// The block slot is greater than the present slot. /// /// ## Peer scoring @@ -786,9 +784,6 @@ impl GossipVerifiedBlock { parent_block.root }; - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent_block.slot, block.message())?; - // We assign to a variable instead of using `if let Some` directly to ensure we drop the // write lock before trying to acquire it again in the `else` clause. let proposer_opt = chain @@ -942,9 +937,6 @@ impl SignatureVerifiedBlock { let (mut parent, block) = load_parent(block_root, block, chain)?; - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent.beacon_block.slot(), block.message())?; - let state = cheap_state_advance_to_obtain_committees( &mut parent.pre_state, parent.beacon_state_root, @@ -1135,9 +1127,6 @@ impl ExecutionPendingBlock { return Err(BlockError::ParentUnknown(block)); } - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent.beacon_block.slot(), block.message())?; - /* * Perform cursory checks to see if the block is even worth processing. */ @@ -1492,30 +1481,6 @@ impl ExecutionPendingBlock { } } -/// Check that the count of skip slots between the block and its parent does not exceed our maximum -/// value. -/// -/// Whilst this is not part of the specification, we include this to help prevent us from DoS -/// attacks. In times of dire network circumstance, the user can configure the -/// `import_max_skip_slots` value. -fn check_block_skip_slots( - chain: &BeaconChain, - parent_slot: Slot, - block: BeaconBlockRef<'_, T::EthSpec>, -) -> Result<(), BlockError> { - // Reject any block that exceeds our limit on skipped slots. - if let Some(max_skip_slots) = chain.config.import_max_skip_slots { - if block.slot() > parent_slot + max_skip_slots { - return Err(BlockError::TooManySkippedSlots { - parent_slot, - block_slot: block.slot(), - }); - } - } - - Ok(()) -} - /// Returns `Ok(())` if the block's slot is greater than the anchor block's slot (if any). fn check_block_against_anchor_slot( block: BeaconBlockRef<'_, T::EthSpec>, diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index a74fdced1..34a5c9a4e 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -17,8 +17,7 @@ pub const FORK_CHOICE_LOOKAHEAD_FACTOR: u32 = 24; #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct ChainConfig { - /// Maximum number of slots to skip when importing a consensus message (e.g., block, - /// attestation, etc). + /// Maximum number of slots to skip when importing an attestation. /// /// If `None`, there is no limit. pub import_max_skip_slots: Option, diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index 121a27fec..e3cff0010 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -835,7 +835,6 @@ impl Worker { | Err(e @ BlockError::NonLinearParentRoots) | Err(e @ BlockError::BlockIsNotLaterThanParent { .. }) | Err(e @ BlockError::InvalidSignature) - | Err(e @ BlockError::TooManySkippedSlots { .. }) | Err(e @ BlockError::WeakSubjectivityConflict) | Err(e @ BlockError::InconsistentFork(_)) | Err(e @ BlockError::ExecutionPayloadError(_)) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index e763d93f8..206cd3c72 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -685,7 +685,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("max-skip-slots") .long("max-skip-slots") .help( - "Refuse to skip more than this many slots when processing a block or attestation. \ + "Refuse to skip more than this many slots when processing an attestation. \ This prevents nodes on minority forks from wasting our time and disk space, \ but could also cause unnecessary consensus failures, so is disabled by default." ) From bd6a015fe73f37c77f365d5baa4d955ff6e61f48 Mon Sep 17 00:00:00 2001 From: chonghe Date: Tue, 20 Jun 2023 05:20:37 +0000 Subject: [PATCH 11/25] Update Lighthouse book on Doppelganger Protection (#4418) Revise the page by removing the info on sync committee delay. Also added an faq on changing the port. --- book/src/faq.md | 10 +++++++--- book/src/validator-doppelganger.md | 5 ++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/book/src/faq.md b/book/src/faq.md index 5651f108a..d3e25438a 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -25,10 +25,11 @@ ## [Network, Monitoring and Maintenance](#network-monitoring-and-maintenance-1) - [I have a low peer count and it is not increasing](#net-peer) - [How do I update lighthouse?](#net-update) -- [Do I need to set up any port mappings (port forwarding)?](#net-port) +- [Do I need to set up any port mappings (port forwarding)?](#net-port-forwarding) - [How can I monitor my validators?](#net-monitor) - [My beacon node and validator client are on different servers. How can I point the validator client to the beacon node?](#net-bn-vc) - [Should I do anything to the beacon node or validator client settings if I have a relocation of the node / change of IP address?](#net-ip) +- [How to change the TCP/UDP port 9000 that Lighthouse listens on?](#net-port) ## [Miscellaneous](#miscellaneous-1) @@ -360,7 +361,7 @@ $ docker pull sigp/lighthouse:v1.0.0 If you are building a docker image, the process will be similar to the one described [here.](./docker.md#building-the-docker-image) You just need to make sure the code you have checked out is up to date. -###
    Do I need to set up any port mappings (port forwarding)? +### Do I need to set up any port mappings (port forwarding)? It is not strictly required to open any ports for Lighthouse to connect and participate in the network. Lighthouse should work out-of-the-box. However, if @@ -386,7 +387,7 @@ For these reasons, we recommend that you make your node publicly accessible. Lighthouse supports UPnP. If you are behind a NAT with a router that supports UPnP, you can simply ensure UPnP is enabled (Lighthouse will inform you in its -initial logs if a route has been established). You can also manually [set up port mappings/port forwarding](./advanced_networking.md/#how-to-open-ports) in your router to your local Lighthouse instance. By default, +initial logs if a route has been established). You can also manually [set up port mappings/port forwarding](./advanced_networking.md#how-to-open-ports) in your router to your local Lighthouse instance. By default, Lighthouse uses port 9000 for both TCP and UDP. Opening both these ports will make your Lighthouse node maximally contactable. @@ -421,6 +422,9 @@ The settings are as follows: ### Should I do anything to the beacon node or validator client settings if I have a relocation of the node / change of IP address? No. Lighthouse will auto-detect the change and update your Ethereum Node Record (ENR). You just need to make sure you are not manually setting the ENR with `--enr-address` (which, for common use cases, this flag is not used). +### How to change the TCP/UDP port 9000 that Lighthouse listens on? +Use the flag ```--port ``` in the beacon node. This flag can be useful when you are running two beacon nodes at the same time. You can leave one beacon node as the default port 9000, and configure the second beacon node to listen on, e.g., ```--port 9001```. + ## Miscellaneous ### What should I do if I lose my slashing protection database? diff --git a/book/src/validator-doppelganger.md b/book/src/validator-doppelganger.md index 6eaddcc7b..7ce2868e9 100644 --- a/book/src/validator-doppelganger.md +++ b/book/src/validator-doppelganger.md @@ -43,13 +43,12 @@ DP works by staying silent on the network for 2-3 epochs before starting to sign Staying silent and refusing to sign messages will cause the following: - 2-3 missed attestations, incurring penalties and missed rewards. -- 2-3 epochs of missed sync committee contributions (if the validator is in a sync committee, which is unlikely), incurring penalties and missed rewards. - Potentially missed rewards by missing a block proposal (if the validator is an elected block proposer, which is unlikely). The loss of rewards and penalties incurred due to the missed duties will be very small in -dollar-values. Generally, they will equate to around one US dollar (at August 2021 figures) or about -2% of the reward for one validator for one day. Since DP costs so little but can protect a user from +dollar-values. Neglecting block proposals, generally they will equate to around 0.00002 ETH (equivalent to USD 0.04 assuming ETH is trading at USD 2000), or less than +1% of the reward for one validator for one day. Since DP costs so little but can protect a user from slashing, many users will consider this a worthwhile trade-off. The 2-3 epochs of missed duties will be incurred whenever the VC is started (e.g., after an update From 3cac6d9ed50d14e9aec0d90cc0fb1bc484c10605 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 22 Jun 2023 02:14:56 +0000 Subject: [PATCH 12/25] Configure the `validator/register_validator` batch size via the CLI (#4399) ## Issue Addressed NA ## Proposed Changes Adds the `--validator-registration-batch-size` flag to the VC to allow runtime configuration of the number of validators POSTed to the [`validator/register_validator`](https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Validator/registerValidator) endpoint. There are builders (Agnostic and Eden) that are timing out with `regsiterValidator` requests with ~400 validators, even with a 9 second timeout. Exposing the batch size will help tune batch sizes to (hopefully) avoid this. This PR should not change the behavior of Lighthouse when the new flag is not provided (i.e., the same default value is used). ## Additional Info NA --- lighthouse/tests/validator_client.rs | 21 +++++++++++++++++++++ validator_client/src/cli.rs | 10 ++++++++++ validator_client/src/config.rs | 9 +++++++++ validator_client/src/lib.rs | 1 + validator_client/src/preparation_service.rs | 21 ++++++++++++++++----- 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 8c1f0477c..27d7c10e9 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -499,3 +499,24 @@ fn latency_measurement_service() { assert!(!config.enable_latency_measurement_service); }); } + +#[test] +fn validator_registration_batch_size() { + CommandLineTest::new().run().with_config(|config| { + assert_eq!(config.validator_registration_batch_size, 500); + }); + CommandLineTest::new() + .flag("validator-registration-batch-size", Some("100")) + .run() + .with_config(|config| { + assert_eq!(config.validator_registration_batch_size, 100); + }); +} + +#[test] +#[should_panic] +fn validator_registration_batch_size_zero_value() { + CommandLineTest::new() + .flag("validator-registration-batch-size", Some("0")) + .run(); +} diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 6e199cb17..436b8eb4d 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -333,6 +333,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("true") .takes_value(true), ) + .arg( + Arg::with_name("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") + .takes_value(true), + ) /* * Experimental/development options. */ diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index fa297dcfe..e0dd12e10 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -77,6 +77,8 @@ pub struct Config { pub disable_run_on_all: bool, /// Enables a service which attempts to measure latency between the VC and BNs. pub enable_latency_measurement_service: bool, + /// Defines the number of validators per `validator/register_validator` request sent to the BN. + pub validator_registration_batch_size: usize, } impl Default for Config { @@ -117,6 +119,7 @@ impl Default for Config { gas_limit: None, disable_run_on_all: false, enable_latency_measurement_service: true, + validator_registration_batch_size: 500, } } } @@ -380,6 +383,12 @@ impl Config { config.enable_latency_measurement_service = parse_optional(cli_args, "latency-measurement-service")?.unwrap_or(true); + config.validator_registration_batch_size = + parse_required(cli_args, "validator-registration-batch-size")?; + if config.validator_registration_batch_size == 0 { + return Err("validator-registration-batch-size cannot be 0".to_string()); + } + /* * Experimental */ diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 6e4a8da6a..60943a260 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -487,6 +487,7 @@ impl ProductionValidatorClient { .beacon_nodes(beacon_nodes.clone()) .runtime_context(context.service_context("preparation".into())) .builder_registration_timestamp_override(config.builder_registration_timestamp_override) + .validator_registration_batch_size(config.validator_registration_batch_size) .build()?; let sync_committee_service = SyncCommitteeService::new( diff --git a/validator_client/src/preparation_service.rs b/validator_client/src/preparation_service.rs index 5bd93a505..7d6e1744c 100644 --- a/validator_client/src/preparation_service.rs +++ b/validator_client/src/preparation_service.rs @@ -23,9 +23,6 @@ const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2; /// Number of epochs to wait before re-submitting validator registration. const EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION: u64 = 1; -/// The number of validator registrations to include per request to the beacon node. -const VALIDATOR_REGISTRATION_BATCH_SIZE: usize = 500; - /// Builds an `PreparationService`. pub struct PreparationServiceBuilder { validator_store: Option>>, @@ -33,6 +30,7 @@ pub struct PreparationServiceBuilder { beacon_nodes: Option>>, context: Option>, builder_registration_timestamp_override: Option, + validator_registration_batch_size: Option, } impl PreparationServiceBuilder { @@ -43,6 +41,7 @@ impl PreparationServiceBuilder { beacon_nodes: None, context: None, builder_registration_timestamp_override: None, + validator_registration_batch_size: None, } } @@ -74,6 +73,14 @@ impl PreparationServiceBuilder { self } + pub fn validator_registration_batch_size( + mut self, + validator_registration_batch_size: usize, + ) -> Self { + self.validator_registration_batch_size = Some(validator_registration_batch_size); + self + } + pub fn build(self) -> Result, String> { Ok(PreparationService { inner: Arc::new(Inner { @@ -91,6 +98,9 @@ impl PreparationServiceBuilder { .ok_or("Cannot build PreparationService without runtime_context")?, builder_registration_timestamp_override: self .builder_registration_timestamp_override, + validator_registration_batch_size: self.validator_registration_batch_size.ok_or( + "Cannot build PreparationService without validator_registration_batch_size", + )?, validator_registration_cache: RwLock::new(HashMap::new()), }), }) @@ -107,6 +117,7 @@ pub struct Inner { // Used to track unpublished validator registration changes. validator_registration_cache: RwLock>, + validator_registration_batch_size: usize, } #[derive(Hash, Eq, PartialEq, Debug, Clone)] @@ -447,7 +458,7 @@ impl PreparationService { } if !signed.is_empty() { - for batch in signed.chunks(VALIDATOR_REGISTRATION_BATCH_SIZE) { + for batch in signed.chunks(self.validator_registration_batch_size) { match self .beacon_nodes .first_success( @@ -462,7 +473,7 @@ impl PreparationService { Ok(()) => info!( log, "Published validator registrations to the builder network"; - "count" => registration_data_len, + "count" => batch.len(), ), Err(e) => warn!( log, From 33c942ff035268fb1ff1078707c1e5a4301a902b Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 22 Jun 2023 02:14:57 +0000 Subject: [PATCH 13/25] Add support for updating validator graffiti (#4417) ## Issue Addressed #4386 ## Proposed Changes The original proposal described in the issue adds a new endpoint to support updating validator graffiti, but I realized we already have an endpoint that we use for updating various validator fields in memory and in the validator definitions file, so I think that would be the best place to add this to. ### API endpoint `PATCH lighthouse/validators/{validator_pubkey}` This endpoint updates the graffiti in both the [ validator definition file](https://lighthouse-book.sigmaprime.io/graffiti.html#2-setting-the-graffiti-in-the-validator_definitionsyml) and the in memory `InitializedValidators`. In the next block proposal, the new graffiti will be used. Note that the [`--graffiti-file`](https://lighthouse-book.sigmaprime.io/graffiti.html#1-using-the---graffiti-file-flag-on-the-validator-client) flag has a priority over the validator definitions file, so if the caller attempts to update the graffiti while the `--graffiti-file` flag is present, the endpoint will return an error (Bad request 400). ## Tasks - [x] Add graffiti update support to `PATCH lighthouse/validators/{validator_pubkey}` - [x] Return error if user tries to update graffiti while the `--graffiti-flag` is set - [x] Update Lighthouse book --- book/src/api-vc-endpoints.md | 3 +- book/src/graffiti.md | 24 ++++++ common/eth2/src/lighthouse_vc/http_client.rs | 3 + common/eth2/src/lighthouse_vc/types.rs | 3 + validator_client/src/http_api/mod.rs | 17 +++- validator_client/src/http_api/tests.rs | 84 ++++++++++++++++++- .../src/http_api/tests/keystores.rs | 2 +- .../src/initialized_validators.rs | 18 +++- 8 files changed, 143 insertions(+), 11 deletions(-) diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index 406c5b1f0..ee0cfd200 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -426,7 +426,8 @@ Example Response Body ## `PATCH /lighthouse/validators/:voting_pubkey` -Update some values for the validator with `voting_pubkey`. The following example updates a validator from `enabled: true` to `enabled: false` +Update some values for the validator with `voting_pubkey`. Possible fields: `enabled`, `gas_limit`, `builder_proposals`, +and `graffiti`. The following example updates a validator from `enabled: true` to `enabled: false`. ### HTTP Specification diff --git a/book/src/graffiti.md b/book/src/graffiti.md index 75c2a86dd..302f8f967 100644 --- a/book/src/graffiti.md +++ b/book/src/graffiti.md @@ -29,6 +29,8 @@ Lighthouse will first search for the graffiti corresponding to the public key of ### 2. Setting the graffiti in the `validator_definitions.yml` Users can set validator specific graffitis in `validator_definitions.yml` with the `graffiti` key. This option is recommended for static setups where the graffitis won't change on every new block proposal. +You can also update the graffitis in the `validator_definitions.yml` file using the [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey). See example in [Set Graffiti via HTTP](#set-graffiti-via-http). + Below is an example of the validator_definitions.yml with validator specific graffitis: ``` --- @@ -62,3 +64,25 @@ Usage: `lighthouse bn --graffiti fortytwo` > 3. If graffiti is not specified in `validator_definitions.yml`, load the graffiti passed in the `--graffiti` flag on the validator client. > 4. If the `--graffiti` flag on the validator client is not passed, load the graffiti passed in the `--graffiti` flag on the beacon node. > 4. If the `--graffiti` flag is not passed, load the default Lighthouse graffiti. + +### Set Graffiti via HTTP + +Use the [Lighthouse API](api-vc-endpoints.md) to set graffiti on a per-validator basis. This method updates the graffiti +both in memory and in the `validator_definitions.yml` file. The new graffiti will be used in the next block proposal +without requiring a validator client restart. + +Refer to [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey) for API specification. + +#### Example Command + +```bash +DATADIR=/var/lib/lighthouse +curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '{ + "graffiti": "Mr F was here" +}' | jq +``` + +A `null` response indicates that the request is successful. \ No newline at end of file diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index e576cfcb3..720d8c779 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -16,6 +16,7 @@ use std::path::Path; pub use reqwest; pub use reqwest::{Response, StatusCode, Url}; +use types::graffiti::GraffitiString; /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`). @@ -467,6 +468,7 @@ impl ValidatorClientHttpClient { enabled: Option, gas_limit: Option, builder_proposals: Option, + graffiti: Option, ) -> Result<(), Error> { let mut path = self.server.full.clone(); @@ -482,6 +484,7 @@ impl ValidatorClientHttpClient { enabled, gas_limit, builder_proposals, + graffiti, }, ) .await diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index dd2ed0322..7bbe041db 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -83,6 +83,9 @@ pub struct ValidatorPatchRequest { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub builder_proposals: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub graffiti: Option, } #[derive(Clone, PartialEq, Serialize, Deserialize)] diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index fa6cde3ed..f08c8da1b 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -357,7 +357,7 @@ pub fn serve( .and(warp::path("graffiti")) .and(warp::path::end()) .and(validator_store_filter.clone()) - .and(graffiti_file_filter) + .and(graffiti_file_filter.clone()) .and(graffiti_flag_filter) .and(signer.clone()) .and(log_filter.clone()) @@ -617,18 +617,27 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(validator_store_filter.clone()) + .and(graffiti_file_filter) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( |validator_pubkey: PublicKey, body: api_types::ValidatorPatchRequest, validator_store: Arc>, + graffiti_file: Option, signer, task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { + if body.graffiti.is_some() && graffiti_file.is_some() { + return Err(warp_utils::reject::custom_bad_request( + "Unable to update graffiti as the \"--graffiti-file\" flag is set" + .to_string(), + )); + } + + let maybe_graffiti = body.graffiti.clone().map(Into::into); let initialized_validators_rw_lock = validator_store.initialized_validators(); let mut initialized_validators = initialized_validators_rw_lock.write(); - match ( initialized_validators.is_enabled(&validator_pubkey), initialized_validators.validator(&validator_pubkey.compress()), @@ -641,7 +650,8 @@ pub fn serve( if Some(is_enabled) == body.enabled && initialized_validator.get_gas_limit() == body.gas_limit && initialized_validator.get_builder_proposals() - == body.builder_proposals => + == body.builder_proposals + && initialized_validator.get_graffiti() == maybe_graffiti => { Ok(()) } @@ -654,6 +664,7 @@ pub fn serve( body.enabled, body.gas_limit, body.builder_proposals, + body.graffiti, ), ) .map_err(|e| { diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index 84d2fe437..dbb9d4d62 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -28,12 +28,14 @@ use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr}; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; use tempfile::{tempdir, TempDir}; use tokio::runtime::Runtime; use tokio::sync::oneshot; +use types::graffiti::GraffitiString; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -533,7 +535,7 @@ impl ApiTester { let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; self.client - .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None) + .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None, None) .await .unwrap(); @@ -575,7 +577,13 @@ impl ApiTester { let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; self.client - .patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None) + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + Some(gas_limit), + None, + None, + ) .await .unwrap(); @@ -602,6 +610,7 @@ impl ApiTester { None, None, Some(builder_proposals), + None, ) .await .unwrap(); @@ -620,6 +629,34 @@ impl ApiTester { self } + + pub async fn set_graffiti(self, index: usize, graffiti: &str) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + let graffiti_str = GraffitiString::from_str(graffiti).unwrap(); + self.client + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + None, + None, + Some(graffiti_str), + ) + .await + .unwrap(); + + self + } + + pub async fn assert_graffiti(self, index: usize, graffiti: &str) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + let graffiti_str = GraffitiString::from_str(graffiti).unwrap(); + assert_eq!( + self.validator_store.graffiti(&validator.voting_pubkey), + Some(graffiti_str.into()) + ); + + self + } } struct HdValidatorScenario { @@ -723,7 +760,13 @@ fn routes_with_invalid_auth() { .await .test_with_invalid_auth(|client| async move { client - .patch_lighthouse_validators(&PublicKeyBytes::empty(), Some(false), None, None) + .patch_lighthouse_validators( + &PublicKeyBytes::empty(), + Some(false), + None, + None, + None, + ) .await }) .await @@ -931,6 +974,41 @@ fn validator_builder_proposals() { }); } +#[test] +fn validator_graffiti() { + let runtime = build_runtime(); + let weak_runtime = Arc::downgrade(&runtime); + runtime.block_on(async { + ApiTester::new(weak_runtime) + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_graffiti(0, "Mr F was here") + .await + .assert_graffiti(0, "Mr F was here") + .await + // Test setting graffiti while the validator is disabled + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_graffiti(0, "Mr F was here again") + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_graffiti(0, "Mr F was here again") + .await + }); +} + #[test] fn keystore_validator_creation() { let runtime = build_runtime(); diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/src/http_api/tests/keystores.rs index 769d8a1d4..7120ee5f9 100644 --- a/validator_client/src/http_api/tests/keystores.rs +++ b/validator_client/src/http_api/tests/keystores.rs @@ -468,7 +468,7 @@ fn import_and_delete_conflicting_web3_signer_keystores() { for pubkey in &pubkeys { tester .client - .patch_lighthouse_validators(pubkey, Some(false), None, None) + .patch_lighthouse_validators(pubkey, Some(false), None, None, None) .await .unwrap(); } diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 468fc2b06..090acbe96 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -27,6 +27,7 @@ use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +use types::graffiti::GraffitiString; use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use url::{ParseError, Url}; use validator_dir::Builder as ValidatorDirBuilder; @@ -147,6 +148,10 @@ impl InitializedValidator { pub fn get_index(&self) -> Option { self.index } + + pub fn get_graffiti(&self) -> Option { + self.graffiti + } } fn open_keystore(path: &Path) -> Result { @@ -671,8 +676,8 @@ impl InitializedValidators { self.validators.get(public_key) } - /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, and `builder_proposals` - /// values. + /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, + /// `builder_proposals`, and `graffiti` values. /// /// ## Notes /// @@ -682,7 +687,7 @@ impl InitializedValidators { /// /// If a `gas_limit` is included in the call to this function, it will also be updated and saved /// to disk. If `gas_limit` is `None` the `gas_limit` *will not* be unset in `ValidatorDefinition` - /// or `InitializedValidator`. The same logic applies to `builder_proposals`. + /// or `InitializedValidator`. The same logic applies to `builder_proposals` and `graffiti`. /// /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. pub async fn set_validator_definition_fields( @@ -691,6 +696,7 @@ impl InitializedValidators { enabled: Option, gas_limit: Option, builder_proposals: Option, + graffiti: Option, ) -> Result<(), Error> { if let Some(def) = self .definitions @@ -708,6 +714,9 @@ impl InitializedValidators { if let Some(builder_proposals) = builder_proposals { def.builder_proposals = Some(builder_proposals); } + if let Some(graffiti) = graffiti.clone() { + def.graffiti = Some(graffiti); + } } self.update_validators().await?; @@ -723,6 +732,9 @@ impl InitializedValidators { if let Some(builder_proposals) = builder_proposals { val.builder_proposals = Some(builder_proposals); } + if let Some(graffiti) = graffiti { + val.graffiti = Some(graffiti.into()); + } } self.definitions From 6d585b5885955db46d8f3ec897033539b04823ad Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 22 Jun 2023 02:14:58 +0000 Subject: [PATCH 14/25] Add `lint-fix` task to automatically fix some Clippy warnings. (#4419) ## Issue Addressed This PR adds a new `lint-fix` task to automatically fix simple Clippy warnings using `cargo clippy --fix`. Usage: ``` make lint-fix ``` --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8e7f3fc32..b833686e1 100644 --- a/Makefile +++ b/Makefile @@ -170,7 +170,7 @@ test-full: cargo-fmt test-release test-debug test-ef test-exec-engine # Lints the code for bad style and potentially unsafe arithmetic using Clippy. # Clippy lints are opt-in per-crate for now. By default, everything is allowed except for performance and correctness lints. lint: - cargo clippy --workspace --tests -- \ + cargo clippy --workspace --tests $(EXTRA_CLIPPY_OPTS) -- \ -D clippy::fn_to_numeric_cast_any \ -D warnings \ -A clippy::derive_partial_eq_without_eq \ @@ -180,6 +180,10 @@ lint: -A clippy::question-mark \ -A clippy::uninlined-format-args +# Lints the code using Clippy and automatically fix some simple compiler warnings. +lint-fix: + EXTRA_CLIPPY_OPTS="--fix --allow-staged --allow-dirty" $(MAKE) lint + nightly-lint: cp .github/custom/clippy.toml . cargo +$(CLIPPY_PINNED_NIGHTLY) clippy --workspace --tests --release -- \ From cc780aae3e0cb89649086a3b63cb02a4f97f7ae2 Mon Sep 17 00:00:00 2001 From: Mac L Date: Thu, 22 Jun 2023 02:14:59 +0000 Subject: [PATCH 15/25] Bump `openssl` deps (#4421) ## Proposed Changes Bump the `openssl` deps to resolve the `cargo-audit` failure caused by https://rustsec.org/advisories/RUSTSEC-2023-0044.html --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18276b3ea..823918371 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5692,9 +5692,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ "bitflags", "cfg-if", @@ -5733,9 +5733,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ "cc", "libc", From 448d3ec9b3a7c90c5209aaa1d50091f2b7303dcf Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 27 Jun 2023 01:06:49 +0000 Subject: [PATCH 16/25] Aggregate subsets (#3493) ## Issue Addressed Resolves #3238 ## Proposed Changes Please list or describe the changes introduced by this PR. ## Additional Info Please provide any additional information. For example, future considerations or information useful for reviewers. --- Cargo.lock | 5 +- beacon_node/beacon_chain/Cargo.toml | 3 +- .../src/attestation_verification.rs | 36 ++-- beacon_node/beacon_chain/src/metrics.rs | 11 + .../beacon_chain/src/observed_aggregates.rs | 200 +++++++++++++----- .../src/sync_committee_verification.rs | 38 +++- .../tests/attestation_verification.rs | 4 +- .../tests/sync_committee_verification.rs | 12 +- beacon_node/execution_layer/Cargo.toml | 2 +- beacon_node/http_api/src/lib.rs | 2 +- beacon_node/http_api/src/sync_committees.rs | 2 +- beacon_node/lighthouse_network/Cargo.toml | 2 +- beacon_node/network/Cargo.toml | 2 +- .../beacon_processor/worker/gossip_methods.rs | 4 +- consensus/cached_tree_hash/Cargo.toml | 2 +- consensus/state_processing/Cargo.toml | 2 +- consensus/types/Cargo.toml | 2 +- 17 files changed, 236 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 823918371..700aecb83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,6 +661,7 @@ dependencies = [ "tokio", "tokio-stream", "tree_hash", + "tree_hash_derive", "types", "unused_port", ] @@ -7849,9 +7850,9 @@ dependencies = [ [[package]] name = "ssz_types" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8052a1004e979c0be24b9e55940195553103cc57d0b34f7e2c4e32793325e402" +checksum = "e43767964a80b2fdeda7a79a57a2b6cbca966688d5b81da8fe91140a94f552a1" dependencies = [ "arbitrary", "derivative", diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 27d07e333..7f884f561 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -32,9 +32,10 @@ sloggers = { version = "2.1.1", features = ["json"] } slot_clock = { path = "../../common/slot_clock" } ethereum_hashing = "1.0.0-beta.2" ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" +ssz_types = "0.5.3" ethereum_ssz_derive = "0.5.0" state_processing = { path = "../../consensus/state_processing" } +tree_hash_derive = "0.5.0" tree_hash = "0.5.0" types = { path = "../../consensus/types" } tokio = "1.14.0" diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 04f601fad..6df0758b2 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -117,14 +117,14 @@ pub enum Error { /// /// The peer has sent an invalid message. AggregatorPubkeyUnknown(u64), - /// The attestation has been seen before; either in a block, on the gossip network or from a - /// local validator. + /// The attestation or a superset of this attestation's aggregations bits for the same data + /// has been seen before; either in a block, on the gossip network or from a local validator. /// /// ## Peer scoring /// /// It's unclear if this attestation is valid, however we have already observed it and do not /// need to observe it again. - AttestationAlreadyKnown(Hash256), + AttestationSupersetKnown(Hash256), /// There has already been an aggregation observed for this validator, we refuse to process a /// second. /// @@ -268,7 +268,7 @@ enum CheckAttestationSignature { struct IndexedAggregatedAttestation<'a, T: BeaconChainTypes> { signed_aggregate: &'a SignedAggregateAndProof, indexed_attestation: IndexedAttestation, - attestation_root: Hash256, + attestation_data_root: Hash256, } /// Wraps a `Attestation` that has been verified up until the point that an `IndexedAttestation` can @@ -467,14 +467,17 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { } // Ensure the valid aggregated attestation has not already been seen locally. - let attestation_root = attestation.tree_hash_root(); + let attestation_data = &attestation.data; + let attestation_data_root = attestation_data.tree_hash_root(); + if chain .observed_attestations .write() - .is_known(attestation, attestation_root) + .is_known_subset(attestation, attestation_data_root) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::AttestationAlreadyKnown(attestation_root)); + metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); + return Err(Error::AttestationSupersetKnown(attestation_data_root)); } let aggregator_index = signed_aggregate.message.aggregator_index; @@ -520,7 +523,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { if attestation.aggregation_bits.is_zero() { Err(Error::EmptyAggregationBitfield) } else { - Ok(attestation_root) + Ok(attestation_data_root) } } @@ -533,7 +536,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { let attestation = &signed_aggregate.message.aggregate; let aggregator_index = signed_aggregate.message.aggregator_index; - let attestation_root = match Self::verify_early_checks(signed_aggregate, chain) { + let attestation_data_root = match Self::verify_early_checks(signed_aggregate, chain) { Ok(root) => root, Err(e) => return Err(SignatureNotChecked(&signed_aggregate.message.aggregate, e)), }; @@ -568,7 +571,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { Ok(IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, - attestation_root, + attestation_data_root, }) } } @@ -577,7 +580,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { /// Run the checks that happen after the indexed attestation and signature have been checked. fn verify_late_checks( signed_aggregate: &SignedAggregateAndProof, - attestation_root: Hash256, + attestation_data_root: Hash256, chain: &BeaconChain, ) -> Result<(), Error> { let attestation = &signed_aggregate.message.aggregate; @@ -587,13 +590,14 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { // // It's important to double check that the attestation is not already known, otherwise two // attestations processed at the same time could be published. - if let ObserveOutcome::AlreadyKnown = chain + if let ObserveOutcome::Subset = chain .observed_attestations .write() - .observe_item(attestation, Some(attestation_root)) + .observe_item(attestation, Some(attestation_data_root)) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::AttestationAlreadyKnown(attestation_root)); + metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); + return Err(Error::AttestationSupersetKnown(attestation_data_root)); } // Observe the aggregator so we don't process another aggregate from them. @@ -653,7 +657,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { let IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, - attestation_root, + attestation_data_root, } = signed_aggregate; match check_signature { @@ -677,7 +681,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { CheckAttestationSignature::No => (), }; - if let Err(e) = Self::verify_late_checks(signed_aggregate, attestation_root, chain) { + if let Err(e) = Self::verify_late_checks(signed_aggregate, attestation_data_root, chain) { return Err(SignatureValid(indexed_attestation, e)); } diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index d0f695062..dff663ded 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -998,6 +998,17 @@ lazy_static! { "light_client_optimistic_update_verification_success_total", "Number of light client optimistic updates verified for gossip" ); + /* + * Aggregate subset metrics + */ + pub static ref SYNC_CONTRIBUTION_SUBSETS: Result = try_create_int_counter( + "beacon_sync_contribution_subsets_total", + "Count of new sync contributions that are subsets of already known aggregates" + ); + pub static ref AGGREGATED_ATTESTATION_SUBSETS: Result = try_create_int_counter( + "beacon_aggregated_attestation_subsets_total", + "Count of new aggregated attestations that are subsets of already known aggregates" + ); } /// Scrape the `beacon_chain` for metrics that are not constantly updated (e.g., the present slot, diff --git a/beacon_node/beacon_chain/src/observed_aggregates.rs b/beacon_node/beacon_chain/src/observed_aggregates.rs index bb0132f5f..18a761e29 100644 --- a/beacon_node/beacon_chain/src/observed_aggregates.rs +++ b/beacon_node/beacon_chain/src/observed_aggregates.rs @@ -1,7 +1,9 @@ //! Provides an `ObservedAggregates` struct which allows us to reject aggregated attestations or //! sync committee contributions if we've already seen them. -use std::collections::HashSet; +use crate::sync_committee_verification::SyncCommitteeData; +use ssz_types::{BitList, BitVector}; +use std::collections::HashMap; use std::marker::PhantomData; use tree_hash::TreeHash; use types::consts::altair::{ @@ -10,8 +12,16 @@ use types::consts::altair::{ use types::slot_data::SlotData; use types::{Attestation, EthSpec, Hash256, Slot, SyncCommitteeContribution}; -pub type ObservedSyncContributions = ObservedAggregates, E>; -pub type ObservedAggregateAttestations = ObservedAggregates, E>; +pub type ObservedSyncContributions = ObservedAggregates< + SyncCommitteeContribution, + E, + BitVector<::SyncSubcommitteeSize>, +>; +pub type ObservedAggregateAttestations = ObservedAggregates< + Attestation, + E, + BitList<::MaxValidatorsPerCommittee>, +>; /// A trait use to associate capacity constants with the type being stored in `ObservedAggregates`. pub trait Consts { @@ -69,10 +79,81 @@ impl Consts for SyncCommitteeContribution { } } +/// A trait for types that implement a behaviour where one object of that type +/// can be a subset/superset of another. +/// This trait allows us to be generic over the aggregate item that we store in the cache that +/// we want to prevent duplicates/subsets for. +pub trait SubsetItem { + /// The item that is stored for later comparison with new incoming aggregate items. + type Item; + + /// Returns `true` if `self` is a non-strict subset of `other` and `false` otherwise. + fn is_subset(&self, other: &Self::Item) -> bool; + + /// Returns `true` if `self` is a non-strict superset of `other` and `false` otherwise. + fn is_superset(&self, other: &Self::Item) -> bool; + + /// Returns the item that gets stored in `ObservedAggregates` for later subset + /// comparison with incoming aggregates. + fn get_item(&self) -> Self::Item; + + /// Returns a unique value that keys the object to the item that is being stored + /// in `ObservedAggregates`. + fn root(&self) -> Hash256; +} + +impl SubsetItem for Attestation { + type Item = BitList; + fn is_subset(&self, other: &Self::Item) -> bool { + self.aggregation_bits.is_subset(other) + } + + fn is_superset(&self, other: &Self::Item) -> bool { + other.is_subset(&self.aggregation_bits) + } + + /// Returns the sync contribution aggregation bits. + fn get_item(&self) -> Self::Item { + self.aggregation_bits.clone() + } + + /// Returns the hash tree root of the attestation data. + fn root(&self) -> Hash256 { + self.data.tree_hash_root() + } +} + +impl SubsetItem for SyncCommitteeContribution { + type Item = BitVector; + fn is_subset(&self, other: &Self::Item) -> bool { + self.aggregation_bits.is_subset(other) + } + + fn is_superset(&self, other: &Self::Item) -> bool { + other.is_subset(&self.aggregation_bits) + } + + /// Returns the sync contribution aggregation bits. + fn get_item(&self) -> Self::Item { + self.aggregation_bits.clone() + } + + /// Returns the hash tree root of the root, slot and subcommittee index + /// of the sync contribution. + fn root(&self) -> Hash256 { + SyncCommitteeData { + root: self.beacon_block_root, + slot: self.slot, + subcommittee_index: self.subcommittee_index, + } + .tree_hash_root() + } +} + #[derive(Debug, PartialEq)] pub enum ObserveOutcome { - /// This item was already known. - AlreadyKnown, + /// This item is a non-strict subset of an already known item. + Subset, /// This was the first time this item was observed. New, } @@ -94,26 +175,28 @@ pub enum Error { }, } -/// A `HashSet` that contains entries related to some `Slot`. -struct SlotHashSet { - set: HashSet, +/// A `HashMap` that contains entries related to some `Slot`. +struct SlotHashSet { + /// Contains a vector of maximally-sized aggregation bitfields/bitvectors + /// such that no bitfield/bitvector is a subset of any other in the list. + map: HashMap>, slot: Slot, max_capacity: usize, } -impl SlotHashSet { +impl SlotHashSet { pub fn new(slot: Slot, initial_capacity: usize, max_capacity: usize) -> Self { Self { slot, - set: HashSet::with_capacity(initial_capacity), + map: HashMap::with_capacity(initial_capacity), max_capacity, } } /// Store the items in self so future observations recognise its existence. - pub fn observe_item( + pub fn observe_item>( &mut self, - item: &T, + item: &S, root: Hash256, ) -> Result { if item.get_slot() != self.slot { @@ -123,29 +206,45 @@ impl SlotHashSet { }); } - if self.set.contains(&root) { - Ok(ObserveOutcome::AlreadyKnown) - } else { - // Here we check to see if this slot has reached the maximum observation count. - // - // The resulting behaviour is that we are no longer able to successfully observe new - // items, however we will continue to return `is_known` values. We could also - // disable `is_known`, however then we would stop forwarding items across the - // gossip network and I think that this is a worse case than sending some invalid ones. - // The underlying libp2p network is responsible for removing duplicate messages, so - // this doesn't risk a broadcast loop. - if self.set.len() >= self.max_capacity { - return Err(Error::ReachedMaxObservationsPerSlot(self.max_capacity)); + if let Some(aggregates) = self.map.get_mut(&root) { + for existing in aggregates { + // Check if `item` is a subset of any of the observed aggregates + if item.is_subset(existing) { + return Ok(ObserveOutcome::Subset); + // Check if `item` is a superset of any of the observed aggregates + // If true, we replace the new item with its existing subset. This allows us + // to hold fewer items in the list. + } else if item.is_superset(existing) { + *existing = item.get_item(); + return Ok(ObserveOutcome::New); + } } - - self.set.insert(root); - - Ok(ObserveOutcome::New) } + + // Here we check to see if this slot has reached the maximum observation count. + // + // The resulting behaviour is that we are no longer able to successfully observe new + // items, however we will continue to return `is_known_subset` values. We could also + // disable `is_known_subset`, however then we would stop forwarding items across the + // gossip network and I think that this is a worse case than sending some invalid ones. + // The underlying libp2p network is responsible for removing duplicate messages, so + // this doesn't risk a broadcast loop. + if self.map.len() >= self.max_capacity { + return Err(Error::ReachedMaxObservationsPerSlot(self.max_capacity)); + } + + let item = item.get_item(); + self.map.entry(root).or_default().push(item); + Ok(ObserveOutcome::New) } - /// Indicates if `item` has been observed before. - pub fn is_known(&self, item: &T, root: Hash256) -> Result { + /// Check if `item` is a non-strict subset of any of the already observed aggregates for + /// the given root and slot. + pub fn is_known_subset>( + &self, + item: &S, + root: Hash256, + ) -> Result { if item.get_slot() != self.slot { return Err(Error::IncorrectSlot { expected: self.slot, @@ -153,25 +252,28 @@ impl SlotHashSet { }); } - Ok(self.set.contains(&root)) + Ok(self + .map + .get(&root) + .map_or(false, |agg| agg.iter().any(|val| item.is_subset(val)))) } /// The number of observed items in `self`. pub fn len(&self) -> usize { - self.set.len() + self.map.len() } } /// Stores the roots of objects for some number of `Slots`, so we can determine if /// these have previously been seen on the network. -pub struct ObservedAggregates { +pub struct ObservedAggregates { lowest_permissible_slot: Slot, - sets: Vec, + sets: Vec>, _phantom_spec: PhantomData, _phantom_tree_hash: PhantomData, } -impl Default for ObservedAggregates { +impl Default for ObservedAggregates { fn default() -> Self { Self { lowest_permissible_slot: Slot::new(0), @@ -182,17 +284,17 @@ impl Default for ObservedAggregates } } -impl ObservedAggregates { - /// Store the root of `item` in `self`. +impl, E: EthSpec, I> ObservedAggregates { + /// Store `item` in `self` keyed at `root`. /// - /// `root` must equal `item.tree_hash_root()`. + /// `root` must equal `item.root::()`. pub fn observe_item( &mut self, item: &T, root_opt: Option, ) -> Result { let index = self.get_set_index(item.get_slot())?; - let root = root_opt.unwrap_or_else(|| item.tree_hash_root()); + let root = root_opt.unwrap_or_else(|| item.root()); self.sets .get_mut(index) @@ -200,17 +302,18 @@ impl ObservedAggregates { .and_then(|set| set.observe_item(item, root)) } - /// Check to see if the `root` of `item` is in self. + /// Check if `item` is a non-strict subset of any of the already observed aggregates for + /// the given root and slot. /// - /// `root` must equal `a.tree_hash_root()`. + /// `root` must equal `item.root::()`. #[allow(clippy::wrong_self_convention)] - pub fn is_known(&mut self, item: &T, root: Hash256) -> Result { + pub fn is_known_subset(&mut self, item: &T, root: Hash256) -> Result { let index = self.get_set_index(item.get_slot())?; self.sets .get(index) .ok_or(Error::InvalidSetIndex(index)) - .and_then(|set| set.is_known(item, root)) + .and_then(|set| set.is_known_subset(item, root)) } /// The maximum number of slots that items are stored for. @@ -296,7 +399,6 @@ impl ObservedAggregates { #[cfg(not(debug_assertions))] mod tests { use super::*; - use tree_hash::TreeHash; use types::{test_utils::test_random_instance, Hash256}; type E = types::MainnetEthSpec; @@ -330,7 +432,7 @@ mod tests { for a in &items { assert_eq!( - store.is_known(a, a.tree_hash_root()), + store.is_known_subset(a, a.root()), Ok(false), "should indicate an unknown attestation is unknown" ); @@ -343,13 +445,13 @@ mod tests { for a in &items { assert_eq!( - store.is_known(a, a.tree_hash_root()), + store.is_known_subset(a, a.root()), Ok(true), "should indicate a known attestation is known" ); assert_eq!( - store.observe_item(a, Some(a.tree_hash_root())), - Ok(ObserveOutcome::AlreadyKnown), + store.observe_item(a, Some(a.root())), + Ok(ObserveOutcome::Subset), "should acknowledge an existing attestation" ); } diff --git a/beacon_node/beacon_chain/src/sync_committee_verification.rs b/beacon_node/beacon_chain/src/sync_committee_verification.rs index 14cdc2400..246bb12cc 100644 --- a/beacon_node/beacon_chain/src/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/src/sync_committee_verification.rs @@ -37,6 +37,7 @@ use bls::{verify_signature_sets, PublicKeyBytes}; use derivative::Derivative; use safe_arith::ArithError; use slot_clock::SlotClock; +use ssz_derive::{Decode, Encode}; use state_processing::per_block_processing::errors::SyncCommitteeMessageValidationError; use state_processing::signature_sets::{ signed_sync_aggregate_selection_proof_signature_set, signed_sync_aggregate_signature_set, @@ -47,6 +48,7 @@ use std::borrow::Cow; use std::collections::HashMap; use strum::AsRefStr; use tree_hash::TreeHash; +use tree_hash_derive::TreeHash; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; use types::slot_data::SlotData; use types::sync_committee::Error as SyncCommitteeError; @@ -110,14 +112,14 @@ pub enum Error { /// /// The peer has sent an invalid message. AggregatorPubkeyUnknown(u64), - /// The sync contribution has been seen before; either in a block, on the gossip network or from a - /// local validator. + /// The sync contribution or a superset of this sync contribution's aggregation bits for the same data + /// has been seen before; either in a block on the gossip network or from a local validator. /// /// ## Peer scoring /// /// It's unclear if this sync contribution is valid, however we have already observed it and do not /// need to observe it again. - SyncContributionAlreadyKnown(Hash256), + SyncContributionSupersetKnown(Hash256), /// There has already been an aggregation observed for this validator, we refuse to process a /// second. /// @@ -268,6 +270,14 @@ pub struct VerifiedSyncContribution { participant_pubkeys: Vec, } +/// The sync contribution data. +#[derive(Encode, Decode, TreeHash)] +pub struct SyncCommitteeData { + pub slot: Slot, + pub root: Hash256, + pub subcommittee_index: u64, +} + /// Wraps a `SyncCommitteeMessage` that has been verified for propagation on the gossip network. #[derive(Clone)] pub struct VerifiedSyncCommitteeMessage { @@ -314,15 +324,22 @@ impl VerifiedSyncContribution { return Err(Error::AggregatorNotInCommittee { aggregator_index }); }; - // Ensure the valid sync contribution has not already been seen locally. - let contribution_root = contribution.tree_hash_root(); + // Ensure the valid sync contribution or its superset has not already been seen locally. + let contribution_data_root = SyncCommitteeData { + slot: contribution.slot, + root: contribution.beacon_block_root, + subcommittee_index: contribution.subcommittee_index, + } + .tree_hash_root(); + if chain .observed_sync_contributions .write() - .is_known(contribution, contribution_root) + .is_known_subset(contribution, contribution_data_root) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::SyncContributionAlreadyKnown(contribution_root)); + metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_SUBSETS); + return Err(Error::SyncContributionSupersetKnown(contribution_data_root)); } // Ensure there has been no other observed aggregate for the given `aggregator_index`. @@ -376,13 +393,14 @@ impl VerifiedSyncContribution { // // It's important to double check that the contribution is not already known, otherwise two // contribution processed at the same time could be published. - if let ObserveOutcome::AlreadyKnown = chain + if let ObserveOutcome::Subset = chain .observed_sync_contributions .write() - .observe_item(contribution, Some(contribution_root)) + .observe_item(contribution, Some(contribution_data_root)) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::SyncContributionAlreadyKnown(contribution_root)); + metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_SUBSETS); + return Err(Error::SyncContributionSupersetKnown(contribution_data_root)); } // Observe the aggregator so we don't process another aggregate from them. diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 1040521e5..5cea51090 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -699,8 +699,8 @@ async fn aggregated_gossip_verification() { |tester, err| { assert!(matches!( err, - AttnError::AttestationAlreadyKnown(hash) - if hash == tester.valid_aggregate.message.aggregate.tree_hash_root() + AttnError::AttestationSupersetKnown(hash) + if hash == tester.valid_aggregate.message.aggregate.data.tree_hash_root() )) }, ) diff --git a/beacon_node/beacon_chain/tests/sync_committee_verification.rs b/beacon_node/beacon_chain/tests/sync_committee_verification.rs index 4204a5121..0e4745ff6 100644 --- a/beacon_node/beacon_chain/tests/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/tests/sync_committee_verification.rs @@ -1,6 +1,6 @@ #![cfg(not(debug_assertions))] -use beacon_chain::sync_committee_verification::Error as SyncCommitteeError; +use beacon_chain::sync_committee_verification::{Error as SyncCommitteeError, SyncCommitteeData}; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType, RelativeSyncCommittee}; use int_to_bytes::int_to_bytes32; use lazy_static::lazy_static; @@ -444,11 +444,17 @@ async fn aggregated_gossip_verification() { * subcommittee index contribution.subcommittee_index. */ + let contribution = &valid_aggregate.message.contribution; + let sync_committee_data = SyncCommitteeData { + slot: contribution.slot, + root: contribution.beacon_block_root, + subcommittee_index: contribution.subcommittee_index, + }; assert_invalid!( "aggregate that has already been seen", valid_aggregate.clone(), - SyncCommitteeError::SyncContributionAlreadyKnown(hash) - if hash == valid_aggregate.message.contribution.tree_hash_root() + SyncCommitteeError::SyncContributionSupersetKnown(hash) + if hash == sync_committee_data.tree_hash_root() ); /* diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index 3ed7ba65d..a96cfb6ca 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -23,7 +23,7 @@ bytes = "1.1.0" task_executor = { path = "../../common/task_executor" } hex = "0.4.2" ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" +ssz_types = "0.5.3" eth2 = { path = "../../common/eth2" } state_processing = { path = "../../consensus/state_processing" } superstruct = "0.6.0" diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 55e00bab3..025b54f32 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2843,7 +2843,7 @@ pub fn serve( // It's reasonably likely that two different validators produce // identical aggregates, especially if they're using the same beacon // node. - Err(AttnError::AttestationAlreadyKnown(_)) => continue, + Err(AttnError::AttestationSupersetKnown(_)) => continue, // If we've already seen this aggregator produce an aggregate, just // skip this one. // diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index c728fbeb1..07dfb5c98 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -304,7 +304,7 @@ pub fn process_signed_contribution_and_proofs( } // If we already know the contribution, don't broadcast it or attempt to // further verify it. Return success. - Err(SyncVerificationError::SyncContributionAlreadyKnown(_)) => continue, + Err(SyncVerificationError::SyncContributionSupersetKnown(_)) => continue, // If we've already seen this aggregator produce an aggregate, just // skip this one. // diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index ca15b5ef2..6d056d835 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" discv5 = { version = "0.3.0", features = ["libp2p"]} unsigned-varint = { version = "0.6.0", features = ["codec"] } types = { path = "../../consensus/types" } -ssz_types = "0.5.0" +ssz_types = "0.5.3" serde = { version = "1.0.116", features = ["derive"] } serde_derive = "1.0.116" ethereum_ssz = "0.5.0" diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index c99172899..aa1827787 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -22,7 +22,7 @@ slot_clock = { path = "../../common/slot_clock" } slog = { version = "2.5.2", features = ["max_level_trace"] } hex = "0.4.2" ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" +ssz_types = "0.5.3" futures = "0.3.7" error-chain = "0.12.4" tokio = { version = "1.14.0", features = ["full"] } diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index e3cff0010..185634c30 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -1735,7 +1735,7 @@ impl Worker { "attn_agg_not_in_committee", ); } - AttnError::AttestationAlreadyKnown { .. } => { + AttnError::AttestationSupersetKnown { .. } => { /* * The aggregate attestation has already been observed on the network or in * a block. @@ -2244,7 +2244,7 @@ impl Worker { "sync_bad_aggregator", ); } - SyncCommitteeError::SyncContributionAlreadyKnown(_) + SyncCommitteeError::SyncContributionSupersetKnown(_) | SyncCommitteeError::AggregatorAlreadyKnown(_) => { /* * The sync committee message already been observed on the network or in diff --git a/consensus/cached_tree_hash/Cargo.toml b/consensus/cached_tree_hash/Cargo.toml index c2856003b..0f43c8890 100644 --- a/consensus/cached_tree_hash/Cargo.toml +++ b/consensus/cached_tree_hash/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] ethereum-types = "0.14.1" -ssz_types = "0.5.0" +ssz_types = "0.5.3" ethereum_hashing = "1.0.0-beta.2" ethereum_ssz_derive = "0.5.0" ethereum_ssz = "0.5.0" diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index c16742782..f19cd1d29 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -15,7 +15,7 @@ integer-sqrt = "0.1.5" itertools = "0.10.0" ethereum_ssz = "0.5.0" ethereum_ssz_derive = "0.5.0" -ssz_types = "0.5.0" +ssz_types = "0.5.3" merkle_proof = { path = "../merkle_proof" } safe_arith = { path = "../safe_arith" } tree_hash = "0.5.0" diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 91ad3089f..583b940d5 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -27,7 +27,7 @@ serde_derive = "1.0.116" slog = "2.5.2" ethereum_ssz = { version = "0.5.0", features = ["arbitrary"] } ethereum_ssz_derive = "0.5.0" -ssz_types = { version = "0.5.0", features = ["arbitrary"] } +ssz_types = { version = "0.5.3", features = ["arbitrary"] } swap_or_not_shuffle = { path = "../swap_or_not_shuffle", features = ["arbitrary"] } test_random_derive = { path = "../../common/test_random_derive" } tree_hash = { version = "0.5.0", features = ["arbitrary"] } From 9072acbfa611367e9e88ce1c5ea1bd6b4ca9ea8d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 27 Jun 2023 01:06:50 +0000 Subject: [PATCH 17/25] Tidy formatting of `Reqwest` errors (#4336) ## Issue Addressed NA ## Proposed Changes Implements the `PrettyReqwestError` to wrap a `reqwest::Error` and give nicer `Debug` formatting. It also wraps the `Url` component in a `SensitiveUrl` to avoid leaking sensitive info in logs. ### Before ``` Reqwest(reqwest::Error { kind: Request, url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("localhost")), port: Some(9999), path: "/eth/v1/node/version", query: None, fragment: None }, source: hyper::Error(Connect, ConnectError("tcp connect error", Os { code: 61, kind: ConnectionRefused, message: "Connection refused" })) }) ``` ### After ``` HttpClient(url: http://localhost:9999/, kind: request, detail: error trying to connect: tcp connect error: Connection refused (os error 61)) ``` ## Additional Info I've also renamed the `Reqwest` error enum variants to `HttpClient`, to give people a better chance at knowing what's going on. Reqwest is pretty odd and looks like a typo. I've implemented it in the `eth2` and `execution_layer` crates. This should affect most logs in the VC and EE-related ones in the BN. I think the last crate that could benefit from the is the `beacon_node/eth1` crate. I haven't updated it in this PR since its error type is not so amenable to it (everything goes into a `String`). I don't have a whole lot of time to jig around with that at the moment and I feel that this PR as it stands is a significant enough improvement to merge on its own. Leaving it as-is is fine for the time being and we can always come back for it later (or implement in-protocol deposits!). --- Cargo.lock | 12 ++++ Cargo.toml | 1 + beacon_node/builder_client/src/lib.rs | 6 +- beacon_node/execution_layer/Cargo.toml | 1 + beacon_node/execution_layer/src/engine_api.rs | 5 +- common/eth2/Cargo.toml | 5 ++ common/eth2/src/lib.rs | 13 ++-- common/eth2/src/lighthouse.rs | 4 +- common/eth2/src/lighthouse_vc/http_client.rs | 12 ++-- common/pretty_reqwest_error/Cargo.toml | 10 +++ common/pretty_reqwest_error/src/lib.rs | 62 +++++++++++++++++++ common/sensitive_url/src/lib.rs | 2 +- 12 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 common/pretty_reqwest_error/Cargo.toml create mode 100644 common/pretty_reqwest_error/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 700aecb83..02922b2d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2292,6 +2292,8 @@ dependencies = [ "libsecp256k1", "lighthouse_network", "mediatype", + "mime", + "pretty_reqwest_error", "procinfo", "proto_array", "psutil", @@ -2302,6 +2304,7 @@ dependencies = [ "serde_json", "slashing_protection", "store", + "tokio", "types", ] @@ -2742,6 +2745,7 @@ dependencies = [ "lru 0.7.8", "mev-rs", "parking_lot 0.12.1", + "pretty_reqwest_error", "rand 0.8.5", "reqwest", "sensitive_url", @@ -6204,6 +6208,14 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "pretty_reqwest_error" +version = "0.1.0" +dependencies = [ + "reqwest", + "sensitive_url", +] + [[package]] name = "prettyplease" version = "0.1.25" diff --git a/Cargo.toml b/Cargo.toml index bbe77d209..5c39e01ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "common/lru_cache", "common/malloc_utils", "common/oneshot_broadcast", + "common/pretty_reqwest_error", "common/sensitive_url", "common/slot_clock", "common/system_health", diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 255c2fdd1..c78f686d0 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -72,7 +72,7 @@ impl BuilderHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Into::into) } /// Perform a HTTP GET request, returning the `Response` for further processing. @@ -85,7 +85,7 @@ impl BuilderHttpClient { if let Some(timeout) = timeout { builder = builder.timeout(timeout); } - let response = builder.send().await.map_err(Error::Reqwest)?; + let response = builder.send().await.map_err(Error::from)?; ok_or_error(response).await } @@ -114,7 +114,7 @@ impl BuilderHttpClient { if let Some(timeout) = timeout { builder = builder.timeout(timeout); } - let response = builder.json(body).send().await.map_err(Error::Reqwest)?; + let response = builder.json(body).send().await.map_err(Error::from)?; ok_or_error(response).await } diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index a96cfb6ca..2cb28346f 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -50,3 +50,4 @@ keccak-hash = "0.10.0" hash256-std-hasher = "0.15.2" triehash = "0.8.4" hash-db = "0.15.2" +pretty_reqwest_error = { path = "../../common/pretty_reqwest_error" } \ No newline at end of file diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 4d2eb565e..826294d5f 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -10,6 +10,7 @@ pub use ethers_core::types::Transaction; use ethers_core::utils::rlp::{self, Decodable, Rlp}; use http::deposit_methods::RpcError; pub use json_structures::{JsonWithdrawal, TransitionConfigurationV1}; +use pretty_reqwest_error::PrettyReqwestError; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -32,7 +33,7 @@ pub type PayloadId = [u8; 8]; #[derive(Debug)] pub enum Error { - Reqwest(reqwest::Error), + HttpClient(PrettyReqwestError), Auth(auth::Error), BadResponse(String), RequestFailed(String), @@ -67,7 +68,7 @@ impl From for Error { ) { Error::Auth(auth::Error::InvalidToken) } else { - Error::Reqwest(e) + Error::HttpClient(e.into()) } } } diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 4eabd3ff8..d8e1a375f 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -27,6 +27,11 @@ futures = "0.3.8" store = { path = "../../beacon_node/store", optional = true } slashing_protection = { path = "../../validator_client/slashing_protection", optional = true } mediatype = "0.19.13" +mime = "0.3.16" +pretty_reqwest_error = { path = "../../common/pretty_reqwest_error" } + +[dev-dependencies] +tokio = { version = "1.14.0", features = ["full"] } [target.'cfg(target_os = "linux")'.dependencies] psutil = { version = "3.2.2", optional = true } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e871efbc2..217d35696 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -19,6 +19,7 @@ use self::types::{Error as ResponseError, *}; use futures::Stream; use futures_util::StreamExt; use lighthouse_network::PeerId; +use pretty_reqwest_error::PrettyReqwestError; pub use reqwest; use reqwest::{IntoUrl, RequestBuilder, Response}; pub use reqwest::{StatusCode, Url}; @@ -39,7 +40,7 @@ pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; #[derive(Debug)] pub enum Error { /// The `reqwest` client raised an error. - Reqwest(reqwest::Error), + HttpClient(PrettyReqwestError), /// The server returned an error message where the body was able to be parsed. ServerMessage(ErrorMessage), /// The server returned an error message with an array of errors. @@ -70,7 +71,7 @@ pub enum Error { impl From for Error { fn from(error: reqwest::Error) -> Self { - Error::Reqwest(error) + Error::HttpClient(error.into()) } } @@ -78,7 +79,7 @@ impl Error { /// If the error has a HTTP status code, return it. pub fn status(&self) -> Option { match self { - Error::Reqwest(error) => error.status(), + Error::HttpClient(error) => error.inner().status(), Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::StatusCode(status) => Some(*status), @@ -278,7 +279,7 @@ impl BeaconNodeHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Into::into) } /// Perform a HTTP POST request with a custom timeout. @@ -303,7 +304,7 @@ impl BeaconNodeHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Error::from) } /// Generic POST function supporting arbitrary responses and timeouts. @@ -1645,7 +1646,7 @@ impl BeaconNodeHttpClient { .bytes_stream() .map(|next| match next { Ok(bytes) => EventKind::from_sse_bytes(bytes.as_ref()), - Err(e) => Err(Error::Reqwest(e)), + Err(e) => Err(Error::HttpClient(e.into())), })) } diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index bb933dbe1..1b4bcc0e3 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -364,12 +364,12 @@ pub struct DatabaseInfo { impl BeaconNodeHttpClient { /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_bytes_opt(&self, url: U) -> Result>, Error> { - let response = self.client.get(url).send().await.map_err(Error::Reqwest)?; + let response = self.client.get(url).send().await.map_err(Error::from)?; match ok_or_error(response).await { Ok(resp) => Ok(Some( resp.bytes() .await - .map_err(Error::Reqwest)? + .map_err(Error::from)? .into_iter() .collect::>(), )), diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index 720d8c779..cd7873c9b 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -170,7 +170,7 @@ impl ValidatorClientHttpClient { .map_err(|_| Error::InvalidSignatureHeader)? .to_string(); - let body = response.bytes().await.map_err(Error::Reqwest)?; + let body = response.bytes().await.map_err(Error::from)?; let message = Message::parse_slice(digest(&SHA256, &body).as_ref()).expect("sha256 is 32 bytes"); @@ -222,7 +222,7 @@ impl ValidatorClientHttpClient { .headers(self.headers()?) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } @@ -236,7 +236,7 @@ impl ValidatorClientHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Error::from) } /// Perform a HTTP GET request, returning `None` on a 404 error. @@ -266,7 +266,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } @@ -297,7 +297,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; let response = ok_or_error(response).await?; self.signed_body(response).await?; Ok(()) @@ -316,7 +316,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } diff --git a/common/pretty_reqwest_error/Cargo.toml b/common/pretty_reqwest_error/Cargo.toml new file mode 100644 index 000000000..ca9f4812b --- /dev/null +++ b/common/pretty_reqwest_error/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pretty_reqwest_error" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +reqwest = { version = "0.11.0", features = ["json","stream"] } +sensitive_url = { path = "../sensitive_url" } diff --git a/common/pretty_reqwest_error/src/lib.rs b/common/pretty_reqwest_error/src/lib.rs new file mode 100644 index 000000000..4c605f38a --- /dev/null +++ b/common/pretty_reqwest_error/src/lib.rs @@ -0,0 +1,62 @@ +use sensitive_url::SensitiveUrl; +use std::error::Error as StdError; +use std::fmt; + +pub struct PrettyReqwestError(reqwest::Error); + +impl PrettyReqwestError { + pub fn inner(&self) -> &reqwest::Error { + &self.0 + } +} + +impl fmt::Debug for PrettyReqwestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(url) = self.0.url() { + if let Ok(url) = SensitiveUrl::new(url.clone()) { + write!(f, "url: {}", url)?; + } else { + write!(f, "url: unable_to_parse")?; + }; + } + + let kind = if self.0.is_builder() { + "builder" + } else if self.0.is_redirect() { + "redirect" + } else if self.0.is_status() { + "status" + } else if self.0.is_timeout() { + "timeout" + } else if self.0.is_request() { + "request" + } else if self.0.is_connect() { + "connect" + } else if self.0.is_body() { + "body" + } else if self.0.is_decode() { + "decode" + } else { + "unknown" + }; + write!(f, ", kind: {}", kind)?; + + if let Some(status) = self.0.status() { + write!(f, ", status_code: {}", status)?; + } + + if let Some(ref source) = self.0.source() { + write!(f, ", detail: {}", source)?; + } else { + write!(f, ", source: unknown")?; + } + + Ok(()) + } +} + +impl From for PrettyReqwestError { + fn from(inner: reqwest::Error) -> Self { + Self(inner) + } +} diff --git a/common/sensitive_url/src/lib.rs b/common/sensitive_url/src/lib.rs index b6705eb60..b6068a2dc 100644 --- a/common/sensitive_url/src/lib.rs +++ b/common/sensitive_url/src/lib.rs @@ -75,7 +75,7 @@ impl SensitiveUrl { SensitiveUrl::new(surl) } - fn new(full: Url) -> Result { + pub fn new(full: Url) -> Result { let mut redacted = full.clone(); redacted .path_segments_mut() From ead4e60a76a47ca9221d3fc383a15131a77f8596 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 27 Jun 2023 01:06:51 +0000 Subject: [PATCH 18/25] Schedule Capella for Gnosis chain (#4433) ## Issue Addressed Closes #4422 Implements https://github.com/gnosischain/configs/pull/12 ## Proposed Changes Schedule the Capella fork for Gnosis chain at epoch 648704, August 1st 2023 11:34:20 UTC. --- .../built_in_network_configs/gnosis/config.yaml | 2 +- consensus/types/src/chain_spec.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml index 95ca9d010..0fdc159ec 100644 --- a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml @@ -38,7 +38,7 @@ BELLATRIX_FORK_VERSION: 0x02000064 BELLATRIX_FORK_EPOCH: 385536 # Capella CAPELLA_FORK_VERSION: 0x03000064 -CAPELLA_FORK_EPOCH: 18446744073709551615 +CAPELLA_FORK_EPOCH: 648704 # Sharding SHARDING_FORK_VERSION: 0x03000064 SHARDING_FORK_EPOCH: 18446744073709551615 diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 5253dcc4b..595718223 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -828,7 +828,7 @@ impl ChainSpec { * Capella hard fork params */ capella_fork_version: [0x03, 0x00, 0x00, 0x64], - capella_fork_epoch: None, + capella_fork_epoch: Some(Epoch::new(648704)), max_validators_per_withdrawals_sweep: 8192, /* From d062f61125c7c14d14f71f31f391a5e0069ecdcb Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 27 Jun 2023 15:26:14 +1000 Subject: [PATCH 19/25] Fix failing tests after merge --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/beacon_chain/src/lib.rs | 2 +- .../beacon_chain/tests/block_verification.rs | 33 +++++++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a6e1d0ee6..c99f312a0 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2921,7 +2921,7 @@ impl BeaconChain { } } - async fn import_available_block( + pub async fn import_available_block( self: &Arc, block: Box>, ) -> Result> { diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 8c004e792..94fb8c855 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -70,7 +70,7 @@ pub use attestation_verification::Error as AttestationError; pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError}; pub use block_verification::{ get_block_root, AvailabilityPendingExecutedBlock, BlockError, ExecutedBlock, - ExecutionPayloadError, GossipVerifiedBlock, IntoExecutionPendingBlock, + ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, PayloadVerificationOutcome, PayloadVerificationStatus, }; pub use canonical_head::{CachedHead, CanonicalHead, CanonicalHeadRwLock}; diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 2bdcc83d8..f9847845b 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -4,6 +4,8 @@ use beacon_chain::blob_verification::BlockWrapper; use beacon_chain::{ blob_verification::AsBlock, test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, + AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, ExecutedBlock, + ExecutionPendingBlock, }; use beacon_chain::{ BeaconSnapshot, BlockError, ChainSegmentResult, IntoExecutionPendingBlock, NotifyExecutionLayer, @@ -1409,7 +1411,8 @@ async fn import_duplicate_block_unrealized_justification() { // Produce a block to justify epoch 2. let state = harness.get_current_state(); let slot = harness.get_current_slot(); - let (block, _) = harness.make_block(state.clone(), slot).await; + let (block_contents, _) = harness.make_block(state.clone(), slot).await; + let (block, _) = block_contents; let block = Arc::new(block); let block_root = block.canonical_root(); @@ -1425,9 +1428,7 @@ async fn import_duplicate_block_unrealized_justification() { .unwrap(); // Import the first block, simulating a block processed via a finalized chain segment. - chain - .clone() - .import_execution_pending_block(verified_block1) + import_execution_pending_block(chain.clone(), verified_block1) .await .unwrap(); @@ -1446,9 +1447,7 @@ async fn import_duplicate_block_unrealized_justification() { drop(fc); // Import the second verified block, simulating a block processed via RPC. - chain - .clone() - .import_execution_pending_block(verified_block2) + import_execution_pending_block(chain.clone(), verified_block2) .await .unwrap(); @@ -1467,3 +1466,23 @@ async fn import_duplicate_block_unrealized_justification() { Some(unrealized_justification) ); } + +async fn import_execution_pending_block( + chain: Arc>, + execution_pending_block: ExecutionPendingBlock, +) -> Result { + match chain + .clone() + .into_executed_block(execution_pending_block) + .await + .unwrap() + { + ExecutedBlock::Available(block) => chain + .import_available_block(Box::from(block)) + .await + .map_err(|e| format!("{e:?}")), + ExecutedBlock::AvailabilityPending(_) => { + Err("AvailabilityPending not expected in this test. Block not imported.".to_string()) + } + } +} From 56caccbac0f9aa3a7e8c0bc8048abcedd8a8df59 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 27 Jun 2023 17:48:50 +1000 Subject: [PATCH 20/25] Added a few fixes from merge --- Cargo.lock | 3 +-- .../src/rpc/codec/ssz_snappy.rs | 4 ++-- .../lighthouse_network/src/rpc/handler.rs | 8 +------- .../lighthouse_network/src/rpc/methods.rs | 5 ++++- .../lighthouse_network/src/rpc/protocol.rs | 16 ++++++++++++++++ 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6efffd62e..cff90ec6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7938,8 +7938,7 @@ dependencies = [ [[package]] name = "ssz_types" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43767964a80b2fdeda7a79a57a2b6cbca966688d5b81da8fe91140a94f552a1" +source = "git+https://github.com/sigp/ssz_types?rev=63a80d04286c8561d5c211230a21bf1299d66059#63a80d04286c8561d5c211230a21bf1299d66059" dependencies = [ "arbitrary", "derivative", diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index 1c912ba79..e09eb3a9c 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -309,7 +309,7 @@ impl Decoder for SSZSnappyOutboundCodec { let _read_bytes = src.split_to(n as usize); // Safe to `take` from `self.fork_name` as we have all the bytes we need to // decode an ssz object at this point. - let fork_name = &mut self.fork_name.take(); + let fork_name = self.fork_name.take(); handle_rpc_response(self.protocol.versioned_protocol, &decoded_buffer, fork_name) } Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), @@ -530,7 +530,7 @@ fn handle_rpc_request( fn handle_rpc_response( versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], - fork_name: &mut Option, + mut fork_name: Option, ) -> Result>, RPCError> { match versioned_protocol { SupportedProtocol::StatusV1 => Ok(Some(RPCResponse::Status( diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index ace47ff91..e76b7dedc 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -7,7 +7,6 @@ use super::protocol::{max_rpc_size, InboundRequest, Protocol, RPCError, RPCProto use super::{RPCReceived, RPCSend, ReqId}; use crate::rpc::outbound::{OutboundFramed, OutboundRequest}; use crate::rpc::protocol::InboundFramed; -use crate::rpc::ResponseTermination; use fnv::FnvHashMap; use futures::prelude::*; use futures::{Sink, SinkExt}; @@ -934,13 +933,8 @@ where // continue sending responses beyond what we would expect. Here // we simply terminate the stream and report a stream // termination to the application - let termination = match protocol { - Protocol::BlocksByRange => Some(ResponseTermination::BlocksByRange), - Protocol::BlocksByRoot => Some(ResponseTermination::BlocksByRoot), - _ => None, // all other protocols are do not have multiple responses and we do not inform the user, we simply drop the stream. - }; - if let Some(termination) = termination { + if let Some(termination) = protocol.terminator() { return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( RPCReceived::EndOfStream(request_id, termination), ))); diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 2ca41eb0e..15d05cd16 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -340,7 +340,10 @@ impl OldBlocksByRangeRequest { } /// Request a number of beacon block bodies from a peer. -#[superstruct(variants(V1, V2), variant_attributes(derive(Clone, Debug, PartialEq)))] +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Encode, Decode, Clone, Debug, PartialEq)) +)] #[derive(Clone, Debug, PartialEq)] pub struct BlocksByRootRequest { /// The list of beacon block bodies being requested. diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 2335f3d92..804fa7274 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -205,6 +205,22 @@ pub enum Protocol { LightClientBootstrap, } +impl Protocol { + pub(crate) fn terminator(self) -> Option { + match self { + Protocol::Status => None, + Protocol::Goodbye => None, + Protocol::BlocksByRange => Some(ResponseTermination::BlocksByRange), + Protocol::BlocksByRoot => Some(ResponseTermination::BlocksByRoot), + Protocol::BlobsByRange => Some(ResponseTermination::BlobsByRange), + Protocol::BlobsByRoot => Some(ResponseTermination::BlobsByRoot), + Protocol::Ping => None, + Protocol::MetaData => None, + Protocol::LightClientBootstrap => None, + } + } +} + /// RPC Encondings supported. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Encoding { From dfbe4b1add910a237fb4c143562489f10f9155b6 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 28 Jun 2023 16:11:38 +1000 Subject: [PATCH 21/25] Add missing Cargo.lock changes (`ssz_types` patch) --- Cargo.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6efffd62e..cff90ec6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7938,8 +7938,7 @@ dependencies = [ [[package]] name = "ssz_types" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43767964a80b2fdeda7a79a57a2b6cbca966688d5b81da8fe91140a94f552a1" +source = "git+https://github.com/sigp/ssz_types?rev=63a80d04286c8561d5c211230a21bf1299d66059#63a80d04286c8561d5c211230a21bf1299d66059" dependencies = [ "arbitrary", "derivative", From d1146ec8b580f8c8c8a6fe0d0b60b63196f87637 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 28 Jun 2023 16:14:39 +1000 Subject: [PATCH 22/25] Sync finalized sync to 2 epochs + 1 slot past our peer's finalized slot in order to finalize the chain locally --- beacon_node/network/src/sync/range_sync/range.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 58f137dfe..b0b99e3ca 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -142,13 +142,20 @@ where debug!(self.log, "Finalization sync peer joined"; "peer_id" => %peer_id); self.awaiting_head_peers.remove(&peer_id); + // Because of our change in finalized sync batch size from 2 to 1 and our transition + // to using exact epoch boundaries for batches (rather than one slot past the epoch + // boundary), we need to sync finalized sync to 2 epochs + 1 slot past our peer's + // finalized slot in order to finalize the chain locally. + let target_head_slot = + remote_finalized_slot + (2 * T::EthSpec::slots_per_epoch()) + 1; + // Note: We keep current head chains. These can continue syncing whilst we complete // this new finalized chain. self.chains.add_peer_or_create_chain( local_info.finalized_epoch, remote_info.finalized_root, - remote_finalized_slot, + target_head_slot, peer_id, RangeSyncType::Finalized, network, From 68140fa0363ba62fdb9b4b82a3a09872fe93b1a5 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 28 Jun 2023 16:46:20 +1000 Subject: [PATCH 23/25] Update max block request limit to `MAX_REQUEST_BLOCKS_DENEB` to ensure this doesn't cause incompatibilities with other clients. --- .../src/beacon_processor/worker/rpc_methods.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs b/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs index faf632548..0cffa634c 100644 --- a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs @@ -5,7 +5,7 @@ use crate::sync::SyncMessage; use beacon_chain::{BeaconChainError, BeaconChainTypes, HistoricalBlockError, WhenSlotSkipped}; use itertools::process_results; use lighthouse_network::rpc::methods::{ - BlobsByRangeRequest, BlobsByRootRequest, MAX_REQUEST_BLOB_SIDECARS, + BlobsByRangeRequest, BlobsByRootRequest, MAX_REQUEST_BLOB_SIDECARS, MAX_REQUEST_BLOCKS_DENEB, }; use lighthouse_network::rpc::StatusMessage; use lighthouse_network::rpc::*; @@ -384,8 +384,15 @@ impl Worker { ); // Should not send more than max request blocks - if *req.count() > MAX_REQUEST_BLOCKS { - *req.count_mut() = MAX_REQUEST_BLOCKS; + // TODO: We should switch the limit to `MAX_REQUEST_BLOCKS` at the fork, + // or maybe consider switching the max value given the fork context. + if *req.count() > MAX_REQUEST_BLOCKS_DENEB { + return self.send_error_response( + peer_id, + RPCResponseErrorCode::InvalidRequest, + "Request exceeded `MAX_REQUEST_BLOCKS_DENEB`".into(), + request_id, + ); } let forwards_block_root_iter = match self From 03a17a84dac0df9d0d76e0689c4f03b3643414fc Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 28 Jun 2023 16:56:52 +1000 Subject: [PATCH 24/25] Update `handle_rpc_response` blobs match arms to be consistent with block v2 protocols. --- .../src/rpc/codec/ssz_snappy.rs | 74 +++++++++---------- .../beacon_processor/worker/rpc_methods.rs | 2 +- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index e09eb3a9c..58de54c00 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -530,7 +530,7 @@ fn handle_rpc_request( fn handle_rpc_response( versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], - mut fork_name: Option, + fork_name: Option, ) -> Result>, RPCError> { match versioned_protocol { SupportedProtocol::StatusV1 => Ok(Some(RPCResponse::Status( @@ -546,46 +546,38 @@ fn handle_rpc_response( SupportedProtocol::BlocksByRootV1 => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), )))), - SupportedProtocol::BlobsByRangeV1 => { - let fork_name = fork_name.take().ok_or_else(|| { - RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - format!( - "No context bytes provided for {:?} response", - versioned_protocol - ), - ) - })?; - match fork_name { - ForkName::Deneb => Ok(Some(RPCResponse::BlobsByRange(Arc::new( - BlobSidecar::from_ssz_bytes(decoded_buffer)?, - )))), - _ => Err(RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - "Invalid fork name for blobs by range".to_string(), - )), - } - } - SupportedProtocol::BlobsByRootV1 => { - let fork_name = fork_name.take().ok_or_else(|| { - RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - format!( - "No context bytes provided for {:?} response", - versioned_protocol - ), - ) - })?; - match fork_name { - ForkName::Deneb => Ok(Some(RPCResponse::SidecarByRoot(Arc::new( - BlobSidecar::from_ssz_bytes(decoded_buffer)?, - )))), - _ => Err(RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - "Invalid fork name for block and blobs by root".to_string(), - )), - } - } + SupportedProtocol::BlobsByRangeV1 => match fork_name { + Some(ForkName::Deneb) => Ok(Some(RPCResponse::BlobsByRange(Arc::new( + BlobSidecar::from_ssz_bytes(decoded_buffer)?, + )))), + Some(_) => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + "Invalid fork name for blobs by range".to_string(), + )), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, + SupportedProtocol::BlobsByRootV1 => match fork_name { + Some(ForkName::Deneb) => Ok(Some(RPCResponse::SidecarByRoot(Arc::new( + BlobSidecar::from_ssz_bytes(decoded_buffer)?, + )))), + Some(_) => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + "Invalid fork name for blobs by root".to_string(), + )), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, SupportedProtocol::PingV1 => Ok(Some(RPCResponse::Pong(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), diff --git a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs b/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs index 0cffa634c..25078147d 100644 --- a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs @@ -375,7 +375,7 @@ impl Worker { send_on_drop: SendOnDrop, peer_id: PeerId, request_id: PeerRequestId, - mut req: BlocksByRangeRequest, + req: BlocksByRangeRequest, ) { debug!(self.log, "Received BlocksByRange Request"; "peer_id" => %peer_id, From 5b85aeca5fa38cf8978665c597887a8c46d195f0 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 28 Jun 2023 22:42:30 +1000 Subject: [PATCH 25/25] Add BlobSidecar encode & decode test and fix `RpcLimit` for `BlobsByRoot` --- .../src/rpc/codec/ssz_snappy.rs | 22 +++++++++++++++++++ .../lighthouse_network/src/rpc/protocol.rs | 5 +---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index 58de54c00..fd8d1e65a 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -723,6 +723,10 @@ mod tests { SignedBeaconBlock::from_block(full_block, Signature::empty()) } + fn default_blob_sidecar() -> Arc> { + Arc::new(BlobSidecar::empty()) + } + /// Merge block with length < max_rpc_size. fn merge_block_small(fork_context: &ForkContext) -> SignedBeaconBlock { let mut block: BeaconBlockMerge<_, FullPayload> = @@ -1023,6 +1027,24 @@ mod tests { ), Ok(Some(RPCResponse::MetaData(metadata()))), ); + + assert_eq!( + encode_then_decode_response( + SupportedProtocol::BlobsByRangeV1, + RPCCodedResponse::Success(RPCResponse::BlobsByRange(default_blob_sidecar())), + ForkName::Deneb, + ), + Ok(Some(RPCResponse::BlobsByRange(default_blob_sidecar()))), + ); + + assert_eq!( + encode_then_decode_response( + SupportedProtocol::BlobsByRootV1, + RPCCodedResponse::Success(RPCResponse::SidecarByRoot(default_blob_sidecar())), + ForkName::Deneb, + ), + Ok(Some(RPCResponse::SidecarByRoot(default_blob_sidecar()))), + ); } // Test RPCResponse encoding/decoding for V1 messages diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 804fa7274..b6de8b2c2 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -421,10 +421,7 @@ impl ProtocolId { Protocol::BlocksByRange => rpc_block_limits_by_fork(fork_context.current_fork()), Protocol::BlocksByRoot => rpc_block_limits_by_fork(fork_context.current_fork()), Protocol::BlobsByRange => RpcLimits::new(*BLOB_SIDECAR_MIN, *BLOB_SIDECAR_MAX), - Protocol::BlobsByRoot => { - // TODO: wrong too - RpcLimits::new(*SIGNED_BLOCK_AND_BLOBS_MIN, *SIGNED_BLOCK_AND_BLOBS_MAX) - } + Protocol::BlobsByRoot => RpcLimits::new(*BLOB_SIDECAR_MIN, *BLOB_SIDECAR_MAX), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(),