diff --git a/Cargo.lock b/Cargo.lock index a9fdfe70bd..c0f337a6a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1758,7 +1758,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -1768,7 +1779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1973,6 +1984,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compare_fields" version = "0.1.1" @@ -2009,7 +2030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" dependencies = [ "futures-core", - "prost", + "prost 0.13.5", "prost-types", "tonic 0.12.3", "tracing-core", @@ -2028,7 +2049,7 @@ dependencies = [ "hdrhistogram", "humantime", "hyper-util", - "prost", + "prost 0.13.5", "prost-types", "serde", "serde_json", @@ -2048,7 +2069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -2146,15 +2167,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2164,6 +2176,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -2319,7 +2340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", @@ -2910,9 +2931,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] @@ -3051,18 +3072,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -3118,7 +3127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3265,7 +3274,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa93f58bb1eb3d1e556e4f408ef1dac130bad01ac37db4e7ade45de40d1c86a" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "ring", "sha2", ] @@ -3724,7 +3733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", ] @@ -3823,11 +3832,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -4081,24 +4104,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", "futures-channel", "futures-io", "futures-util", + "hickory-proto", "idna", "ipnet", - "once_cell", - "rand 0.9.2", - "ring", - "socket2 0.5.10", + "jni", + "rand 0.10.1", "thiserror 2.0.17", "tinyvec", "tokio", @@ -4107,21 +4128,46 @@ dependencies = [ ] [[package]] -name = "hickory-resolver" -version = "0.25.2" +name = "hickory-proto" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", + "hickory-net", "hickory-proto", "ipconfig", + "ipnet", + "jni", "moka", + "ndk-context", "once_cell", "parking_lot", - "rand 0.9.2", + "rand 0.10.1", "resolv-conf", "smallvec", + "system-configuration", "thiserror 2.0.17", "tokio", "tracing", @@ -4349,7 +4395,7 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -4388,7 +4434,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -4499,6 +4545,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -4590,6 +4642,26 @@ dependencies = [ "xmltree", ] +[[package]] +name = "igd-next" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http 1.4.0", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "log", + "rand 0.10.1", + "tokio", + "url", + "xmltree", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -4707,6 +4779,9 @@ name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -4766,6 +4841,55 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.17", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4778,9 +4902,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -4822,7 +4946,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -4918,6 +5042,12 @@ dependencies = [ "yaml_serde", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "leveldb" version = "0.8.6" @@ -4943,9 +5073,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -4981,7 +5111,7 @@ dependencies = [ [[package]] name = "libp2p" version = "0.57.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "bytes", "either", @@ -5012,7 +5142,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.7.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "libp2p-core", "libp2p-identity", @@ -5022,7 +5152,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.7.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "libp2p-core", "libp2p-identity", @@ -5032,7 +5162,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.44.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "either", "fnv", @@ -5044,7 +5174,7 @@ dependencies = [ "multistream-select", "parking_lot", "pin-project", - "quick-protobuf", + "prost 0.14.3", "rand 0.8.5", "rw-stream-sink", "thiserror 2.0.17", @@ -5056,7 +5186,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.45.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "hickory-resolver", @@ -5070,7 +5200,7 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.50.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "async-channel 2.5.0", "asynchronous-codec", @@ -5088,8 +5218,8 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "prometheus-client", - "quick-protobuf", - "quick-protobuf-codec", + "prost 0.14.3", + "prost-codec", "rand 0.8.5", "regex", "sha2", @@ -5100,7 +5230,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.48.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "asynchronous-codec", "either", @@ -5110,8 +5240,8 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "quick-protobuf", - "quick-protobuf-codec", + "prost 0.14.3", + "prost-codec", "smallvec", "thiserror 2.0.17", "tracing", @@ -5119,9 +5249,9 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" +checksum = "9525f3831544f7ae497bde79adf114ef127b0fbbb97edbbf692a80408636421c" dependencies = [ "asn1_der", "bs58 0.5.1", @@ -5129,7 +5259,7 @@ dependencies = [ "hkdf", "k256", "multihash", - "quick-protobuf", + "prost 0.14.3", "rand 0.8.5", "sha2", "thiserror 2.0.17", @@ -5140,7 +5270,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.49.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "hickory-proto", @@ -5150,7 +5280,7 @@ dependencies = [ "libp2p-swarm", "rand 0.8.5", "smallvec", - "socket2 0.6.3", + "socket2 0.6.4", "tokio", "tracing", ] @@ -5158,7 +5288,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.18.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "libp2p-core", @@ -5174,7 +5304,7 @@ dependencies = [ [[package]] name = "libp2p-mplex" version = "0.44.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "asynchronous-codec", "bytes", @@ -5192,7 +5322,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.47.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "asynchronous-codec", "bytes", @@ -5201,7 +5331,7 @@ dependencies = [ "libp2p-identity", "multiaddr", "multihash", - "quick-protobuf", + "prost 0.14.3", "rand 0.8.5", "snow", "static_assertions", @@ -5214,7 +5344,7 @@ dependencies = [ [[package]] name = "libp2p-quic" version = "0.14.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "futures-timer", @@ -5225,8 +5355,8 @@ dependencies = [ "quinn", "rand 0.8.5", "ring", - "rustls 0.23.35", - "socket2 0.6.3", + "rustls 0.23.40", + "socket2 0.6.4", "thiserror 2.0.17", "tokio", "tracing", @@ -5235,7 +5365,7 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.48.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "either", "fnv", @@ -5258,7 +5388,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.36.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "heck", "quote", @@ -5268,14 +5398,14 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.45.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "futures-timer", "if-watch", "libc", "libp2p-core", - "socket2 0.6.3", + "socket2 0.6.4", "tokio", "tracing", ] @@ -5283,7 +5413,7 @@ dependencies = [ [[package]] name = "libp2p-tls" version = "0.7.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "futures-rustls", @@ -5291,21 +5421,21 @@ dependencies = [ "libp2p-identity", "rcgen", "ring", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-webpki 0.103.13", "thiserror 2.0.17", - "x509-parser", - "yasna", + "x509-parser 0.18.1", + "yasna 0.6.0", ] [[package]] name = "libp2p-upnp" version = "0.7.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "futures-timer", - "igd-next", + "igd-next 0.17.1", "libp2p-core", "libp2p-swarm", "tokio", @@ -5315,7 +5445,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.48.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "either", "futures", @@ -5448,7 +5578,6 @@ dependencies = [ "if-addrs 0.14.0", "itertools 0.14.0", "libp2p", - "libp2p-gossipsub", "libp2p-mplex", "lighthouse_version", "logging", @@ -5831,9 +5960,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -5985,18 +6114,17 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.3" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" dependencies = [ - "core2", "unsigned-varint", ] [[package]] name = "multistream-select" version = "0.14.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "bytes", "futures", @@ -6006,6 +6134,12 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "netlink-packet-core" version = "0.8.1" @@ -6077,7 +6211,7 @@ dependencies = [ "futures", "genesis", "hex", - "igd-next", + "igd-next 0.16.2", "itertools 0.14.0", "k256", "kzg", @@ -6196,7 +6330,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6410,7 +6544,7 @@ dependencies = [ "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost", + "prost 0.13.5", "reqwest", "thiserror 2.0.17", "tokio", @@ -6426,7 +6560,7 @@ checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" dependencies = [ "opentelemetry", "opentelemetry_sdk", - "prost", + "prost 0.13.5", "tonic 0.13.1", ] @@ -6492,7 +6626,7 @@ dependencies = [ "sha1", "sha2", "thiserror 2.0.17", - "x509-parser", + "x509-parser 0.17.0", ] [[package]] @@ -6624,18 +6758,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -6754,7 +6888,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -6766,7 +6900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -6827,6 +6961,17 @@ dependencies = [ "termtree", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_reqwest_error" version = "0.1.0" @@ -6991,7 +7136,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive 0.14.3", +] + +[[package]] +name = "prost-codec" +version = "0.4.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" +dependencies = [ + "asynchronous-codec", + "bytes", + "prost 0.14.3", + "thiserror 2.0.17", + "unsigned-varint", ] [[package]] @@ -7007,19 +7174,33 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "prost-types" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ - "prost", + "prost 0.13.5", ] [[package]] name = "proto_array" version = "0.2.0" dependencies = [ + "criterion", "ethereum_ssz", "ethereum_ssz_derive", "fixed_bytes", @@ -7057,26 +7238,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-protobuf" -version = "0.8.1" -source = "git+https://github.com/sigp/quick-protobuf.git?rev=87c4ccb9bb2af494de375f5f6c62850badd26304#87c4ccb9bb2af494de375f5f6c62850badd26304" -dependencies = [ - "byteorder", -] - -[[package]] -name = "quick-protobuf-codec" -version = "0.4.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" -dependencies = [ - "asynchronous-codec", - "bytes", - "quick-protobuf", - "thiserror 2.0.17", - "unsigned-varint", -] - [[package]] name = "quinn" version = "0.11.9" @@ -7090,8 +7251,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.35", - "socket2 0.6.3", + "rustls 0.23.40", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -7110,7 +7271,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -7128,9 +7289,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7148,6 +7309,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "r2d2" version = "0.8.10" @@ -7199,6 +7366,17 @@ dependencies = [ "serde", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -7238,6 +7416,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.3.0" @@ -7304,7 +7488,7 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "yasna", + "yasna 0.5.2", ] [[package]] @@ -7407,7 +7591,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "serde", "serde_json", @@ -7652,7 +7836,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7671,9 +7855,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -7758,7 +7942,7 @@ dependencies = [ [[package]] name = "rw-stream-sink" version = "0.5.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "pin-project", @@ -8087,7 +8271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -8098,7 +8282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -8180,6 +8364,22 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version 0.4.1", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -8354,9 +8554,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.60.2", @@ -8704,7 +8904,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -8917,9 +9117,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -8927,7 +9127,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2 0.6.4", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -8935,9 +9135,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -8961,7 +9161,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.40", "tokio", ] @@ -9042,7 +9242,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "socket2 0.5.10", "tokio", "tokio-stream", @@ -9069,7 +9269,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", @@ -9676,24 +9876,32 @@ dependencies = [ name = "validator_services" version = "0.1.0" dependencies = [ + "account_utils", "beacon_node_fallback", "bls", "either", "eth2", + "eth2_keystore", "futures", "graffiti_file", + "initialized_validators", + "lighthouse_validator_store", "logging", + "mockito", "parking_lot", "reqwest", "safe_arith", + "slashing_protection", "slot_clock", "task_executor", + "tempfile", "tokio", "tracing", "tree_hash", "types", "validator_metrics", "validator_store", + "validator_test_rig", ] [[package]] @@ -9712,6 +9920,7 @@ name = "validator_test_rig" version = "0.1.0" dependencies = [ "eth2", + "ethereum_ssz", "mockito", "regex", "reqwest", @@ -9831,14 +10040,23 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -9849,11 +10067,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -9862,9 +10081,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9872,9 +10091,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -9885,13 +10104,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.12.1", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -9905,6 +10146,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "semver 1.0.27", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -9921,9 +10174,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -10015,7 +10268,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -10402,6 +10655,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.12.1", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.12.1", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.12.1", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "workspace_members" version = "0.1.0" @@ -10465,6 +10806,23 @@ dependencies = [ "time", ] +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + [[package]] name = "xdelta3" version = "0.1.5" @@ -10558,6 +10916,12 @@ dependencies = [ "time", ] +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 71398530fe..50b1733232 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,7 +161,7 @@ initialized_validators = { path = "validator_client/initialized_validators" } int_to_bytes = { path = "consensus/int_to_bytes" } itertools = "0.14" kzg = { path = "crypto/kzg" } -libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", default-features = false, features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "secp256k1", "macros", "metrics", "quic", "upnp", "gossipsub"] } +libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", default-features = false, features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "secp256k1", "macros", "metrics", "quic", "upnp", "gossipsub", "gossipsub-partial-messages"] } libsecp256k1 = "0.7" lighthouse_network = { path = "beacon_node/lighthouse_network" } lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } @@ -273,6 +273,3 @@ incremental = false inherits = "release" debug = true -[patch.crates-io] -quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "87c4ccb9bb2af494de375f5f6c62850badd26304" } - diff --git a/Makefile b/Makefile index 3c00883ce9..94ad55bf6b 100644 --- a/Makefile +++ b/Makefile @@ -30,17 +30,15 @@ TEST_FEATURES ?= # Cargo profile for regular builds. PROFILE ?= release -# List of all hard forks up to gloas. This list is used to set env variables for several tests so that -# they run for different forks. -# TODO(EIP-7732) Remove this once we extend network tests to support gloas and use RECENT_FORKS instead +# List of recent hard forks before Gloas. Used by tests that do not support Gloas yet. RECENT_FORKS_BEFORE_GLOAS=fulu -# List of all recent hard forks. This list is used to set env variables for http_api tests +# List of all recent hard forks. This list is used to set env variables for several tests. # Include phase0 to test the code paths in sync that are pre blobs RECENT_FORKS=fulu gloas # For network tests include phase0 to cover genesis syncing (blocks without blobs or columns) -TEST_NETWORK_FORKS=phase0 $(RECENT_FORKS_BEFORE_GLOAS) +TEST_NETWORK_FORKS=phase0 $(RECENT_FORKS) # Extra flags for Cargo CARGO_INSTALL_EXTRA_FLAGS?= @@ -228,7 +226,6 @@ test-op-pool-%: -p operation_pool # Run the tests in the `network` crate for all known forks. -# TODO(EIP-7732) Extend to support gloas by using RECENT_FORKS instead test-network: $(patsubst %,test-network-%,$(TEST_NETWORK_FORKS)) test-network-%: diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 946a6996e5..c6d6a09399 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2197,8 +2197,11 @@ impl BeaconChain { slot_start.is_some_and(|start| observed.saturating_sub(start) < payload_due) }); - // TODO(EIP-7732): Check blob data availability. For now, default to true. - let blob_data_available = true; + // A payload is only imported into fork choice if its data was available. + let blob_data_available = self + .canonical_head + .fork_choice_read_lock() + .is_payload_received(&beacon_block_root); Ok(PayloadAttestationData { beacon_block_root, @@ -3105,6 +3108,15 @@ impl BeaconChain { let mut blocks = filtered_chain_segment.split_off(last_index); std::mem::swap(&mut blocks, &mut filtered_chain_segment); + // Extract envelopes before passing blocks to signature verification. + let envelopes: Vec<_> = blocks + .iter() + .map(|(_, block)| match block { + RangeSyncBlock::Gloas { envelope, .. } => envelope.clone(), + RangeSyncBlock::Base(_) => None, + }) + .collect(); + let chain = self.clone(); let signature_verification_future = self.spawn_blocking_handle( move || signature_verify_chain_segment(blocks, &chain), @@ -3129,11 +3141,15 @@ impl BeaconChain { }; // Import the blocks into the chain. - for signature_verified_block in signature_verified_blocks { + for (signature_verified_block, maybe_envelope) in + signature_verified_blocks.into_iter().zip(envelopes) + { let block_slot = signature_verified_block.slot(); + let block_root = signature_verified_block.block_root(); + let block = signature_verified_block.block_cloned(); match self .process_block( - signature_verified_block.block_root(), + block_root, signature_verified_block, notify_execution_layer, BlockImportSource::RangeSync, @@ -3163,11 +3179,8 @@ impl BeaconChain { } } Err(BlockError::DuplicateFullyImported(block_root)) => { - debug!( - ?block_root, - "Ignoring already known blocks while processing chain segment" - ); - continue; + // Block was already imported, envelope might need re-import + imported_blocks.push((block_root, block_slot)); } Err(error) => { return ChainSegmentResult::Failed { @@ -3176,6 +3189,18 @@ impl BeaconChain { }; } } + + // Process the envelope after the block has been imported. + if let Some(envelope) = maybe_envelope + && let Err(e) = self + .process_range_sync_envelope(envelope, block_root, block) + .await + { + return ChainSegmentResult::Failed { + imported_blocks, + error: BlockError::EnvelopeError(Box::new(e)), + }; + } } } @@ -3394,6 +3419,7 @@ impl BeaconChain { { return Err(BlockError::ParentUnknown { parent_root: blob.block_parent_root(), + parent_block_hash: None, }); } } @@ -3520,7 +3546,10 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&parent_root) { - return Err(BlockError::ParentUnknown { parent_root }); + return Err(BlockError::ParentUnknown { + parent_root, + parent_block_hash: None, + }); } self.emit_sse_data_column_sidecar_events( @@ -4101,6 +4130,7 @@ impl BeaconChain { publish_fn()?; self.import_available_execution_payload_envelope(available_envelope) .await + .map_err(Into::into) } PayloadAvailability::MissingComponents(block_root) => Ok( AvailabilityProcessingStatus::MissingComponents(slot, block_root), diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs index a94bc697b9..17fa34ce02 100644 --- a/beacon_node/beacon_chain/src/block_production/mod.rs +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -179,14 +179,6 @@ impl BeaconChain { let re_org_max_epochs_since_finalization = Epoch::new(self.spec.reorg_max_epochs_since_finalization); - if self.spec.proposer_score_boost.is_none() { - warn!( - reason = "this network does not have proposer boost enabled", - "Ignoring proposer re-org configuration" - ); - return None; - } - let slot_delay = self .slot_clock .seconds_from_current_slot_start() diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 22e50e4185..6b1ac3b033 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -52,6 +52,7 @@ use crate::beacon_snapshot::PreProcessingSnapshot; use crate::block_verification_types::{AsBlock, BlockImportData, LookupBlock, RangeSyncBlock}; use crate::data_availability_checker::{ AvailabilityCheckError, AvailableBlock, AvailableBlockData, MaybeAvailableBlock, + verify_columns_against_block, }; use crate::data_column_verification::GossipDataColumnError; use crate::execution_payload::{ @@ -59,6 +60,7 @@ use crate::execution_payload::{ }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; +use crate::payload_envelope_verification::EnvelopeError; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ @@ -70,7 +72,7 @@ use bls::{PublicKey, PublicKeyBytes}; use educe::Educe; use eth2::types::{BlockGossip, EventKind}; use execution_layer::PayloadStatus; -pub use fork_choice::{AttestationFromBlock, PayloadVerificationStatus}; +pub use fork_choice::{AttestationFromBlock, ParentImportStatus, PayloadVerificationStatus}; use metrics::TryExt; use parking_lot::RwLockReadGuard; use proto_array::Block as ProtoBlock; @@ -92,9 +94,10 @@ use std::fs; use std::io::Write; use std::sync::Arc; use store::{Error as DBError, KeyValueStore}; -use strum::AsRefStr; +use strum::{AsRefStr, IntoStaticStr}; use task_executor::JoinHandle; use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument}; +use types::ExecutionBlockHash; use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecarList, Epoch, EthSpec, FullPayload, Hash256, InconsistentFork, KzgProofs, RelativeEpoch, @@ -114,7 +117,7 @@ const WRITE_BLOCK_PROCESSING_SSZ: bool = cfg!(feature = "write_ssz_files"); /// /// - The block is malformed/invalid (indicated by all results other than `BeaconChainError`. /// - We encountered an error whilst trying to verify the block (a `BeaconChainError`). -#[derive(Debug, AsRefStr)] +#[derive(Debug, AsRefStr, IntoStaticStr)] pub enum BlockError { /// The parent block was unknown. /// @@ -122,7 +125,10 @@ pub enum BlockError { /// /// It's unclear if this block is valid, but it cannot be processed without already knowing /// its parent. - ParentUnknown { parent_root: Hash256 }, + ParentUnknown { + parent_root: Hash256, + parent_block_hash: Option, + }, /// The block slot is greater than the present slot. /// /// ## Peer scoring @@ -137,7 +143,10 @@ pub enum BlockError { /// ## Peer scoring /// /// The peer has incompatible state transition logic and is faulty. - StateRootMismatch { block: Hash256, local: Hash256 }, + StateRootMismatch { + block: Hash256, + local: Hash256, + }, /// The block was a genesis block, these blocks cannot be re-imported. GenesisBlock, /// The slot is finalized, no need to import. @@ -156,7 +165,9 @@ pub enum BlockError { /// /// It's unclear if this block is valid, but it conflicts with finality and shouldn't be /// imported. - NotFinalizedDescendant { block_parent_root: Hash256 }, + NotFinalizedDescendant { + block_parent_root: Hash256, + }, /// Block is already known and valid, no need to re-import. /// /// ## Peer scoring @@ -183,7 +194,10 @@ pub enum BlockError { /// ## Peer scoring /// /// The block is invalid and the peer is faulty. - IncorrectBlockProposer { block: u64, local_shuffling: u64 }, + IncorrectBlockProposer { + block: u64, + local_shuffling: u64, + }, /// The `block.proposal_index` is not known. /// /// ## Peer scoring @@ -201,7 +215,10 @@ pub enum BlockError { /// ## Peer scoring /// /// The block is invalid and the peer is faulty. - BlockIsNotLaterThanParent { block_slot: Slot, parent_slot: Slot }, + BlockIsNotLaterThanParent { + block_slot: Slot, + parent_slot: Slot, + }, /// At least one block in the chain segment did not have it's parent root set to the root of /// the prior block. /// @@ -257,7 +274,9 @@ pub enum BlockError { /// If it's actually our fault (e.g. our execution node database is corrupt) we have bigger /// problems to worry about than losing peers, and we're doing the network a favour by /// disconnecting. - ParentExecutionPayloadInvalid { parent_root: Hash256 }, + ParentExecutionPayloadInvalid { + parent_root: Hash256, + }, /// This is a known invalid block that was listed in Lighthouses configuration. /// At the moment this error is only relevant as part of the Holesky network recovery efforts. KnownInvalidExecutionPayload(Hash256), @@ -285,10 +304,6 @@ pub enum BlockError { /// TODO: We may need to penalize the peer that gave us a potentially invalid rpc blob. /// https://github.com/sigp/lighthouse/issues/4546 AvailabilityCheck(AvailabilityCheckError), - /// The payload envelope's block root is unknown. - EnvelopeBlockRootUnknown(Hash256), - /// Optimistic sync is not supported for Gloas payload envelopes. - OptimisticSyncNotSupported { block_root: Hash256 }, /// An internal error has occurred when processing the block or sidecars. /// /// ## Peer scoring @@ -315,6 +330,7 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, + EnvelopeError(Box), } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -336,7 +352,7 @@ impl From for BlockError { /// Returned when block validation failed due to some issue verifying /// the execution payload. -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] pub enum ExecutionPayloadError { /// There's no eth1 connection (mandatory after merge) /// @@ -487,6 +503,50 @@ pub struct PayloadVerificationOutcome { pub payload_verification_status: PayloadVerificationStatus, } +/// The set of errors that can occur while notifying the execution layer of a new payload. +/// +/// This is deliberately narrow: notifying the EL can only fail in these two ways. The type is +/// shared by both the pre-Gloas block import path and the Gloas payload envelope path so that +/// neither pipeline has to borrow the other's error enum. It converts cleanly into both +/// [`BlockError`] and [`EnvelopeError`](crate::payload_envelope_verification::EnvelopeError) at the +/// point where the verification handle is consumed. +#[derive(Debug)] +pub enum PayloadVerificationError { + /// The execution payload was rejected by, or could not be sent to, the execution engine. + ExecutionPayloadError(ExecutionPayloadError), + /// An internal error occurred while notifying the execution layer. + BeaconChainError(Box), +} + +impl From for PayloadVerificationError { + fn from(e: ExecutionPayloadError) -> Self { + PayloadVerificationError::ExecutionPayloadError(e) + } +} + +impl From for PayloadVerificationError { + fn from(e: BeaconChainError) -> Self { + PayloadVerificationError::BeaconChainError(Box::new(e)) + } +} + +impl From for PayloadVerificationError { + fn from(e: BeaconStateError) -> Self { + PayloadVerificationError::BeaconChainError(Box::new(BeaconChainError::BeaconStateError(e))) + } +} + +impl From for BlockError { + fn from(e: PayloadVerificationError) -> Self { + match e { + PayloadVerificationError::ExecutionPayloadError(e) => { + BlockError::ExecutionPayloadError(e) + } + PayloadVerificationError::BeaconChainError(e) => BlockError::BeaconChainError(e), + } + } +} + /// Information about invalid blocks which might still be slashable despite being invalid. #[allow(clippy::enum_variant_names)] pub enum BlockSlashInfo { @@ -593,14 +653,16 @@ pub fn signature_verify_chain_segment( )?; let mut available_blocks = Vec::with_capacity(chain_segment.len()); + let mut envelopes = Vec::with_capacity(chain_segment.len()); let mut signature_verified_blocks = Vec::with_capacity(chain_segment.len()); for (block_root, block) in chain_segment { let consensus_context = ConsensusContext::new(block.slot()).set_current_block_root(block_root); - let available_block = block.into_available_block(); + let (available_block, envelope) = block.into_available_block()?; available_blocks.push(available_block.clone()); + envelopes.push(envelope); signature_verified_blocks.push(SignatureVerifiedBlock { block: MaybeAvailableBlock::Available(available_block), block_root, @@ -608,12 +670,17 @@ pub fn signature_verify_chain_segment( consensus_context, }); } - // TODO(gloas) When implementing range and backfill sync for gloas - // we need a batch verify kzg function in the new da checker as well. + chain .data_availability_checker .batch_verify_kzg_for_available_blocks(&available_blocks)?; + for (available_block, maybe_envelope) in available_blocks.iter().zip(envelopes.iter()) { + if let Some(envelope) = maybe_envelope { + verify_columns_against_block(&chain.kzg, available_block.block(), &envelope.columns)?; + } + } + // verify signatures let pubkey_cache = get_validator_pubkey_cache(chain)?; let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); @@ -657,7 +724,7 @@ pub struct SignatureVerifiedBlock { /// Used to await the result of executing payload with an EE. pub type PayloadVerificationHandle = - JoinHandle>>; + JoinHandle>>; /// A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and /// ready to import into the `BeaconChain`. The validation includes: @@ -870,7 +937,7 @@ impl GossipVerifiedBlock { let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); let (parent_block, block) = - verify_parent_block_is_known::(&fork_choice_read_lock, block)?; + verify_parent_block_and_envelope_are_known::(&fork_choice_read_lock, block)?; // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. if let Ok(bid) = block.message().body().signed_execution_payload_bid() @@ -882,13 +949,6 @@ impl GossipVerifiedBlock { }); } - // TODO(gloas) The following validation can only be completed once fork choice has been implemented: - // The block's parent execution payload (defined by bid.parent_block_hash) has been seen - // (via gossip or non-gossip sources) (a client MAY queue blocks for processing - // once the parent payload is retrieved). If execution_payload verification of block's execution - // payload parent by an execution node is complete, verify the block's execution payload - // parent (defined by bid.parent_block_hash) passes all validation. - drop(fork_choice_read_lock); // Track the number of skip slots between the block and its parent. @@ -1289,10 +1349,13 @@ impl IntoExecutionPendingBlock for RangeSyncBlock Result, BlockSlashInfo> { // Perform an early check to prevent wasting time on irrelevant blocks. + let header = self.signed_block_header(); let block_root = check_block_relevancy(self.as_block(), block_root, chain) - .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; + .map_err(|e| BlockSlashInfo::SignatureNotChecked(header.clone(), e))?; - let available_block = self.into_available_block(); + let (available_block, _envelope) = self.into_available_block().map_err(|e| { + BlockSlashInfo::SignatureNotChecked(header.clone(), BlockError::AvailabilityCheck(e)) + })?; chain .data_availability_checker .verify_kzg_for_available_block(&available_block) @@ -1381,32 +1444,24 @@ impl ExecutionPendingBlock { .observe_proposal(block_root, block.message()) .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; - if let Some(parent) = chain + match chain .canonical_head .fork_choice_read_lock() - .get_block(&block.parent_root()) + .get_parent_import_status(block.as_block()) { - // Reject any block where the parent has an invalid payload. It's impossible for a valid - // block to descend from an invalid parent. - if parent.execution_status.is_invalid() { - return Err(BlockError::ParentExecutionPayloadInvalid { + ParentImportStatus::Imported(parent) => { + if parent.execution_status.is_invalid() { + return Err(BlockError::ParentExecutionPayloadInvalid { + parent_root: block.parent_root(), + }); + } + } + ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => { + return Err(BlockError::ParentUnknown { parent_root: block.parent_root(), + parent_block_hash: block.as_block().payload_bid_parent_block_hash().ok(), }); } - } else { - // Reject any block if its parent is not known to fork choice. - // - // A block that is not in fork choice is either: - // - // - Not yet imported: we should reject this block because we should only import a child - // after its parent has been fully imported. - // - Pre-finalized: if the parent block is _prior_ to finalization, we should ignore it - // because it will revert finalization. Note that the finalized block is stored in fork - // choice, so we will not reject any child of the finalized block (this is relevant during - // genesis). - return Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }); } /* @@ -1780,6 +1835,7 @@ pub fn check_block_is_finalized_checkpoint_or_descendant< } else { Err(BlockError::ParentUnknown { parent_root: block.parent_root(), + parent_block_hash: block.as_block().payload_bid_parent_block_hash().ok(), }) } } @@ -1862,19 +1918,21 @@ pub fn get_block_header_root(block_header: &SignedBeaconBlockHeader) -> Hash256 block_root } -/// Verify the parent of `block` is known, returning some information about the parent block from -/// fork choice. +/// Verify the parent block — and, for a post-Gloas FULL child, the parent payload — are known to +/// fork choice; both missing cases return `ParentUnknown`. #[allow(clippy::type_complexity)] -fn verify_parent_block_is_known( +fn verify_parent_block_and_envelope_are_known( fork_choice_read_lock: &RwLockReadGuard>, block: Arc>, ) -> Result<(ProtoBlock, Arc>), BlockError> { - if let Some(proto_block) = fork_choice_read_lock.get_block(&block.parent_root()) { - Ok((proto_block, block)) - } else { - Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }) + match fork_choice_read_lock.get_parent_import_status(&block) { + ParentImportStatus::Imported(parent) => Ok((parent, block)), + ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => { + Err(BlockError::ParentUnknown { + parent_root: block.parent_root(), + parent_block_hash: block.payload_bid_parent_block_hash().ok(), + }) + } } } @@ -1901,10 +1959,11 @@ fn load_parent>( if !chain .canonical_head .fork_choice_read_lock() - .contains_block(&block.parent_root()) + .is_parent_imported(block.as_block()) { return Err(BlockError::ParentUnknown { parent_root: block.parent_root(), + parent_block_hash: block.as_block().payload_bid_parent_block_hash().ok(), }); } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index be73ef15d7..18e95f58f3 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -2,10 +2,12 @@ use crate::data_availability_checker::{AvailabilityCheckError, DataAvailabilityC pub use crate::data_availability_checker::{ AvailableBlock, AvailableBlockData, MaybeAvailableBlock, }; +use crate::payload_envelope_verification::AvailableEnvelope; +use crate::payload_envelope_verification::gossip_verified_envelope::verify_envelope_consistency; use crate::{BeaconChainTypes, PayloadVerificationOutcome}; -use educe::Educe; use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::sync::Arc; use types::data::BlobIdentifier; use types::{ @@ -40,15 +42,27 @@ impl LookupBlock { } } -/// A fully available block that has been constructed by range sync. -/// The block contains all the data required to import into fork choice. -/// This includes any and all blobs/columns required, including zero if -/// none are required. This can happen if the block is pre-deneb or if -/// it's simply past the DA boundary. -#[derive(Clone, Educe)] -#[educe(Hash(bound(E: EthSpec)))] -pub struct RangeSyncBlock { - block: AvailableBlock, +/// A block that has been constructed by range sync, ready for import. +/// Pre-Gloas: wraps an `AvailableBlock` with all data. +/// Gloas: carries the block and an optional envelope which contains the sidecar data. +/// +/// Note: In the gloas case, we only ensure that the block is consistent with the envelope +/// if the envelope is `Some` when constructing a `RangeSyncBlock` type. +/// If `envelope` is None, then there is no guarantee that the canonical chain also contains +/// an empty payload. The only way to ensure that is to process the next block. +#[derive(Clone)] +pub enum RangeSyncBlock { + Base(AvailableBlock), + Gloas { + block: Arc>, + envelope: Option>, + }, +} + +impl Hash for RangeSyncBlock { + fn hash(&self, state: &mut H) { + self.block_root().hash(state); + } } impl Debug for RangeSyncBlock { @@ -59,31 +73,48 @@ impl Debug for RangeSyncBlock { impl RangeSyncBlock { pub fn block_root(&self) -> Hash256 { - self.block.block_root() + match self { + Self::Base(block) => block.block_root(), + Self::Gloas { block, .. } => block.canonical_root(), + } } pub fn as_block(&self) -> &SignedBeaconBlock { - self.block.block() + match self { + Self::Base(block) => block.block(), + Self::Gloas { block, .. } => block, + } } pub fn block_cloned(&self) -> Arc> { - self.block.block_cloned() + match self { + Self::Base(block) => block.block_cloned(), + Self::Gloas { block, .. } => block.clone(), + } } pub fn block_data(&self) -> &AvailableBlockData { - self.block.data() + match self { + Self::Base(block) => block.data(), + Self::Gloas { .. } => &AvailableBlockData::NoData, + } + } + + /// Returns the data columns associated with this block. For Gloas blocks the columns are + /// carried by the payload envelope rather than `block_data`, so this unwraps that case. + pub fn data_columns(&self) -> Option> { + match self { + Self::Base(block) => block.data().data_columns(), + Self::Gloas { envelope, .. } => envelope + .as_ref() + .map(|envelope| envelope.columns.clone()) + .filter(|columns| !columns.is_empty()), + } } } impl RangeSyncBlock { - /// Constructs an `RangeSyncBlock` from a block and availability data. - /// - /// # Errors - /// - /// Returns `AvailabilityCheckError` if: - /// - `InvalidAvailableBlockData`: Block data is provided but not required. - /// - `MissingBlobs`: Block requires blobs but they are missing or incomplete. - /// - `MissingCustodyColumns`: Block requires custody columns but they are incomplete. + /// Constructs a `RangeSyncBlock` from a block and availability data (pre-Gloas). pub fn new( block: Arc>, block_data: AvailableBlockData, @@ -93,33 +124,91 @@ impl RangeSyncBlock { where T: BeaconChainTypes, { + if block.fork_name_unchecked().gloas_enabled() { + return Err(AvailabilityCheckError::InvalidVariant); + } let available_block = AvailableBlock::new(block, block_data, da_checker, spec)?; - Ok(Self { - block: available_block, - }) + Ok(Self::Base(available_block)) + } + + /// Constructs a Gloas `RangeSyncBlock` with block and optional `AvailableEnvelope` + /// which wraps the payload envelope with its data columns. + /// + /// This function only checks for consistency between the block and the envelope + /// if envelope.is_some() == true . + /// In the `None` case, we cannot guarantee that the payload is empty until we + /// process the block that builds on top of this block. + /// + /// Expects `block.canonical_root() == envelope.beacon_block_root` as they are coupled. + pub fn new_gloas( + block: Arc>, + envelope: Option>, + ) -> Result { + if let Some(envelope) = envelope.as_ref() { + let execution_bid = &block + .message() + .body() + .signed_execution_payload_bid() + .map_err(|e| format!("missing signed_execution_payload_bid: {e:?}"))? + .message; + // Skip the finalized-slot check; range sync imports historical (finalized) blocks. + let latest_finalized_slot = Slot::new(0); + verify_envelope_consistency( + envelope.message(), + &block, + execution_bid, + latest_finalized_slot, + ) + .map_err(|e| format!("Inconsistent envelope: {e:?}"))?; + } + + Ok(Self::Gloas { block, envelope }) } #[allow(clippy::type_complexity)] pub fn deconstruct(self) -> (Hash256, Arc>, AvailableBlockData) { - self.block.deconstruct() + match self { + Self::Base(block) => block.deconstruct(), + Self::Gloas { block, .. } => { + (block.canonical_root(), block, AvailableBlockData::NoData) + } + } } pub fn n_blobs(&self) -> usize { - match self.block_data() { - AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0, - AvailableBlockData::Blobs(blobs) => blobs.len(), + match self { + Self::Base(block) => match block.data() { + AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0, + AvailableBlockData::Blobs(blobs) => blobs.len(), + }, + Self::Gloas { .. } => 0, } } pub fn n_data_columns(&self) -> usize { - match self.block_data() { - AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0, - AvailableBlockData::DataColumns(columns) => columns.len(), + match self { + Self::Base(block) => match block.data() { + AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0, + AvailableBlockData::DataColumns(columns) => columns.len(), + }, + Self::Gloas { .. } => 0, } } - pub fn into_available_block(self) -> AvailableBlock { - self.block + /// Converts into an `AvailableBlock` for import, returning any associated envelope + /// separately. Callers processing Gloas blocks must handle the envelope themselves. + #[allow(clippy::type_complexity)] + pub fn into_available_block( + self, + ) -> Result<(AvailableBlock, Option>), AvailabilityCheckError> { + match self { + Self::Base(block) => Ok((block, None)), + Self::Gloas { block, envelope } => { + let available = + AvailableBlock::new_gloas(block).map_err(AvailabilityCheckError::Unexpected)?; + Ok((available, envelope)) + } + } } } @@ -405,13 +494,13 @@ impl AsBlock for RangeSyncBlock { self.as_block().message() } fn as_block(&self) -> &SignedBeaconBlock { - self.block.as_block() + RangeSyncBlock::as_block(self) } fn block_cloned(&self) -> Arc> { - self.block.block_cloned() + RangeSyncBlock::block_cloned(self) } fn canonical_root(&self) -> Hash256 { - self.block.block_root() + self.block_root() } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 3c2ba13fed..a0b117f072 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -669,7 +669,7 @@ impl DataAvailabilityChecker { /// Verify a batch of data columns belonging to a single block, picking the right commitment /// source for the block's fork (Fulu: inline on column; Gloas: from the embedded payload bid). -fn verify_columns_against_block( +pub fn verify_columns_against_block( kzg: &Kzg, block: &SignedBeaconBlock, columns: &[Arc>], @@ -792,7 +792,12 @@ async fn availability_cache_maintenance_service( #[derive(Debug, Clone)] // TODO(#8633) move this to `block_verification_types.rs` pub enum AvailableBlockData { - /// Block is pre-Deneb or has zero blobs + /// Block has no inline DA object for block import. + /// + /// This covers: + /// - pre-Deneb blocks, + /// - blocks with zero blobs, and + /// - Gloas blocks, where DA is checked on the payload envelope instead. NoData, /// Block is post-Deneb, pre-PeerDAS and has more than zero blobs Blobs(BlobSidecarList), @@ -861,8 +866,6 @@ pub struct AvailableBlock { #[educe(Hash(ignore))] /// Timestamp at which this block first became available (UNIX timestamp, time since 1970). blobs_available_timestamp: Option, - #[educe(Hash(ignore))] - pub spec: Arc, } impl AvailableBlock { @@ -895,12 +898,8 @@ impl AvailableBlock { match &block_data { AvailableBlockData::NoData => { // For Gloas, DA is checked for the PayloadEnvelope, not for the block. - if !block.fork_name_unchecked().gloas_enabled() { - if columns_required { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } else if blobs_required { - return Err(AvailabilityCheckError::MissingBlobs); - } + if !block.fork_name_unchecked().gloas_enabled() && columns_required { + return Err(AvailabilityCheckError::MissingCustodyColumns); } } AvailableBlockData::Blobs(blobs) => { @@ -956,10 +955,22 @@ impl AvailableBlock { block, blob_data: block_data, blobs_available_timestamp: None, - spec: spec.clone(), }) } + pub fn new_gloas(block: Arc>) -> Result { + if block.fork_name_unchecked().gloas_enabled() { + Ok(Self { + block_root: block.canonical_root(), + block, + blob_data: AvailableBlockData::NoData, + blobs_available_timestamp: None, + }) + } else { + Err("Block is not gloas".to_owned()) + } + } + pub fn block(&self) -> &SignedBeaconBlock { &self.block } @@ -1011,7 +1022,6 @@ impl AvailableBlock { } }, blobs_available_timestamp: self.blobs_available_timestamp, - spec: self.spec.clone(), }) } } @@ -1302,7 +1312,7 @@ mod test { let available_blocks = blocks_with_columns .into_iter() - .map(|block| block.into_available_block()) + .map(|block| block.into_available_block().unwrap().0) .collect::>(); // WHEN verifying all blocks together (totalling 256 data columns) diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index ab69a62985..2653c84860 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -1,7 +1,8 @@ use kzg::{Error as KzgError, KzgCommitment}; +use strum::IntoStaticStr; use types::{BeaconStateError, ColumnIndex, Hash256}; -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] pub enum Error { InvalidBlobs(KzgError), MissingBid(Hash256), diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 2ce0b4cd4a..2254728850 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -9,7 +9,7 @@ use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::{BeaconChainTypes, BlockProcessStatus}; use lru::LruCache; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use ssz_types::{RuntimeFixedVector, RuntimeVariableList}; +use ssz_types::RuntimeFixedVector; use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; @@ -200,7 +200,6 @@ impl PendingComponents { /// must be persisted in the DB along with the block. pub fn make_available( &self, - spec: &Arc, num_expected_columns_opt: Option, ) -> Result>, AvailabilityCheckError> { let Some(CachedBlock::Executed(block)) = &self.block else { @@ -235,36 +234,7 @@ impl PendingComponents { } } } else { - // Before PeerDAS, blobs - let num_received_blobs = self.verified_blobs.iter().flatten().count(); - match num_received_blobs.cmp(&num_expected_blobs) { - Ordering::Greater => { - // Should never happen - return Err(AvailabilityCheckError::Unexpected(format!( - "too many blobs got {num_received_blobs} expected {num_expected_blobs}" - ))); - } - Ordering::Equal => { - let max_blobs = spec.max_blobs_per_block(block.block.epoch()) as usize; - let blobs_vec = self - .verified_blobs - .iter() - .flatten() - .map(|blob| blob.clone().to_blob()) - .collect::>(); - let blobs_len = blobs_vec.len(); - let blobs = RuntimeVariableList::new(blobs_vec, max_blobs).map_err(|_| { - AvailabilityCheckError::Unexpected(format!( - "over max_blobs len {blobs_len} max {max_blobs}" - )) - })?; - Some(AvailableBlockData::Blobs(blobs)) - } - Ordering::Less => { - // Not enough blobs received yet - None - } - } + Some(AvailableBlockData::NoData) }; // Block's data not available yet @@ -300,7 +270,6 @@ impl PendingComponents { block: block.clone(), blob_data, blobs_available_timestamp, - spec: spec.clone(), }; self.span.in_scope(|| { @@ -558,7 +527,7 @@ impl DataAvailabilityCheckerInner { num_expected_columns_opt: Option, ) -> Result, AvailabilityCheckError> { if let Some(available_block) = - pending_components.make_available(&self.spec, num_expected_columns_opt)? + pending_components.make_available(num_expected_columns_opt)? { // Explicitly drop read lock before acquiring write lock drop(pending_components); diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index c8976fc6a8..d8cd3e0287 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -9,7 +9,7 @@ use crate::{ BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, BlockProductionError, - ExecutionPayloadError, + ExecutionPayloadError, PayloadVerificationError, }; use execution_layer::{ BlockProposalContentsType, BuilderParams, NewPayloadRequest, PayloadAttributes, @@ -104,7 +104,9 @@ impl PayloadNotifier { }) } - pub async fn notify_new_payload(self) -> Result { + pub async fn notify_new_payload( + self, + ) -> Result { if let Some(precomputed_status) = self.payload_verification_status { Ok(precomputed_status) } else { @@ -133,7 +135,7 @@ pub async fn notify_new_payload( slot: Slot, parent_beacon_block_root: Hash256, new_payload_request: NewPayloadRequest<'_, T::EthSpec>, -) -> Result { +) -> Result { let execution_layer = chain .execution_layer .as_ref() diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 804268a613..9795d360ca 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -85,9 +85,9 @@ pub use beacon_fork_choice_store::{ }; pub use block_verification::{ BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, - IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, - PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars, - get_block_root, signature_verify_chain_segment, + IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, ParentImportStatus, + PayloadVerificationError, PayloadVerificationOutcome, PayloadVerificationStatus, + build_blob_data_column_sidecars, get_block_root, signature_verify_chain_segment, }; pub use block_verification_types::AvailabilityPendingExecutedBlock; pub use block_verification_types::ExecutedBlock; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index df1b005820..4f7bdbbb52 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1711,21 +1711,21 @@ pub static PARTIAL_DATA_COLUMN_SIDECAR_HEADER_PROCESSING_REQUESTS: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_partial_data_column_sidecar_header_processing_dupes_total", - "Number of partial data column sidecars verified for gossip (excluding dupes)", + "Number of partial data column sidecar headers received that matched a cached header", ) }); pub static PARTIAL_DATA_COLUMN_SIDECAR_HEADER_PROCESSING_SUCCESSES: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_partial_data_column_sidecar_header_processing_successes_total", - "Number of partial data column sidecar headers verified for gossip (excluding dupes)", + "Number of partial data column sidecar headers verified for gossip", ) }); pub static PARTIAL_DATA_COLUMN_SIDECAR_HEADER_GOSSIP_VERIFICATION_TIMES: LazyLock< diff --git a/beacon_node/beacon_chain/src/partial_data_column_assembler.rs b/beacon_node/beacon_chain/src/partial_data_column_assembler.rs index 0ce754c8a0..ee59102cfd 100644 --- a/beacon_node/beacon_chain/src/partial_data_column_assembler.rs +++ b/beacon_node/beacon_chain/src/partial_data_column_assembler.rs @@ -106,7 +106,7 @@ impl PartialDataColumnAssembler { let merged = match existing.merge(&partial) { Ok(merged) => merged, Err(err) => { - error!("Unexpected error merging partial data column: {:?}", err); + error!(error = ?err, "Unexpected error merging partial data column"); continue; } }; @@ -204,14 +204,16 @@ impl PartialDataColumnAssembler { .cloned() } - /// Get all current partials for a block for publishing after fetching local blobs. - /// To unlock future publishing, mark blobs as fetched locally. - /// We do this within one write lock to avoid useless double publishes. - pub fn get_partials_and_mark_as_local_fetched( + /// Get all current columns for a block (complete *and* incomplete) for publishing after + /// fetching local blobs. + /// + /// To unlock future publishing, mark blobs as fetched locally. We do this within one write + /// lock to avoid useless double publishes. + pub fn get_columns_and_mark_as_local_fetched( &self, block_root: Hash256, header: &Arc>, - ) -> Vec> { + ) -> Vec> { let mut assemblies = self.assemblies.write(); let assembly = assemblies.get_or_insert_mut(block_root, || PartialAssembly { header: header.clone(), @@ -221,17 +223,7 @@ impl PartialDataColumnAssembler { assembly.has_local_blobs = true; - assembly - .columns - .values() - .filter_map(|value| { - if let AssemblyColumn::Incomplete(partial) = value { - Some(partial.clone()) - } else { - None - } - }) - .collect() + assembly.columns.values().cloned().collect() } /// Get header for a block if we have an active assembly @@ -473,6 +465,37 @@ mod tests { assert_eq!(result.updated_partials[0].index(), 0); } + #[test] + fn get_columns_returns_complete_and_incomplete() { + let assembler = make_assembler(); + let root = Hash256::repeat_byte(1); + let header = Arc::new(make_header(4)); + + // One complete column (all cells present) and one still-incomplete column. + let complete = make_partial(root, 0, 4, &[0, 1, 2, 3]); + let incomplete = make_partial(root, 1, 4, &[0, 1]); + assembler.merge_partials(root, vec![complete, incomplete], header.clone()); + + // Both must be returned for seeding. Previously the complete column was dropped, so it was + // published as an empty placeholder and never served to peers. + let columns = assembler.get_columns_and_mark_as_local_fetched(root, &header); + assert_eq!(columns.len(), 2); + assert_eq!( + columns + .iter() + .filter(|c| matches!(c, AssemblyColumn::Complete(_))) + .count(), + 1 + ); + assert_eq!( + columns + .iter() + .filter(|c| matches!(c, AssemblyColumn::Incomplete(_))) + .count(), + 1 + ); + } + // -- mark_as_complete tests -- #[test] diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index 73ddb43273..00806f0e17 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -4,9 +4,10 @@ use std::time::Duration; use eth2::types::{EventKind, SseExecutionPayload, SseExecutionPayloadAvailable}; use fork_choice::PayloadVerificationStatus; use slot_clock::SlotClock; +use state_processing::{VerifySignatures, envelope_processing::verify_execution_payload_envelope}; use store::StoreOp; use tracing::{debug, error, info, info_span, instrument, warn}; -use types::{BlockImportSource, Hash256, SignedExecutionPayloadEnvelope}; +use types::{BlockImportSource, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope}; use super::{ AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, @@ -19,6 +20,7 @@ use crate::{ metrics, payload_envelope_verification::{ AvailabilityPendingExecutedEnvelope, ExecutionPendingEnvelope, + load_snapshot_from_state_root, payload_notifier::PayloadNotifier, }, validator_monitor::get_slot_delay_ms, }; @@ -33,6 +35,13 @@ impl BeaconChain { /// /// Returns an `Err` if the given payload envelope was invalid, or an error was encountered during /// verification. + /// + /// Note: Returns a `BlockError` even though its an envelope processing function. + /// The reason is that this function actually imports the envelope in `check_envelope_availability_and_import` + /// which is coupled tightly with the block and data column import functions. + /// These functions return one error type for consistency across function signatures. + /// In the future, we could make the import error types more generic and then + /// this function could return an `EnvelopeError` as well. #[instrument(skip_all, fields(block_root = ?block_root, envelope_source = %envelope_source))] pub async fn process_execution_payload_envelope( self: &Arc, @@ -41,7 +50,7 @@ impl BeaconChain { notify_execution_layer: NotifyExecutionLayer, envelope_source: BlockImportSource, publish_fn: impl FnOnce() -> Result<(), EnvelopeError>, - ) -> Result { + ) -> Result { let block_slot = unverified_envelope.signed_envelope.slot(); // Set observed time if not already set. Usually this should be set by gossip or RPC, @@ -83,13 +92,7 @@ impl BeaconChain { // about what the function actually does. let executed_envelope = chain .into_executed_payload_envelope(execution_pending) - .await - .map_err(|error| match error { - BlockError::ExecutionPayloadError(error) => { - EnvelopeError::ExecutionPayloadError(error) - } - error => EnvelopeError::ImportError(error), - })?; + .await?; // Record the time it took to wait for execution layer verification. if let Some(timestamp) = slot_clock.now_duration() { @@ -100,7 +103,6 @@ impl BeaconChain { self.check_envelope_availability_and_import(executed_envelope) .await - .map_err(EnvelopeError::ImportError) }; // Verify and import the payload envelope. @@ -128,28 +130,12 @@ impl BeaconChain { Ok(status) } - Err(EnvelopeError::BeaconChainError(e)) => { - if matches!(e.as_ref(), BeaconChainError::TokioJoin(_)) { - debug!(error = ?e, "Envelope processing cancelled"); - } else { - warn!(error = ?e, "Execution payload envelope rejected"); - } - Err(EnvelopeError::BeaconChainError(e)) - } - Err(EnvelopeError::ImportError(BlockError::BeaconChainError(e))) => { - if matches!(e.as_ref(), BeaconChainError::TokioJoin(_)) { - debug!(error = ?e, "Envelope processing cancelled"); - } else { - warn!(error = ?e, "Execution payload envelope rejected"); - } - Err(EnvelopeError::ImportError(BlockError::BeaconChainError(e))) - } - Err(other) => { + Err(err) => { warn!( - reason = other.to_string(), + reason = err.to_string(), "Execution payload envelope rejected" ); - Err(other) + Err(err) } } } @@ -175,7 +161,7 @@ impl BeaconChain { async fn into_executed_payload_envelope( self: Arc, pending_envelope: ExecutionPendingEnvelope, - ) -> Result, BlockError> { + ) -> Result, EnvelopeError> { let ExecutionPendingEnvelope { signed_envelope, block_root, @@ -192,7 +178,7 @@ impl BeaconChain { .payload_verification_status .is_optimistic() { - return Err(BlockError::OptimisticSyncNotSupported { block_root }); + return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); } Ok(AvailabilityPendingExecutedEnvelope::new( @@ -206,7 +192,7 @@ impl BeaconChain { pub async fn import_available_execution_payload_envelope( self: &Arc, envelope: Box>, - ) -> Result { + ) -> Result { let AvailableExecutedEnvelope { envelope, block_root, @@ -243,13 +229,13 @@ impl BeaconChain { signed_envelope: AvailableEnvelope, block_root: Hash256, payload_verification_status: PayloadVerificationStatus, - ) -> Result { + ) -> Result { // Everything in this initial section is on the hot path for processing the envelope. // Take an upgradable read lock on fork choice so we can check if this block has already // been imported. We don't want to repeat work importing a block that is already imported. let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); if !fork_choice_reader.contains_block(&block_root) { - return Err(BlockError::EnvelopeBlockRootUnknown(block_root)); + return Err(EnvelopeError::BlockRootNotInForkChoice(block_root)); } // TODO(gloas) add defensive check to see if payload envelope is already in fork choice @@ -264,7 +250,7 @@ impl BeaconChain { // node which can be eligible for head. fork_choice .on_valid_payload_envelope_received(block_root) - .map_err(|e| BlockError::InternalError(format!("{e:?}")))?; + .map_err(|e| EnvelopeError::InternalError(format!("{e:?}")))?; // TODO(gloas) emit SSE event if the payload became the new head payload @@ -388,4 +374,58 @@ impl BeaconChain { )); } } + + /// Process an envelope received during range sync. The associated block must already + /// be imported into fork choice. This performs signature verification, state processing, + /// EL verification and import. + #[instrument(skip_all, level = "debug")] + pub async fn process_range_sync_envelope( + self: &Arc, + available_envelope: AvailableEnvelope, + block_root: Hash256, + block: Arc>, + ) -> Result<(), EnvelopeError> { + let signed_envelope = available_envelope.envelope().clone(); + + // Load the state snapshot for envelope processing + let state_root = block.state_root(); + let snapshot = load_snapshot_from_state_root::(block_root, state_root, &self.store)?; + + // Verify envelope signature and state processing + verify_execution_payload_envelope( + &snapshot.pre_state, + &signed_envelope, + VerifySignatures::True, + snapshot.state_root, + &self.spec, + )?; + + // Send to EL for verification + let payload_notifier = PayloadNotifier::new( + self.clone(), + signed_envelope.clone(), + block, + NotifyExecutionLayer::Yes, + )?; + + let payload_verification_status = payload_notifier.notify_new_payload().await?; + + // Import directly — we already have all components (envelope + columns). + let chain = self.clone(); + let _ = self + .spawn_blocking_handle( + move || { + chain.import_execution_payload_envelope( + available_envelope, + block_root, + payload_verification_status, + ) + }, + "range_sync_envelope_import", + ) + .await + .map_err(|e| EnvelopeError::BeaconChainError(Box::new(e)))?; + + Ok(()) + } } diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index a1e4e34eb6..a0d34949c6 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -30,7 +30,7 @@ use types::{ use crate::{ BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, ExecutionPayloadError, - PayloadVerificationOutcome, + PayloadVerificationError, PayloadVerificationOutcome, }; pub mod execution_pending_envelope; @@ -40,7 +40,7 @@ mod payload_notifier; pub use execution_pending_envelope::ExecutionPendingEnvelope; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct AvailableEnvelope { envelope: Arc>, pub columns: DataColumnSidecarList, @@ -54,6 +54,10 @@ impl AvailableEnvelope { Self { envelope, columns } } + pub fn envelope(&self) -> &Arc> { + &self.envelope + } + pub fn message(&self) -> &ExecutionPayloadEnvelope { &self.envelope.message } @@ -155,15 +159,23 @@ pub enum EnvelopeError { latest_finalized_slot: Slot, }, /// Some Beacon Chain Error - BeaconChainError(Arc), + BeaconChainError(Box), /// Some Beacon State error BeaconStateError(BeaconStateError), /// Some EnvelopeProcessingError EnvelopeProcessingError(EnvelopeProcessingError), /// Error verifying the execution payload ExecutionPayloadError(ExecutionPayloadError), - /// An error from importing the envelope. - ImportError(BlockError), + /// Optimistic sync is not supported for Gloas payload envelopes. + OptimisticSyncNotSupported { block_root: Hash256 }, + /// The envelope's beacon block was not present in fork choice at import time. + /// + /// Unlike [`EnvelopeError::BlockRootUnknown`] (raised during gossip verification, where the + /// block may simply not have arrived yet), this is raised during import where the block is + /// expected to already be present, so it indicates an internal inconsistency. + BlockRootNotInForkChoice(Hash256), + /// An internal error occurred while importing the envelope (e.g. updating fork choice). + InternalError(String), } impl std::fmt::Display for EnvelopeError { @@ -172,9 +184,31 @@ impl std::fmt::Display for EnvelopeError { } } +impl EnvelopeError { + pub fn penalize_peer(&self) -> bool { + match self { + EnvelopeError::BadSignature + | EnvelopeError::BuilderIndexMismatch { .. } + | EnvelopeError::SlotMismatch { .. } + | EnvelopeError::BlockHashMismatch { .. } + | EnvelopeError::UnknownValidator { .. } + | EnvelopeError::IncorrectBlockProposer { .. } + | EnvelopeError::EnvelopeProcessingError(_) => true, + EnvelopeError::ExecutionPayloadError(e) => e.penalize_peer(), + EnvelopeError::BlockRootUnknown { .. } + | EnvelopeError::PriorToFinalization { .. } + | EnvelopeError::BeaconChainError(_) + | EnvelopeError::BeaconStateError(_) + | EnvelopeError::OptimisticSyncNotSupported { .. } + | EnvelopeError::BlockRootNotInForkChoice(_) + | EnvelopeError::InternalError(_) => false, + } + } +} + impl From for EnvelopeError { fn from(e: BeaconChainError) -> Self { - EnvelopeError::BeaconChainError(Arc::new(e)) + EnvelopeError::BeaconChainError(Box::new(e)) } } @@ -192,7 +226,24 @@ impl From for EnvelopeError { impl From for EnvelopeError { fn from(e: DBError) -> Self { - EnvelopeError::BeaconChainError(Arc::new(BeaconChainError::DBError(e))) + EnvelopeError::BeaconChainError(Box::new(BeaconChainError::DBError(e))) + } +} + +impl From for BlockError { + fn from(e: EnvelopeError) -> Self { + BlockError::EnvelopeError(Box::new(e)) + } +} + +impl From for EnvelopeError { + fn from(e: PayloadVerificationError) -> Self { + match e { + PayloadVerificationError::ExecutionPayloadError(e) => { + EnvelopeError::ExecutionPayloadError(e) + } + PayloadVerificationError::BeaconChainError(e) => EnvelopeError::BeaconChainError(e), + } } } diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs index 0bbe32525a..8a47e4689a 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -7,7 +7,7 @@ use tracing::warn; use types::{SignedBeaconBlock, SignedExecutionPayloadEnvelope}; use crate::{ - BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer, + BeaconChain, BeaconChainTypes, NotifyExecutionLayer, PayloadVerificationError, execution_payload::notify_new_payload, payload_envelope_verification::EnvelopeError, }; @@ -31,8 +31,7 @@ impl PayloadNotifier { match notify_execution_layer { NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => { - let new_payload_request = Self::build_new_payload_request(&envelope, &block) - .map_err(EnvelopeError::ImportError)?; + let new_payload_request = Self::build_new_payload_request(&envelope, &block)?; // TODO(gloas): check and test RLP block hash calculation post-Gloas if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() { warn!( @@ -58,7 +57,9 @@ impl PayloadNotifier { }) } - pub async fn notify_new_payload(self) -> Result { + pub async fn notify_new_payload( + self, + ) -> Result { if let Some(precomputed_status) = self.payload_verification_status { Ok(precomputed_status) } else { @@ -71,12 +72,12 @@ impl PayloadNotifier { fn build_new_payload_request<'a>( envelope: &'a SignedExecutionPayloadEnvelope, block: &'a SignedBeaconBlock, - ) -> Result, BlockError> { + ) -> Result, PayloadVerificationError> { let bid = &block .message() .body() .signed_execution_payload_bid() - .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))? + .map_err(|e| PayloadVerificationError::BeaconChainError(Box::new(e.into())))? .message; let versioned_hashes = bid diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 919bb43bfd..62c7fb3a45 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -4,6 +4,7 @@ use crate::data_availability_checker::DataAvailabilityChecker; use crate::graffiti_calculator::GraffitiSettings; use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sidecars_gloas}; use crate::observed_operations::ObservationOutcome; +use crate::payload_envelope_verification::AvailableEnvelope; pub use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::{BeaconBlockResponseWrapper, CustodyContext, get_block_root}; use crate::{ @@ -1568,6 +1569,7 @@ where beacon_block_root: Hash256, mut state: Cow>, state_root: Hash256, + payload_present_override: Option, ) -> Result, BeaconChainError> { assert_eq!( state.get_latest_block_root(state_root), @@ -1602,12 +1604,17 @@ where *state.get_block_root(target_slot)? }; - let payload_present = state.fork_name_unchecked().gloas_enabled() - && state.latest_block_header().slot != slot - && self - .chain - .canonical_head - .block_has_canonical_payload(&beacon_block_root, &self.spec)?; + let payload_present = match payload_present_override { + Some(payload_present) => payload_present, + None => { + state.fork_name_unchecked().gloas_enabled() + && state.latest_block_header().slot != slot + && self + .chain + .canonical_head + .block_has_canonical_payload(&beacon_block_root, &self.spec)? + } + }; Ok(Attestation::empty_for_signing( index, @@ -1646,7 +1653,11 @@ where state_root, head_block_root, attestation_slot, - MakeAttestationOptions { limit: None, fork }, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: None, + }, ) .0 } @@ -1673,7 +1684,11 @@ where state_root, head_block_root, attestation_slot, - MakeAttestationOptions { limit: None, fork }, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: None, + }, ) .0 } @@ -1687,7 +1702,7 @@ where attestation_slot: Slot, opts: MakeAttestationOptions, ) -> (Vec, Vec) { - let MakeAttestationOptions { limit, fork } = opts; + let MakeAttestationOptions { limit, fork, .. } = opts; let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap(); let num_attesters = AtomicUsize::new(0); @@ -1780,7 +1795,11 @@ where attestation_slot: Slot, opts: MakeAttestationOptions, ) -> (Vec>, Vec) { - let MakeAttestationOptions { limit, fork } = opts; + let MakeAttestationOptions { + limit, + fork, + payload_present_override, + } = opts; let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap(); let num_attesters = AtomicUsize::new(0); @@ -1813,6 +1832,7 @@ where head_block_root.into(), Cow::Borrowed(state), state_root, + payload_present_override, ) .unwrap(); @@ -2015,7 +2035,11 @@ where state_root, block_hash, slot, - MakeAttestationOptions { limit, fork }, + MakeAttestationOptions { + limit, + fork, + payload_present_override: None, + }, ) } @@ -2906,18 +2930,29 @@ where block: Arc>, ) -> RangeSyncBlock { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); + let is_gloas = block.fork_name_unchecked().gloas_enabled(); // For Gloas, kzg commitments live in the bid (`signed_execution_payload_bid`), so the // body's `blob_kzg_commitments()` accessor returns Err. `num_expected_blobs` already // handles both shapes. let has_blobs = block.num_expected_blobs() > 0; if !has_blobs { - return RangeSyncBlock::new( - block, - AvailableBlockData::NoData, - &self.chain.data_availability_checker, - self.chain.spec.clone(), - ) - .unwrap(); + return if is_gloas { + let envelope = self + .chain + .get_payload_envelope(&block_root) + .unwrap() + .map(Arc::new) + .map(|envelope| AvailableEnvelope::new(envelope, vec![])); + RangeSyncBlock::new_gloas(block, envelope).unwrap() + } else { + RangeSyncBlock::new( + block, + AvailableBlockData::NoData, + &self.chain.data_availability_checker, + self.chain.spec.clone(), + ) + .unwrap() + }; } // Blobs are stored as data columns from Fulu (PeerDAS) @@ -2929,14 +2964,24 @@ where .unwrap() .unwrap(); let custody_columns = columns.into_iter().collect::>(); - let block_data = AvailableBlockData::new_with_data_columns(custody_columns); - RangeSyncBlock::new( - block, - block_data, - &self.chain.data_availability_checker, - self.chain.spec.clone(), - ) - .unwrap() + if is_gloas { + let envelope = self + .chain + .get_payload_envelope(&block_root) + .unwrap() + .map(Arc::new) + .map(|envelope| AvailableEnvelope::new(envelope, custody_columns)); + RangeSyncBlock::new_gloas(block, envelope).unwrap() + } else { + let block_data = AvailableBlockData::new_with_data_columns(custody_columns); + RangeSyncBlock::new( + block, + block_data, + &self.chain.data_availability_checker, + self.chain.spec.clone(), + ) + .unwrap() + } } else { let blobs = self.chain.get_blobs(&block_root).unwrap().blobs(); let block_data = if let Some(blobs) = blobs { @@ -2961,6 +3006,19 @@ where block: Arc>>, blob_items: Option<(KzgProofs, BlobsList)>, ) -> Result, BlockError> { + if block.fork_name_unchecked().gloas_enabled() { + let columns = blob_items + .map(|_| generate_data_column_sidecars_from_block(&block, &self.spec)) + .unwrap_or_default(); + let envelope = self + .chain + .get_payload_envelope(&block.canonical_root()) + .map_err(|e| BlockError::BeaconChainError(Box::new(e)))? + .map(Arc::new) + .map(|envelope| AvailableEnvelope::new(envelope, columns)); + return RangeSyncBlock::new_gloas(block, envelope).map_err(BlockError::InternalError); + } + Ok(if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { let epoch = block.slot().epoch(E::slots_per_epoch()); let sampling_columns = self.chain.sampling_columns_for_epoch(epoch); @@ -3744,6 +3802,8 @@ pub struct MakeAttestationOptions { pub limit: Option, /// Fork to use for signing attestations. pub fork: Fork, + /// Override post-Gloas regular attestation payload-present encoding. + pub payload_present_override: Option, } pub enum NumBlobs { diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 1b87fc041a..9d32b37134 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -8,6 +8,7 @@ use beacon_chain::test_utils::{ use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; +use slot_clock::SlotClock; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; @@ -234,7 +235,7 @@ async fn produces_attestations() { let range_sync_block = harness .build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(block.clone())); - let available_block = range_sync_block.into_available_block(); + let (available_block, _envelope) = range_sync_block.into_available_block().unwrap(); // For Gloas non-same-slot attestations, the early attester cache returns None. let is_same_slot_attestation = slot == block_slot; @@ -299,12 +300,13 @@ async fn early_attester_cache_old_request() { .get_block(&head.beacon_block_root) .unwrap(); - let available_block = harness + let (available_block, _envelope) = harness .build_range_sync_block_from_store_blobs( Some(head.beacon_block_root), head.beacon_block.clone(), ) - .into_available_block(); + .into_available_block() + .unwrap(); harness .chain @@ -448,3 +450,69 @@ async fn gloas_attestation_index_payload_absent() { "gloas attestation to prior slot without payload should have index=0 (payload_absent)" ); } + +/// Verify that `produce_payload_attestation_data` reports `payload_present = true` but +/// `blob_data_available = false` when the envelope was observed on but not imported +/// because its data was unavailable. +/// +/// Setup: build a chain through slot 2, then at slot 3 import only the beacon block (no +/// envelope) and mark the envelope as observed on time. +#[tokio::test] +async fn gloas_payload_attestation_seen_but_data_unavailable() { + if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) { + return; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let chain = &harness.chain; + + harness.advance_slot(); + harness + .extend_chain( + 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Slot 3: import the beacon block but withhold its envelope. + harness.advance_slot(); + let state = harness.get_current_state(); + let (block_contents, _envelope, _new_state) = + harness.make_block_with_envelope(state, Slot::new(3)).await; + let block_root = block_contents.0.canonical_root(); + harness + .process_block(Slot::new(3), block_root, block_contents) + .await + .expect("block should import without envelope"); + + assert_eq!(chain.head_snapshot().beacon_block.slot(), Slot::new(3)); + + // Mark the envelope as observed at the start of the slot, before its deadline. + let slot_start = chain.slot_clock.start_of(Slot::new(3)).unwrap(); + chain.envelope_times_cache.write().set_time_observed( + block_root, + Slot::new(3), + slot_start, + None, + ); + + let pa_data = chain + .produce_payload_attestation_data(Slot::new(3)) + .expect("should produce payload attestation data"); + + assert!( + pa_data.payload_present, + "envelope observed before the deadline should vote payload_present=true" + ); + assert!( + !pa_data.blob_data_available, + "unimported envelope data should vote blob_data_available=false" + ); +} diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index da7f380e36..03b8ae58ac 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -1636,6 +1636,7 @@ async fn attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: capella_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1667,6 +1668,7 @@ async fn attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: bellatrix_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1741,6 +1743,7 @@ async fn aggregated_attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: capella_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1768,6 +1771,7 @@ async fn aggregated_attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: bellatrix_fork, limit: None, + payload_present_override: None, }, ) .0 diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 67fe0eaae0..94d4b3b9da 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -3,12 +3,14 @@ use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, LookupBlock, RangeSyncBlock}; use beacon_chain::data_availability_checker::{AvailabilityCheckError, AvailableBlockData}; use beacon_chain::data_column_verification::CustodyDataColumn; +use beacon_chain::payload_envelope_verification::AvailableEnvelope; use beacon_chain::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, ExecutionPendingBlock, WhenSlotSkipped, custody_context::NodeCustodyType, test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + MakeAttestationOptions, test_spec, }, }; use beacon_chain::{ @@ -17,6 +19,7 @@ use beacon_chain::{ }; use bls::{AggregateSignature, Keypair, Signature}; use fixed_bytes::FixedBytesExtended; +use fork_choice::PayloadStatus; use logging::create_test_tracing_subscriber; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ @@ -33,20 +36,30 @@ type E = MainnetEthSpec; // Gloas requires >= 1 validator per slot for PTC committee computation, so >= 32 for MainnetEthSpec. const VALIDATOR_COUNT: usize = 32; -const CHAIN_SEGMENT_LENGTH: usize = 64 * 5; -const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGTH - 1]; +const CHAIN_SEGMENT_LENGTH: usize = 32 * 6; +const BLOCK_INDICES: &[usize] = &[1, 32, 64]; /// A cached set of keys. static KEYPAIRS: LazyLock> = LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT)); // TODO(#8633): Delete this unnecessary enum and refactor this file to use `AvailableBlockData` instead. +#[derive(Clone)] enum DataSidecars { Blobs(BlobSidecarList), DataColumns(Vec>), } -async fn get_chain_segment() -> (Vec>, Vec>>) { +type ChainSegmentData = (Vec>, Vec>>); + +static CHAIN_SEGMENT: LazyLock> = + LazyLock::new(tokio::sync::OnceCell::new); + +async fn get_chain_segment() -> &'static ChainSegmentData { + CHAIN_SEGMENT.get_or_init(build_chain_segment).await +} + +async fn build_chain_segment() -> ChainSegmentData { // The assumption that you can re-import a block based on what you have in your DB // is no longer true, as fullnodes stores less than what they sample. // We use a supernode here to build a chain segment. @@ -146,19 +159,37 @@ where .zip(chain_segment_sidecars.iter()) .map(|(snapshot, data_sidecars)| { let block = snapshot.beacon_block.clone(); - build_range_sync_block(block, data_sidecars, chain.clone()) + build_range_sync_block( + block, + snapshot.execution_envelope.clone(), + data_sidecars, + chain.clone(), + ) }) .collect() } fn build_range_sync_block( block: Arc>, + execution_envelope: Option>>, data_sidecars: &Option>, chain: Arc>, ) -> RangeSyncBlock where T: BeaconChainTypes, { + if block.fork_name_unchecked().gloas_enabled() { + let columns = match data_sidecars { + Some(DataSidecars::DataColumns(columns)) => columns + .iter() + .map(|c| c.as_data_column().clone()) + .collect::>(), + Some(DataSidecars::Blobs(_)) | None => vec![], + }; + let envelope = execution_envelope.map(|envelope| AvailableEnvelope::new(envelope, columns)); + return RangeSyncBlock::new_gloas(block, envelope).unwrap(); + } + match data_sidecars { Some(DataSidecars::Blobs(blobs)) => { let block_data = AvailableBlockData::new_with_blobs(blobs.clone()); @@ -274,6 +305,14 @@ fn update_proposal_signatures( } } +fn update_envelope_block_root(snapshot: &mut BeaconSnapshot) { + if let Some(envelope) = snapshot.execution_envelope.as_ref() { + let mut envelope = envelope.as_ref().clone(); + envelope.message.beacon_block_root = snapshot.beacon_block.canonical_root(); + snapshot.execution_envelope = Some(Arc::new(envelope)); + } +} + fn update_parent_roots(snapshots: &mut [BeaconSnapshot], blobs: &mut [Option>]) { for i in 0..snapshots.len() { let root = snapshots[i].beacon_block.canonical_root(); @@ -292,6 +331,7 @@ fn update_parent_roots(snapshots: &mut [BeaconSnapshot], blobs: &mut [Option< } } child.beacon_block = new_child; + update_envelope_block_root(child); } } } @@ -359,9 +399,9 @@ fn update_data_column_signed_header( async fn chain_segment_full_segment() { let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; - store_envelopes_for_chain_segment(&chain_segment, &harness); + store_envelopes_for_chain_segment(chain_segment, &harness); let blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); @@ -385,7 +425,7 @@ async fn chain_segment_full_segment() { .into_block_error() .expect("should import chain segment"); - update_fork_choice_with_envelopes(&chain_segment, &harness); + update_fork_choice_with_envelopes(chain_segment, &harness); harness.chain.recompute_head_at_current_slot().await; assert_eq!( @@ -400,13 +440,13 @@ async fn chain_segment_varying_chunk_size() { let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); - for chunk_size in &[1, 2, 31, 32, 33] { + for chunk_size in &[1, 32, 33] { let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); - store_envelopes_for_chain_segment(&chain_segment, &harness); + store_envelopes_for_chain_segment(chain_segment, &harness); harness .chain @@ -422,7 +462,7 @@ async fn chain_segment_varying_chunk_size() { .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); } - update_fork_choice_with_envelopes(&chain_segment, &harness); + update_fork_choice_with_envelopes(chain_segment, &harness); harness.chain.recompute_head_at_current_slot().await; assert_eq!( @@ -447,7 +487,7 @@ async fn chain_segment_non_linear_parent_roots() { * Test with a block removed. */ let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); blocks.remove(2); @@ -468,20 +508,25 @@ async fn chain_segment_non_linear_parent_roots() { * Test with a modified parent root. */ let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.parent_root_mut() = Hash256::zero(); - blocks[3] = RangeSyncBlock::new( - Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().clone(), - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let mutated_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + blocks[3] = if mutated_block.fork_name_unchecked().gloas_enabled() { + RangeSyncBlock::new_gloas(mutated_block, None).unwrap() + } else { + RangeSyncBlock::new( + mutated_block, + blocks[3].block_data().clone(), + &harness.chain.data_availability_checker, + harness.spec.clone(), + ) + .unwrap() + }; assert!( matches!( @@ -510,18 +555,23 @@ async fn chain_segment_non_linear_slots() { */ let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.slot_mut() = Slot::new(0); - blocks[3] = RangeSyncBlock::new( - Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().clone(), - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let mutated_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + blocks[3] = if mutated_block.fork_name_unchecked().gloas_enabled() { + RangeSyncBlock::new_gloas(mutated_block, None).unwrap() + } else { + RangeSyncBlock::new( + mutated_block, + blocks[3].block_data().clone(), + &harness.chain.data_availability_checker, + harness.spec.clone(), + ) + .unwrap() + }; assert!( matches!( @@ -540,18 +590,23 @@ async fn chain_segment_non_linear_slots() { */ let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.slot_mut() = blocks[2].slot(); - blocks[3] = RangeSyncBlock::new( - Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().clone(), - &harness.chain.data_availability_checker, - harness.chain.spec.clone(), - ) - .unwrap(); + let mutated_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + blocks[3] = if mutated_block.fork_name_unchecked().gloas_enabled() { + RangeSyncBlock::new_gloas(mutated_block, None).unwrap() + } else { + RangeSyncBlock::new( + mutated_block, + blocks[3].block_data().clone(), + &harness.chain.data_availability_checker, + harness.chain.spec.clone(), + ) + .unwrap() + }; assert!( matches!( @@ -579,7 +634,12 @@ async fn assert_invalid_signature( .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect(); @@ -618,7 +678,12 @@ async fn assert_invalid_signature( .zip(chain_segment_blobs.iter()) .filter(|(snapshot, _)| snapshot.beacon_block.slot() > finalized_slot) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect(); // We don't care if this fails, we just call this to ensure that all prior blocks have been @@ -636,6 +701,7 @@ async fn assert_invalid_signature( snapshots[block_index].beacon_block.canonical_root(), build_range_sync_block( snapshots[block_index].beacon_block.clone(), + snapshots[block_index].execution_envelope.clone(), &chain_segment_blobs[block_index], harness.chain.clone(), ), @@ -680,7 +746,7 @@ async fn invalid_signature_gossip_block() { let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { // Ensure the block will be rejected if imported on its own (without gossip checking). - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (block, _) = snapshots[block_index] .beacon_block @@ -697,7 +763,12 @@ async fn invalid_signature_gossip_block() { .take(block_index) .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect(); harness @@ -735,7 +806,7 @@ async fn invalid_signature_gossip_block() { async fn invalid_signature_block_proposal() { let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (block, _) = snapshots[block_index] .beacon_block @@ -750,7 +821,12 @@ async fn invalid_signature_block_proposal() { .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect::>(); // Ensure the block will be rejected if imported in a chain segment. @@ -772,9 +848,10 @@ async fn invalid_signature_block_proposal() { #[tokio::test] async fn invalid_signature_randao_reveal() { - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (mut block, signature) = snapshots[block_index] .beacon_block @@ -784,10 +861,11 @@ async fn invalid_signature_randao_reveal() { *block.body_mut().randao_reveal_mut() = junk_signature(); snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -800,9 +878,10 @@ async fn invalid_signature_randao_reveal() { #[tokio::test] async fn invalid_signature_proposer_slashing() { - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (mut block, signature) = snapshots[block_index] .beacon_block @@ -826,10 +905,11 @@ async fn invalid_signature_proposer_slashing() { .expect("should update proposer slashing"); snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -842,9 +922,10 @@ async fn invalid_signature_proposer_slashing() { #[tokio::test] async fn invalid_signature_attester_slashing() { - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let fork_name = harness.chain.spec.fork_name_at_slot::(Slot::new(0)); @@ -947,10 +1028,11 @@ async fn invalid_signature_attester_slashing() { } snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -963,11 +1045,12 @@ async fn invalid_signature_attester_slashing() { #[tokio::test] async fn invalid_signature_attestation() { - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); let mut checked_attestation = false; for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (mut block, signature) = snapshots[block_index] .beacon_block @@ -1012,10 +1095,11 @@ async fn invalid_signature_attestation() { if block.body().attestations_len() > 0 { snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -1035,10 +1119,11 @@ async fn invalid_signature_attestation() { #[tokio::test] async fn invalid_signature_deposit() { - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { // Note: an invalid deposit signature is permitted! - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let deposit = Deposit { proof: vec![Hash256::zero(); DEPOSIT_TREE_DEPTH + 1] @@ -1063,13 +1148,19 @@ async fn invalid_signature_deposit() { .expect("should update deposit"); snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); let blocks: Vec> = snapshots .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect(); assert!( @@ -1088,9 +1179,10 @@ async fn invalid_signature_deposit() { #[tokio::test] async fn invalid_signature_exit() { - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let epoch = snapshots[block_index].beacon_state.current_epoch(); let (mut block, signature) = snapshots[block_index] @@ -1111,10 +1203,11 @@ async fn invalid_signature_exit() { .expect("should update deposit"); snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -1135,7 +1228,8 @@ fn unwrap_err(result: Result) -> U { #[tokio::test] async fn block_gossip_verification() { let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); - let (chain_segment, chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let chain_segment_blobs = ref_blobs.clone(); let block_index = CHAIN_SEGMENT_LENGTH - 2; @@ -1303,7 +1397,7 @@ async fn block_gossip_verification() { assert!( matches!( unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), - BlockError::ParentUnknown {parent_root: p} + BlockError::ParentUnknown {parent_root: p, ..} if p == parent_root ), "should not import a block for an unknown parent" @@ -1909,6 +2003,153 @@ async fn add_altair_block_to_base_chain() { )); } +// This is a regression test for the bogus `InvalidBestNode` error which was reachable in Gloas +// networks. Previously Lighthouse would return an `InvalidBestNode` error from `get_head` in +// contradiction to the spec, which states that the justified root should be returned when no leaf +// node is viable. +// +// The chain construction in this test is contrived but not impossible: the justified block's full +// branch is what contained the evidence to justify it, but the empty branch is more weighty and +// wins out. +#[tokio::test] +async fn gloas_get_head_can_return_justified_empty_payload_branch() { + let spec = test_spec::(); + if !spec.fork_name_at_epoch(Epoch::new(0)).gloas_enabled() { + return; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone().into()) + .chain_config(ChainConfig { + archive: true, + ..ChainConfig::default() + }) + .keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec()) + .node_custody_type(NodeCustodyType::Supernode) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness + .extend_slots(E::slots_per_epoch() as usize * 3) + .await; + + let justified_checkpoint = harness.justified_checkpoint(); + assert_ne!(justified_checkpoint.epoch, Epoch::new(0)); + let justified_root = justified_checkpoint.root; + let justified_block = harness + .chain + .get_blinded_block(&justified_root) + .unwrap() + .unwrap(); + let justified_slot = justified_block.message().slot(); + let justified_state_root = justified_block.message().state_root(); + + harness.advance_slot(); + harness + .extend_chain( + E::slots_per_epoch() as usize * 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.get_current_slot(); + let current_epoch = current_slot.epoch(E::slots_per_epoch()); + assert_eq!( + harness + .chain + .canonical_head + .cached_head() + .head_payload_status(), + PayloadStatus::Full + ); + + { + let fork_choice = harness.chain.canonical_head.fork_choice_read_lock(); + assert!(fork_choice.is_payload_received(&justified_root)); + let justified_node = fork_choice.get_block(&justified_root).unwrap(); + let voting_source = justified_node + .unrealized_justified_checkpoint + .unwrap_or(justified_node.justified_checkpoint); + assert!( + voting_source.epoch + 2 < current_epoch, + "the justified node's own voting source must be stale" + ); + } + + let mut attestation_state = harness + .chain + .get_state(&justified_state_root, Some(justified_slot), true) + .unwrap() + .unwrap(); + assert!( + attestation_state + .validators() + .iter() + .all(|validator| !validator.slashed), + "reproducer must not rely on slashed validators" + ); + + let all_validators = harness.get_all_validators(); + let mut validators_with_empty_vote = [false; VALIDATOR_COUNT]; + let attestation_start_slot = (current_epoch - 1).start_slot(E::slots_per_epoch()); + let attestation_slot = current_slot - 1; + assert_eq!( + attestation_start_slot + E::slots_per_epoch() - 1, + attestation_slot + ); + + // Create two epochs worth of attestations with `payload_present=false`, all pointing at the + // justified block. This ensures it's very much the canonical head, instead of the justifying + // chain built off its `Full` branch. + for slot in (attestation_start_slot.as_u64()..current_slot.as_u64()).map(Slot::new) { + while attestation_state.slot() < slot { + per_slot_processing(&mut attestation_state, None, &spec).unwrap(); + } + attestation_state.build_caches(&spec).unwrap(); + let attestation_state_root = attestation_state.update_tree_hash_cache().unwrap(); + assert_eq!( + attestation_state.get_latest_block_root(attestation_state_root), + justified_root + ); + + let fork = spec.fork_at_epoch(slot.epoch(E::slots_per_epoch())); + let (attestations, attesters) = harness.make_attestations_with_opts( + &all_validators, + &attestation_state, + attestation_state_root, + justified_root.into(), + slot, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: Some(false), + }, + ); + + for validator_index in attesters { + validators_with_empty_vote[validator_index] = true; + } + harness.process_attestations(attestations, &attestation_state); + } + + assert!( + validators_with_empty_vote.iter().all(|attested| *attested), + "all validators should have a latest regular attestation to the justified root" + ); + + let (head_root, payload_status) = harness + .chain + .canonical_head + .fork_choice_write_lock() + .get_head(current_slot, &spec) + .expect("fork choice should return the justified root on the empty payload branch"); + + assert_eq!(head_root, justified_root); + assert_eq!(payload_status, PayloadStatus::Empty); +} + // This is a regression test for this bug: // https://github.com/sigp/lighthouse/issues/4332#issuecomment-1565092279 #[tokio::test] @@ -1956,13 +2197,19 @@ async fn import_duplicate_block_unrealized_justification() { // Create two verified variants of the block, representing the same block being processed in // parallel. let notify_execution_layer = NotifyExecutionLayer::Yes; - let range_sync_block = RangeSyncBlock::new( - block.clone(), - AvailableBlockData::NoData, - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let range_sync_block = if block.fork_name_unchecked().gloas_enabled() { + // Fine to pass no envelope as we are testing duplicate block import + // which is not related. + RangeSyncBlock::new_gloas(block.clone(), None).unwrap() + } else { + RangeSyncBlock::new( + block.clone(), + AvailableBlockData::NoData, + &harness.chain.data_availability_checker, + harness.spec.clone(), + ) + .unwrap() + }; let verified_block1 = range_sync_block .clone() .into_execution_pending_block(block_root, chain, notify_execution_layer) @@ -2037,6 +2284,111 @@ async fn import_execution_pending_block( } } +async fn make_gloas_range_sync_block_inputs() +-> Option<(Arc>, SignedExecutionPayloadEnvelope)> { + let spec = test_spec::(); + if !spec.fork_name_at_slot::(Slot::new(1)).gloas_enabled() { + return None; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.into()) + .keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec()) + .node_custody_type(NodeCustodyType::Supernode) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let ((block, _), envelope, _) = harness.make_block_with_envelope(state, slot).await; + Some((block, envelope.expect("gloas block should have envelope"))) +} + +#[tokio::test] +async fn range_sync_block_new_gloas_accepts_matching_envelope() { + let Some((block, envelope)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); + + assert!( + result.is_ok(), + "new_gloas should accept matching block/envelope, got: {:?}", + result + ); +} + +#[tokio::test] +async fn range_sync_block_new_gloas_allows_missing_envelope() { + let Some((block, _)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + let result = RangeSyncBlock::new_gloas(block, None); + + assert!( + result.is_ok(), + "new_gloas should allow None envelope, got: {:?}", + result + ); +} + +#[tokio::test] +async fn range_sync_block_new_gloas_rejects_slot_mismatch() { + let Some((block, mut envelope)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + envelope.message.payload.slot_number += 1; + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); + + assert!( + matches!(result, Err(ref err) if err.contains("SlotMismatch")), + "new_gloas should reject mismatched slot, got: {:?}", + result + ); +} + +#[tokio::test] +async fn range_sync_block_new_gloas_rejects_builder_index_mismatch() { + let Some((block, mut envelope)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + envelope.message.builder_index += 1; + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); + + assert!( + matches!(result, Err(ref err) if err.contains("BuilderIndexMismatch")), + "new_gloas should reject mismatched builder index, got: {:?}", + result + ); +} + +#[tokio::test] +async fn range_sync_block_new_gloas_rejects_block_hash_mismatch() { + let Some((block, mut envelope)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + envelope.message.payload.block_hash = ExecutionBlockHash::repeat_byte(0x22); + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); + + assert!( + matches!(result, Err(ref err) if err.contains("BlockHashMismatch")), + "new_gloas should reject mismatched block hash, got: {:?}", + result + ); +} + // Test that RpcBlock::new() rejects blocks when blob count doesn't match expected. #[tokio::test] async fn range_sync_block_construction_fails_with_wrong_blob_count() { diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index abf1fe48a6..42a78d740f 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -6,7 +6,7 @@ use beacon_chain::{ BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, - canonical_head::{CachedHead, CanonicalHead}, + canonical_head::CachedHead, test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, }; use execution_layer::{ @@ -108,10 +108,6 @@ impl InvalidPayloadRig { self.harness.chain.canonical_head.cached_head() } - fn canonical_head(&self) -> &CanonicalHead> { - &self.harness.chain.canonical_head - } - fn previous_forkchoice_update_params(&self) -> (ForkchoiceState, PayloadAttributes) { let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); let json = mock_execution_layer @@ -353,19 +349,6 @@ impl InvalidPayloadRig { .await .unwrap(); } - - fn assert_get_head_error_contains(&self, s: &str) { - match self - .harness - .chain - .canonical_head - .fork_choice_write_lock() - .get_head(self.harness.chain.slot().unwrap(), &self.harness.chain.spec) - { - Err(ForkChoiceError::ProtoArrayStringError(e)) if e.contains(s) => (), - other => panic!("expected {} error, got {:?}", s, other), - }; - } } /// Simple test of the different import types. @@ -1297,21 +1280,14 @@ impl InvalidHeadSetup { rig.invalidate_manually(invalid_head.head_block_root()) .await; - // Since our setup ensures that there is only a single, invalid block - // that's viable for head (according to FFG filtering), setting the - // head block as invalid should not result in another head being chosen. - // Rather, it should fail to run fork choice and leave the invalid block as - // the head. - assert!( - rig.canonical_head() - .head_execution_status() - .unwrap() - .is_invalid() - ); - - // Ensure that we're getting the correct error when trying to find a new - // head. - rig.assert_get_head_error_contains("InvalidBestNode"); + // Ensure the justified root is the head. This is the spec-correct choice of head when + // all leaves are ineligible. + let mut fork_choice = rig.harness.chain.canonical_head.fork_choice_write_lock(); + let head = fork_choice + .get_head(rig.harness.chain.slot().unwrap(), &rig.harness.chain.spec) + .unwrap(); + assert_eq!(head.0, fork_choice.justified_checkpoint().root); + drop(fork_choice); Self { rig, diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 0ac77dcfaa..4d392ef524 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3148,6 +3148,14 @@ async fn weak_subjectivity_sync_test( .store .put_payload_envelope(&wss_block_root, &envelope) .unwrap(); + + // `from_anchor` doesn't mark the anchor's payload received, so do it here; otherwise the + // first forward block (a FULL child of the anchor) would be rejected with `ParentUnknown`. + beacon_chain + .canonical_head + .fork_choice_write_lock() + .on_valid_payload_envelope_received(wss_block_root) + .unwrap(); } // Apply blocks forward to reach head. @@ -3296,7 +3304,8 @@ async fn weak_subjectivity_sync_test( let range_sync_block = harness .build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(full_block)); - let fully_available_block = range_sync_block.into_available_block(); + let (fully_available_block, _envelope) = + range_sync_block.into_available_block().unwrap(); harness .chain .data_availability_checker diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index af3ff09c8a..d6233ebaf9 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -41,8 +41,8 @@ pub use crate::scheduler::BeaconProcessorQueueLengths; use crate::scheduler::work_queue::WorkQueues; use crate::work_reprocessing_queue::{ - QueuedBackfillBatch, QueuedColumnReconstruction, QueuedGossipBlock, QueuedGossipEnvelope, - ReprocessQueueMessage, + QueuedBackfillBatch, QueuedColumnReconstruction, QueuedGossipBlock, QueuedGossipDataColumn, + QueuedGossipEnvelope, ReprocessQueueMessage, }; use futures::stream::{Stream, StreamExt}; use futures::task::Poll; @@ -304,6 +304,10 @@ impl From for WorkEvent { work: Work::ColumnReconstruction(process_fn), } } + ReadyWork::DataColumn(QueuedGossipDataColumn { process_fn, .. }) => Self { + drop_during_sync: true, + work: Work::UnknownBlockDataColumn { process_fn }, + }, } } } @@ -369,6 +373,9 @@ pub enum Work { UnknownBlockAttestation { process_fn: BlockingFn, }, + UnknownBlockDataColumn { + process_fn: BlockingFn, + }, GossipAttestationBatch { attestations: GossipAttestationBatch, process_batch: Box, @@ -464,6 +471,7 @@ pub enum WorkType { GossipAttestation, GossipAttestationToConvert, UnknownBlockAttestation, + UnknownBlockDataColumn, GossipAttestationBatch, GossipAggregate, UnknownBlockAggregate, @@ -569,6 +577,7 @@ impl Work { Work::LightClientFinalityUpdateRequest(_) => WorkType::LightClientFinalityUpdateRequest, Work::LightClientUpdatesByRangeRequest(_) => WorkType::LightClientUpdatesByRangeRequest, Work::UnknownBlockAttestation { .. } => WorkType::UnknownBlockAttestation, + Work::UnknownBlockDataColumn { .. } => WorkType::UnknownBlockDataColumn, Work::UnknownBlockAggregate { .. } => WorkType::UnknownBlockAggregate, Work::UnknownLightClientOptimisticUpdate { .. } => { WorkType::UnknownLightClientOptimisticUpdate @@ -842,6 +851,9 @@ impl BeaconProcessor { Some(item) } else if let Some(item) = work_queues.gossip_data_column_queue.pop() { Some(item) + } else if let Some(item) = work_queues.unknown_block_data_column_queue.pop() + { + Some(item) } else if let Some(item) = work_queues.gossip_partial_data_column_queue.pop() { @@ -1238,6 +1250,9 @@ impl BeaconProcessor { Work::UnknownBlockAttestation { .. } => { work_queues.unknown_block_attestation_queue.push(work) } + Work::UnknownBlockDataColumn { .. } => work_queues + .unknown_block_data_column_queue + .push(work, work_id), Work::UnknownBlockAggregate { .. } => { work_queues.unknown_block_aggregate_queue.push(work) } @@ -1288,6 +1303,9 @@ impl BeaconProcessor { WorkType::UnknownBlockAttestation => { work_queues.unknown_block_attestation_queue.len() } + WorkType::UnknownBlockDataColumn => { + work_queues.unknown_block_data_column_queue.len() + } WorkType::GossipAttestationBatch => 0, // No queue WorkType::GossipAggregate => work_queues.aggregate_queue.len(), WorkType::UnknownBlockAggregate => { @@ -1504,6 +1522,7 @@ impl BeaconProcessor { }), Work::UnknownBlockAttestation { process_fn } | Work::UnknownBlockAggregate { process_fn } + | Work::UnknownBlockDataColumn { process_fn } | Work::UnknownLightClientOptimisticUpdate { process_fn, .. } => { task_spawner.spawn_blocking(process_fn) } diff --git a/beacon_node/beacon_processor/src/scheduler/work_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_queue.rs index ebd66e743d..cc03feac51 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_queue.rs @@ -111,6 +111,7 @@ pub struct BeaconProcessorQueueLengths { attestation_queue: usize, unknown_block_aggregate_queue: usize, unknown_block_attestation_queue: usize, + unknown_block_data_column_queue: usize, sync_message_queue: usize, sync_contribution_queue: usize, gossip_voluntary_exit_queue: usize, @@ -174,6 +175,8 @@ impl BeaconProcessorQueueLengths { Ok(Self { aggregate_queue: 4096, unknown_block_aggregate_queue: 1024, + // Capacity for two slot's worth of data columns for a supernode. + unknown_block_data_column_queue: 256, // Capacity for a full slot's worth of attestations if subscribed to all subnets attestation_queue: std::cmp::max( active_validator_count / slots_per_epoch, @@ -245,6 +248,7 @@ pub struct WorkQueues { pub attestation_debounce: TimeLatch, pub unknown_block_aggregate_queue: LifoQueue>, pub unknown_block_attestation_queue: LifoQueue>, + pub unknown_block_data_column_queue: FifoQueue>, pub sync_message_queue: LifoQueue>, pub sync_contribution_queue: LifoQueue>, pub gossip_voluntary_exit_queue: FifoQueue>, @@ -302,6 +306,8 @@ impl WorkQueues { LifoQueue::new(queue_lengths.unknown_block_aggregate_queue); let unknown_block_attestation_queue = LifoQueue::new(queue_lengths.unknown_block_attestation_queue); + let unknown_block_data_column_queue = + FifoQueue::new(queue_lengths.unknown_block_data_column_queue); let sync_message_queue = LifoQueue::new(queue_lengths.sync_message_queue); let sync_contribution_queue = LifoQueue::new(queue_lengths.sync_contribution_queue); @@ -383,6 +389,7 @@ impl WorkQueues { attestation_debounce, unknown_block_aggregate_queue, unknown_block_attestation_queue, + unknown_block_data_column_queue, sync_message_queue, sync_contribution_queue, gossip_voluntary_exit_queue, diff --git a/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs index b1fa56af01..62ed86fbad 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs @@ -52,6 +52,10 @@ pub const QUEUED_ATTESTATION_DELAY: Duration = Duration::from_secs(12); /// For how long to queue light client updates for re-processing. pub const QUEUED_LIGHT_CLIENT_UPDATE_DELAY: Duration = Duration::from_secs(12); +/// Data column timeout as a multiplier of slot duration. Columns waiting for their block will be +/// sent for processing after this many slots worth of time, even if the block hasn't arrived. +const QUEUED_DATA_COLUMN_DELAY_SLOTS: u32 = 1; + /// Envelope timeout as a multiplier of slot duration. Envelopes waiting for their block will be /// sent for processing after this many slots worth of time, even if the block hasn't arrived. const QUEUED_ENVELOPE_DELAY_SLOTS: u32 = 1; @@ -76,6 +80,9 @@ const MAXIMUM_QUEUED_ENVELOPES: usize = 16; /// How many attestations we keep before new ones get dropped. const MAXIMUM_QUEUED_ATTESTATIONS: usize = 16_384; +/// How many columns we keep before new ones get dropped. +const MAXIMUM_QUEUED_DATA_COLUMNS: usize = 256; + /// How many light client updates we keep before new ones get dropped. const MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES: usize = 128; @@ -123,6 +130,8 @@ pub enum ReprocessQueueMessage { UnknownLightClientOptimisticUpdate(QueuedLightClientUpdate), /// A new backfill batch that needs to be scheduled for processing. BackfillSync(QueuedBackfillBatch), + /// A gossip data column that references an unknown block. + UnknownBlockDataColumn(QueuedGossipDataColumn), /// A delayed column reconstruction that needs checking DelayColumnReconstruction(QueuedColumnReconstruction), } @@ -138,6 +147,7 @@ pub enum ReadyWork { LightClientUpdate(QueuedLightClientUpdate), BackfillSync(QueuedBackfillBatch), ColumnReconstruction(QueuedColumnReconstruction), + DataColumn(QueuedGossipDataColumn), } /// An Attestation for which the corresponding block was not seen while processing, queued for @@ -200,6 +210,12 @@ pub struct QueuedColumnReconstruction { pub process_fn: AsyncFn, } +/// A gossip data column that references an unknown block, queued for later reprocessing. +pub struct QueuedGossipDataColumn { + pub beacon_block_root: Hash256, + pub process_fn: BlockingFn, +} + impl TryFrom> for QueuedBackfillBatch { type Error = WorkEvent; @@ -240,6 +256,8 @@ enum InboundEvent { ReadyBackfillSync(QueuedBackfillBatch), /// A column reconstruction that was queued is ready for processing. ReadyColumnReconstruction(QueuedColumnReconstruction), + /// A gossip data column that is ready for re-processing. + ReadyDataColumn(Hash256), /// A message sent to the `ReprocessQueue` Msg(ReprocessQueueMessage), } @@ -264,6 +282,8 @@ struct ReprocessQueue { lc_updates_delay_queue: DelayQueue, /// Queue to manage scheduled column reconstructions. column_reconstructions_delay_queue: DelayQueue, + /// Queue to manage gossip data column timeouts. + data_columns_delay_queue: DelayQueue, /* Queued items */ /// Queued blocks. @@ -284,6 +304,10 @@ struct ReprocessQueue { queued_column_reconstructions: HashMap>, /// Queued backfill batches queued_backfill_batches: Vec, + /// Queued gossip data columns awaiting their block, keyed by block root. + awaiting_data_columns_per_root: HashMap, DelayKey)>, + /// Total number of queued gossip data columns across all roots. + queued_data_columns_count: usize, /* Aux */ /// Next attestation id, used for both aggregated and unaggregated attestations @@ -294,6 +318,7 @@ struct ReprocessQueue { rpc_block_debounce: TimeLatch, attestation_delay_debounce: TimeLatch, lc_update_delay_debounce: TimeLatch, + data_column_delay_debounce: TimeLatch, next_backfill_batch_event: Option>>, slot_clock: Arc, } @@ -387,6 +412,13 @@ impl Stream for ReprocessQueue { Poll::Ready(None) | Poll::Pending => (), } + match self.data_columns_delay_queue.poll_expired(cx) { + Poll::Ready(Some(block_root)) => { + return Poll::Ready(Some(InboundEvent::ReadyDataColumn(block_root.into_inner()))); + } + Poll::Ready(None) | Poll::Pending => (), + } + if let Some(next_backfill_batch_event) = self.next_backfill_batch_event.as_mut() { match next_backfill_batch_event.as_mut().poll(cx) { Poll::Ready(_) => { @@ -455,6 +487,7 @@ impl ReprocessQueue { attestations_delay_queue: DelayQueue::new(), lc_updates_delay_queue: DelayQueue::new(), column_reconstructions_delay_queue: DelayQueue::new(), + data_columns_delay_queue: DelayQueue::new(), queued_gossip_block_roots: HashSet::new(), awaiting_envelopes_per_root: HashMap::new(), queued_lc_updates: FnvHashMap::default(), @@ -464,6 +497,8 @@ impl ReprocessQueue { awaiting_lc_updates_per_parent_root: HashMap::new(), queued_backfill_batches: Vec::new(), queued_column_reconstructions: HashMap::new(), + awaiting_data_columns_per_root: HashMap::new(), + queued_data_columns_count: 0, next_attestation: 0, next_lc_update: 0, early_block_debounce: TimeLatch::default(), @@ -471,6 +506,7 @@ impl ReprocessQueue { rpc_block_debounce: TimeLatch::default(), attestation_delay_debounce: TimeLatch::default(), lc_update_delay_debounce: TimeLatch::default(), + data_column_delay_debounce: TimeLatch::default(), next_backfill_batch_event: None, slot_clock, } @@ -551,22 +587,16 @@ impl ReprocessQueue { return; } - // When the queue is full, evict the oldest entry to make room for newer envelopes. + // When the queue is full, drop the new envelope. if self.awaiting_envelopes_per_root.len() >= MAXIMUM_QUEUED_ENVELOPES { if self.envelope_delay_debounce.elapsed() { warn!( queue_size = MAXIMUM_QUEUED_ENVELOPES, msg = "system resources may be saturated", - "Envelope delay queue is full, evicting oldest entry" + "Envelope delay queue is full, dropping envelope" ); } - if let Some(oldest_root) = - self.awaiting_envelopes_per_root.keys().next().copied() - && let Some((_envelope, delay_key)) = - self.awaiting_envelopes_per_root.remove(&oldest_root) - { - self.envelope_delay_queue.remove(&delay_key); - } + return; } // Register the timeout. @@ -688,6 +718,37 @@ impl ReprocessQueue { self.next_attestation += 1; } + InboundEvent::Msg(UnknownBlockDataColumn(queued_data_column)) => { + let block_root = queued_data_column.beacon_block_root; + + if self.queued_data_columns_count >= MAXIMUM_QUEUED_DATA_COLUMNS { + if self.data_column_delay_debounce.elapsed() { + warn!( + queue_size = MAXIMUM_QUEUED_DATA_COLUMNS, + msg = "system resources may be saturated", + "Data column delay queue is full, dropping column" + ); + } + return; + } + + if let Some((columns, _delay_key)) = + self.awaiting_data_columns_per_root.get_mut(&block_root) + { + // Append to existing entry; the timer for this root is already running. + columns.push(queued_data_column); + } else { + let delay_key = self.data_columns_delay_queue.insert( + block_root, + self.slot_clock.slot_duration() * QUEUED_DATA_COLUMN_DELAY_SLOTS, + ); + + self.awaiting_data_columns_per_root + .insert(block_root, (vec![queued_data_column], delay_key)); + } + + self.queued_data_columns_count += 1; + } InboundEvent::Msg(UnknownLightClientOptimisticUpdate( queued_light_client_optimistic_update, )) => { @@ -800,6 +861,25 @@ impl ReprocessQueue { ); } } + + // Unqueue the data columns we have for this root, if any. + if let Some((data_columns, delay_key)) = + self.awaiting_data_columns_per_root.remove(&block_root) + { + self.data_columns_delay_queue.remove(&delay_key); + self.queued_data_columns_count = self + .queued_data_columns_count + .saturating_sub(data_columns.len()); + for data_column in data_columns { + if self + .ready_work_tx + .try_send(ReadyWork::DataColumn(data_column)) + .is_err() + { + error!(?block_root, "Failed to send data column for reprocessing"); + } + } + } } InboundEvent::Msg(NewLightClientOptimisticUpdate { parent_root }) => { // Unqueue the light client optimistic updates we have for this root, if any. @@ -1053,6 +1133,27 @@ impl ReprocessQueue { ); } } + InboundEvent::ReadyDataColumn(block_root) => { + if let Some((data_columns, _)) = + self.awaiting_data_columns_per_root.remove(&block_root) + { + self.queued_data_columns_count = self + .queued_data_columns_count + .saturating_sub(data_columns.len()); + for data_column in data_columns { + if self + .ready_work_tx + .try_send(ReadyWork::DataColumn(data_column)) + .is_err() + { + error!( + hint = "system may be overloaded", + "Ignored expired gossip data column" + ); + } + } + } + } } metrics::set_gauge_vec( @@ -1581,48 +1682,87 @@ mod tests { assert_eq!(queue.envelope_delay_queue.len(), 1); } + /// Tests that a queued gossip data column is released when its block is imported. #[tokio::test] - async fn envelope_capacity_evicts_oldest() { + async fn data_column_released_on_block_imported() { + create_test_tracing_subscriber(); + + let config = BeaconProcessorConfig::default(); + let (ready_work_tx, mut ready_work_rx) = + mpsc::channel::(config.max_scheduled_work_queue_len); + let (_, reprocess_work_rx) = + mpsc::channel::(config.max_scheduled_work_queue_len); + let slot_clock = Arc::new(testing_slot_clock(12)); + let mut queue = ReprocessQueue::new(ready_work_tx, reprocess_work_rx, slot_clock); + + tokio::time::pause(); + + let beacon_block_root = Hash256::repeat_byte(0xbb); + + let msg = ReprocessQueueMessage::UnknownBlockDataColumn(QueuedGossipDataColumn { + beacon_block_root, + process_fn: Box::new(|| {}), + }); + queue.handle_message(InboundEvent::Msg(msg)); + + assert_eq!(queue.awaiting_data_columns_per_root.len(), 1); + assert!( + queue + .awaiting_data_columns_per_root + .contains_key(&beacon_block_root) + ); + assert_eq!(queue.data_columns_delay_queue.len(), 1); + + // Simulate block import. + queue.handle_message(InboundEvent::Msg(ReprocessQueueMessage::BlockImported { + block_root: beacon_block_root, + parent_root: Hash256::repeat_byte(0x00), + })); + + // Internal state should be cleaned up. + assert!(queue.awaiting_data_columns_per_root.is_empty()); + assert_eq!(queue.data_columns_delay_queue.len(), 0); + + // The column should have been sent to the ready_work channel. + let ready = ready_work_rx.try_recv().expect("column should be ready"); + assert!(matches!(ready, ReadyWork::DataColumn(_))); + } + + /// Tests that an expired gossip data column is pruned cleanly from all internal state. + #[tokio::test] + async fn prune_awaiting_data_columns_per_root() { create_test_tracing_subscriber(); let mut queue = test_queue(); - // Pause time so it only advances manually tokio::time::pause(); - // Fill the queue to capacity. - for i in 0..MAXIMUM_QUEUED_ENVELOPES { - let block_root = Hash256::repeat_byte(i as u8); - let msg = ReprocessQueueMessage::UnknownBlockForEnvelope(QueuedGossipEnvelope { - beacon_block_slot: Slot::new(1), - beacon_block_root: block_root, - process_fn: Box::pin(async {}), - }); - queue.handle_message(InboundEvent::Msg(msg)); - } - assert_eq!( - queue.awaiting_envelopes_per_root.len(), - MAXIMUM_QUEUED_ENVELOPES - ); + let beacon_block_root = Hash256::repeat_byte(0xcd); - // One more should evict the oldest and insert the new one. - let overflow_root = Hash256::repeat_byte(0xff); - let msg = ReprocessQueueMessage::UnknownBlockForEnvelope(QueuedGossipEnvelope { - beacon_block_slot: Slot::new(1), - beacon_block_root: overflow_root, - process_fn: Box::pin(async {}), + let msg = ReprocessQueueMessage::UnknownBlockDataColumn(QueuedGossipDataColumn { + beacon_block_root, + process_fn: Box::new(|| {}), }); queue.handle_message(InboundEvent::Msg(msg)); - // Queue should still be at capacity, with the new root present. - assert_eq!( - queue.awaiting_envelopes_per_root.len(), - MAXIMUM_QUEUED_ENVELOPES - ); + assert_eq!(queue.awaiting_data_columns_per_root.len(), 1); assert!( queue - .awaiting_envelopes_per_root - .contains_key(&overflow_root) + .awaiting_data_columns_per_root + .contains_key(&beacon_block_root) ); + + // Advance time past the delay so the entry expires. + advance_time( + &queue.slot_clock, + 2 * queue.slot_clock.slot_duration() * QUEUED_DATA_COLUMN_DELAY_SLOTS, + ) + .await; + let ready_msg = queue.next().await.unwrap(); + assert!(matches!(ready_msg, InboundEvent::ReadyDataColumn(_))); + queue.handle_message(ready_msg); + + // All internal state should be cleaned up. + assert!(queue.awaiting_data_columns_per_root.is_empty()); } } diff --git a/beacon_node/http_api/src/beacon/execution_payload_bids.rs b/beacon_node/http_api/src/beacon/execution_payload_bids.rs new file mode 100644 index 0000000000..856670aa94 --- /dev/null +++ b/beacon_node/http_api/src/beacon/execution_payload_bids.rs @@ -0,0 +1,112 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter, + publish_pubsub_message, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use ssz::Decode; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{debug, warn}; +use types::SignedExecutionPayloadBid; +use warp::{Filter, Rejection, Reply, hyper::Body, hyper::Response}; + +// POST /eth/v1/beacon/execution_payload_bids (SSZ) +pub(crate) fn post_beacon_execution_payload_bids_ssz( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_bids")) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&body_bytes) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_execution_payload_bid(bid, &chain, &network_tx) + }) + }, + ) + .boxed() +} + +// POST /eth/v1/beacon/execution_payload_bids +pub(crate) fn post_beacon_execution_payload_bids( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_bids")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |bid: SignedExecutionPayloadBid, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + publish_execution_payload_bid(bid, &chain, &network_tx) + }) + }, + ) + .boxed() +} + +pub fn publish_execution_payload_bid( + bid: SignedExecutionPayloadBid, + chain: &Arc>, + network_tx: &UnboundedSender>, +) -> Result, Rejection> { + let slot = bid.slot(); + let builder_index = bid.message.builder_index; + + if !chain.spec.is_gloas_scheduled() { + return Err(warp_utils::reject::custom_bad_request( + "Execution payload bids are not supported before the Gloas fork".into(), + )); + } + + debug!( + %slot, + builder_index, + "Publishing signed execution payload bid to network" + ); + + let gossip_verified_bid = chain + .verify_payload_bid_for_gossip(Arc::new(bid)) + .map_err(|e| { + warn!(%slot, error = ?e, "Execution payload bid failed gossip verification"); + warp_utils::reject::custom_bad_request(format!("bid failed gossip verification: {e}")) + })?; + + let bid_for_gossip = gossip_verified_bid.signed_bid.as_ref().clone(); + + publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayloadBid(Box::new(bid_for_gossip)), + )?; + + Ok(warp::reply().into_response()) +} diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs similarity index 94% rename from beacon_node/http_api/src/beacon/execution_payload_envelope.rs rename to beacon_node/http_api/src/beacon/execution_payload_envelopes.rs index 2e7fe693d6..f8ab8cddc8 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs @@ -11,7 +11,6 @@ use beacon_chain::payload_envelope_verification::EnvelopeError; use beacon_chain::{BeaconChain, BeaconChainTypes, NotifyExecutionLayer}; use bytes::Bytes; use eth2::types as api_types; -use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use ssz::{Decode, Encode}; @@ -25,8 +24,8 @@ use warp::{ hyper::{Body, Response}, }; -// POST beacon/execution_payload_envelope (SSZ) -pub(crate) fn post_beacon_execution_payload_envelope_ssz( +// POST beacon/execution_payload_envelopes (SSZ) +pub(crate) fn post_beacon_execution_payload_envelopes_ssz( eth_v1: EthV1Filter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, @@ -34,12 +33,8 @@ pub(crate) fn post_beacon_execution_payload_envelope_ssz( ) -> ResponseFilter { eth_v1 .and(warp::path("beacon")) - .and(warp::path("execution_payload_envelope")) + .and(warp::path("execution_payload_envelopes")) .and(warp::path::end()) - .and(warp::header::exact( - CONTENT_TYPE_HEADER, - SSZ_CONTENT_TYPE_HEADER, - )) .and(warp::body::bytes()) .and(task_spawner_filter) .and(chain_filter) @@ -62,8 +57,8 @@ pub(crate) fn post_beacon_execution_payload_envelope_ssz( .boxed() } -// POST beacon/execution_payload_envelope -pub(crate) fn post_beacon_execution_payload_envelope( +// POST beacon/execution_payload_envelopes +pub(crate) fn post_beacon_execution_payload_envelopes( eth_v1: EthV1Filter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, @@ -71,7 +66,7 @@ pub(crate) fn post_beacon_execution_payload_envelope( ) -> ResponseFilter { eth_v1 .and(warp::path("beacon")) - .and(warp::path("execution_payload_envelope")) + .and(warp::path("execution_payload_envelopes")) .and(warp::path::end()) .and(warp::body::json()) .and(task_spawner_filter.clone()) @@ -90,7 +85,7 @@ pub(crate) fn post_beacon_execution_payload_envelope( .boxed() } /// Publishes a signed execution payload envelope to the network. Implements -/// `POST /eth/v1/beacon/execution_payload_envelope` per the in-flight beacon-APIs PR +/// `POST /eth/v1/beacon/execution_payload_envelopes` per the in-flight beacon-APIs PR /// . pub async fn publish_execution_payload_envelope( envelope: SignedExecutionPayloadEnvelope, @@ -149,7 +144,7 @@ pub async fn publish_execution_payload_envelope( PubsubMessage::ExecutionPayload(Box::new(envelope_for_gossip)), ) .map_err(|_| { - EnvelopeError::BeaconChainError(Arc::new( + EnvelopeError::BeaconChainError(Box::new( beacon_chain::BeaconChainError::UnableToPublish, )) }) @@ -297,8 +292,8 @@ fn build_gloas_data_columns( } // TODO(gloas): add tests for this endpoint once we support importing payloads into the db -// GET beacon/execution_payload_envelope/{block_id} -pub(crate) fn get_beacon_execution_payload_envelope( +// GET beacon/execution_payload_envelopes/{block_id} +pub(crate) fn get_beacon_execution_payload_envelopes( eth_v1: EthV1Filter, block_id_or_err: impl Filter + Clone @@ -310,7 +305,7 @@ pub(crate) fn get_beacon_execution_payload_envelope( ) -> ResponseFilter { eth_v1 .and(warp::path("beacon")) - .and(warp::path("execution_payload_envelope")) + .and(warp::path("execution_payload_envelopes")) .and(block_id_or_err) .and(warp::path::end()) .and(task_spawner_filter) diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index 9ec1c476f6..31c4077540 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,3 +1,4 @@ -pub mod execution_payload_envelope; +pub mod execution_payload_bids; +pub mod execution_payload_envelopes; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index 8843541c11..dce4713245 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -533,7 +533,8 @@ mod tests { use super::*; use beacon_chain::{ PayloadVerificationStatus, - block_verification_types::{AvailableBlockData, RangeSyncBlock}, + block_verification_types::AvailableBlockData, + data_availability_checker::AvailableBlock, test_utils::{ BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, generate_data_column_sidecars_from_block, @@ -611,14 +612,13 @@ mod tests { "precondition: {fork_name:?} test block must produce data columns" ); - let available_block = RangeSyncBlock::new( + let available_block = AvailableBlock::new( block.clone(), AvailableBlockData::new_with_data_columns(data_columns), &chain.data_availability_checker, chain.spec.clone(), ) - .unwrap() - .into_available_block(); + .unwrap(); let current_slot = harness.get_current_slot(); let cached_head = chain.canonical_head.cached_head(); diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 74bf1ccd76..94f2e3f1df 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,9 +36,12 @@ mod validator_inclusion; mod validators; mod version; -use crate::beacon::execution_payload_envelope::{ - get_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope, - post_beacon_execution_payload_envelope_ssz, +use crate::beacon::execution_payload_bids::{ + post_beacon_execution_payload_bids, post_beacon_execution_payload_bids_ssz, +}; +use crate::beacon::execution_payload_envelopes::{ + get_beacon_execution_payload_envelopes, post_beacon_execution_payload_envelopes, + post_beacon_execution_payload_envelopes_ssz, }; use crate::beacon::pool::*; use crate::caches::DEFAULT_HISTORICAL_COMMITTEE_CACHE_SIZE; @@ -98,7 +101,7 @@ use types::{ BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, SignedBlindedBeaconBlock, }; -use validator::execution_payload_envelope::get_validator_execution_payload_envelope; +use validator::execution_payload_envelopes::get_validator_execution_payload_envelopes; use version::{ ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection, @@ -1539,24 +1542,40 @@ pub fn serve( network_tx_filter.clone(), ); - // POST beacon/execution_payload_envelope - let post_beacon_execution_payload_envelope = post_beacon_execution_payload_envelope( + // POST beacon/execution_payload_envelopes + let post_beacon_execution_payload_envelopes = post_beacon_execution_payload_envelopes( eth_v1.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), ); - // POST beacon/execution_payload_envelope (SSZ) - let post_beacon_execution_payload_envelope_ssz = post_beacon_execution_payload_envelope_ssz( + // POST beacon/execution_payload_envelopes (SSZ) + let post_beacon_execution_payload_envelopes_ssz = post_beacon_execution_payload_envelopes_ssz( eth_v1.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), ); - // GET beacon/execution_payload_envelope/{block_id} - let get_beacon_execution_payload_envelope = get_beacon_execution_payload_envelope( + // POST beacon/execution_payload_bids + let post_beacon_execution_payload_bids = post_beacon_execution_payload_bids( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // POST beacon/execution_payload_bids (SSZ) + let post_beacon_execution_payload_bids_ssz = post_beacon_execution_payload_bids_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // GET beacon/execution_payload_envelopes/{block_id} + let get_beacon_execution_payload_envelopes = get_beacon_execution_payload_envelopes( eth_v1.clone(), block_id_or_err, task_spawner_filter.clone(), @@ -2565,8 +2584,8 @@ pub fn serve( task_spawner_filter.clone(), ); - // GET validator/execution_payload_envelope/{slot}/{builder_index} - let get_validator_execution_payload_envelope = get_validator_execution_payload_envelope( + // GET validator/execution_payload_envelopes/{slot}/{builder_index} + let get_validator_execution_payload_envelopes = get_validator_execution_payload_envelopes( eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), @@ -3382,7 +3401,7 @@ pub fn serve( .uor(get_beacon_block_root) .uor(get_blob_sidecars) .uor(get_blobs) - .uor(get_beacon_execution_payload_envelope) + .uor(get_beacon_execution_payload_envelopes) .uor(get_beacon_pool_attestations) .uor(get_beacon_pool_attester_slashings) .uor(get_beacon_pool_proposer_slashings) @@ -3406,7 +3425,7 @@ pub fn serve( .uor(get_validator_duties_proposer) .uor(get_validator_blocks) .uor(get_validator_blinded_blocks) - .uor(get_validator_execution_payload_envelope) + .uor(get_validator_execution_payload_envelopes) .uor(get_validator_attestation_data) .uor(get_validator_payload_attestation_data) .uor(get_validator_aggregate_attestation) @@ -3444,7 +3463,8 @@ pub fn serve( .uor(post_beacon_blocks_v2_ssz) .uor(post_beacon_blinded_blocks_ssz) .uor(post_beacon_blinded_blocks_v2_ssz) - .uor(post_beacon_execution_payload_envelope_ssz) + .uor(post_beacon_execution_payload_envelopes_ssz) + .uor(post_beacon_execution_payload_bids_ssz) .uor(post_beacon_pool_payload_attestations_ssz) .uor(post_validator_proposer_preferences_ssz), ) @@ -3460,7 +3480,8 @@ pub fn serve( .uor(post_beacon_pool_payload_attestations) .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_validator_proposer_preferences) - .uor(post_beacon_execution_payload_envelope) + .uor(post_beacon_execution_payload_envelopes) + .uor(post_beacon_execution_payload_bids) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) diff --git a/beacon_node/http_api/src/validator/execution_payload_envelope.rs b/beacon_node/http_api/src/validator/execution_payload_envelopes.rs similarity index 95% rename from beacon_node/http_api/src/validator/execution_payload_envelope.rs rename to beacon_node/http_api/src/validator/execution_payload_envelopes.rs index 7a7a430414..3a20b37c9b 100644 --- a/beacon_node/http_api/src/validator/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/validator/execution_payload_envelopes.rs @@ -12,8 +12,8 @@ use types::Slot; use warp::http::Response; use warp::{Filter, Rejection}; -// GET validator/execution_payload_envelope/{slot} -pub fn get_validator_execution_payload_envelope( +// GET validator/execution_payload_envelopes/{slot} +pub fn get_validator_execution_payload_envelopes( eth_v1: EthV1Filter, chain_filter: ChainFilter, not_while_syncing_filter: NotWhileSyncingFilter, @@ -21,7 +21,7 @@ pub fn get_validator_execution_payload_envelope( ) -> ResponseFilter { eth_v1 .and(warp::path("validator")) - .and(warp::path("execution_payload_envelope")) + .and(warp::path("execution_payload_envelopes")) .and(warp::path::param::().or_else(|_| async { Err(warp_utils::reject::custom_bad_request( "Invalid slot".to_string(), diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 77df94bc36..8639914774 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -36,7 +36,7 @@ use types::{ use warp::{Filter, Rejection, Reply}; use warp_utils::reject::convert_rejection; -pub mod execution_payload_envelope; +pub mod execution_payload_envelopes; /// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator /// index and then ensures that the validator exists in the given `state`. diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 06b3a6197b..455a957337 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -48,10 +48,10 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, ProposerPreferences, - RelativeEpoch, SelectionProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, - consts::gloas::BUILDER_INDEX_SELF_BUILD, + Address, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, MainnetEthSpec, + ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -3055,6 +3055,69 @@ impl ApiTester { self } + /// Build a `SignedExecutionPayloadBid` + fn make_signed_execution_payload_bid(&self) -> (SignedExecutionPayloadBid, ForkName) { + let head = self.chain.canonical_head.cached_head(); + let slot = self.chain.slot().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + + let bid = ExecutionPayloadBid { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + }; + + let signed = SignedExecutionPayloadBid { + message: bid, + signature: bls::Signature::empty(), + }; + + (signed, fork_name) + } + + /// JSON bid with a valid structure reaches gossip verification and is rejected with 400. + pub async fn test_post_beacon_execution_payload_bids_json(self) -> Self { + let (bid, fork_name) = self.make_signed_execution_payload_bid(); + + let result = self + .client + .post_beacon_execution_payload_bids(&bid, fork_name) + .await; + + assert!( + result.is_err(), + "bid should be rejected by gossip verification" + ); + + self + } + + /// SSZ bid with a valid structure reaches gossip verification and is rejected with 400. + pub async fn test_post_beacon_execution_payload_bids_ssz(self) -> Self { + let (bid, fork_name) = self.make_signed_execution_payload_bid(); + + let result = self + .client + .post_beacon_execution_payload_bids_ssz(&bid, fork_name) + .await; + + assert!( + result.is_err(), + "bid (SSZ) should be rejected by gossip verification" + ); + + self + } + pub async fn test_get_config_fork_schedule(self) -> Self { let result = self.client.get_config_fork_schedule().await.unwrap().data; @@ -4370,7 +4433,7 @@ impl ApiTester { let envelope = self .client - .get_validator_execution_payload_envelope::(slot) + .get_validator_execution_payload_envelopes::(slot) .await .unwrap() .data; @@ -4389,7 +4452,7 @@ impl ApiTester { let signed_envelope = self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); self.client - .post_beacon_execution_payload_envelope(&signed_envelope, fork_name) + .post_beacon_execution_payload_envelopes(&signed_envelope, fork_name) .await .unwrap(); @@ -4432,7 +4495,7 @@ impl ApiTester { let envelope = self .client - .get_validator_execution_payload_envelope_ssz::(slot) + .get_validator_execution_payload_envelopes_ssz::(slot) .await .unwrap(); @@ -4450,7 +4513,7 @@ impl ApiTester { let signed_envelope = self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); self.client - .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .post_beacon_execution_payload_envelopes_ssz(&signed_envelope, fork_name) .await .unwrap(); @@ -4879,7 +4942,7 @@ impl ApiTester { // Retrieve and publish the envelope. let envelope = self .client - .get_validator_execution_payload_envelope::(slot) + .get_validator_execution_payload_envelopes::(slot) .await .unwrap() .data; @@ -4887,7 +4950,7 @@ impl ApiTester { let signed_envelope = self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); self.client - .post_beacon_execution_payload_envelope(&signed_envelope, fork_name) + .post_beacon_execution_payload_envelopes(&signed_envelope, fork_name) .await .unwrap(); @@ -4907,6 +4970,10 @@ impl ApiTester { "payload attestation should report payload_present=true after publishing \ the envelope via the HTTP API (slot {slot})" ); + assert!( + pa_data.blob_data_available, + "blob_data_available should be true once the envelope is imported (slot {slot})" + ); self.chain.slot_clock.set_slot(slot.as_u64() + 1); } @@ -4914,6 +4981,71 @@ impl ApiTester { self } + /// When a payload hasn't been seen, the payload attestation data + /// must report `payload_present = false` and `blob_data_available = false`. + pub async fn test_payload_attestation_unavailable_without_envelope(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return self; + } + + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let (sk, randao_reveal) = self + .proposer_setup(slot, epoch, &fork, genesis_validators_root) + .await; + + // Produce and publish a block, but withhold its envelope. + let (response, _metadata) = self + .client + .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None, None) + .await + .unwrap(); + let block = response.data; + let block_root = block.tree_hash_root(); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block)).unwrap(); + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + + let pa_data = self + .client + .get_validator_payload_attestation_data(slot) + .await + .unwrap() + .expect("expected payload attestation data for slot with block") + .into_data(); + + assert_eq!(pa_data.beacon_block_root, block_root); + assert!( + !pa_data.payload_present, + "payload_present should be false when the envelope is withheld (slot {slot})" + ); + assert!( + !pa_data.blob_data_available, + "blob_data_available should be false when the envelope is not imported (slot {slot})" + ); + + return self; + } + + self + } + pub async fn test_get_validator_payload_attestation_data_pre_gloas(self) -> Self { let slot = self.chain.slot().unwrap(); @@ -8640,6 +8772,14 @@ async fn payload_attestation_present_after_envelope_publish() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn payload_attestation_unavailable_without_envelope() { + ApiTester::new_with_hard_forks() + .await + .test_payload_attestation_unavailable_without_envelope() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_beacon_pool_payload_attestations_valid() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { @@ -9416,3 +9556,16 @@ async fn post_validator_proposer_preferences() { .test_post_validator_proposer_preferences_duplicate() .await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_beacon_execution_payload_bids() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + ApiTester::new_with_hard_forks() + .await + .test_post_beacon_execution_payload_bids_json() + .await + .test_post_beacon_execution_payload_bids_ssz() + .await; +} diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 44af8d7006..659886f0f1 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -21,8 +21,6 @@ ethereum_ssz_derive = { workspace = true } fixed_bytes = { workspace = true } fnv = { workspace = true } futures = { workspace = true } -# Enable partial messages feature -gossipsub = { package = "libp2p-gossipsub", git = "https://github.com/libp2p/rust-libp2p.git", features = ["partial_messages"] } hex = { workspace = true } if-addrs = "0.14" itertools = { workspace = true } diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index db42d0cfa8..8f7c1dd8de 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -125,6 +125,9 @@ pub struct Config { /// Whether light client protocols should be enabled. pub enable_light_client_server: bool, + /// Whether to enable the deprecated mplex multiplexer alongside yamux. + pub enable_mplex: bool, + /// Configuration for the outbound rate limiter (requests made by this node). pub outbound_rate_limiter_config: Option, @@ -362,6 +365,7 @@ impl Default for Config { proposer_only: false, metrics_enabled: false, enable_light_client_server: true, + enable_mplex: false, outbound_rate_limiter_config: None, invalid_block_storage: None, inbound_rate_limiter_config: None, @@ -504,7 +508,9 @@ pub fn gossipsub_config( .fanout_ttl(Duration::from_secs(60)) .history_length(12) .flood_publish(false) - .max_messages_per_rpc(Some(500)) // Responses to IWANT can be quite large + .max_publish_messages(500) // Responses to IWANT can be quite large + .max_control_messages_sent(500) + .max_control_message_size(128 << 10) // 128KB .history_gossip(load.history_gossip) .validate_messages() // require validation before propagation .validation_mode(gossipsub::ValidationMode::Anonymous) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 01a01d55ab..0735cbb37a 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -320,11 +320,12 @@ fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { && (local_enr.udp4().is_none() || local_enr.udp4() == disk_enr.udp4()) && (local_enr.udp6().is_none() || local_enr.udp6() == disk_enr.udp6()) // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY and - // PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY key to match, otherwise we use a new ENR. This will - // likely only be true for non-validating nodes. + // PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY and NEXT_FORK_DIGEST_ENR_KEY keys to match, + // otherwise we use a new ENR. This will likely only be true for non-validating nodes. && local_enr.get_decodable::(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get_decodable(ATTESTATION_BITFIELD_ENR_KEY) && local_enr.get_decodable::(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get_decodable(SYNC_COMMITTEE_BITFIELD_ENR_KEY) && local_enr.get_decodable::(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) == disk_enr.get_decodable(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) + && local_enr.get_decodable::(NEXT_FORK_DIGEST_ENR_KEY) == disk_enr.get_decodable(NEXT_FORK_DIGEST_ENR_KEY) } /// Loads enr from the given directory diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 6b5144fa6f..898b97a85f 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -2040,11 +2040,11 @@ mod tests { .peer_info_mut(&peer0) .unwrap() .set_meta_data(MetaData::V3(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer0, Subnet::Attestation(1.into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer0, + Subnet::Attestation(1.into()), + false, + ); let mut attnets = crate::types::EnrAttestationBitfield::::new(); attnets.set(10, true).unwrap(); @@ -2061,11 +2061,11 @@ mod tests { .peer_info_mut(&peer2) .unwrap() .set_meta_data(MetaData::V3(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer2, Subnet::Attestation(10.into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer2, + Subnet::Attestation(10.into()), + false, + ); let mut syncnets = crate::types::EnrSyncCommitteeBitfield::::new(); syncnets.set(3, true).unwrap(); @@ -2082,11 +2082,11 @@ mod tests { .peer_info_mut(&peer4) .unwrap() .set_meta_data(MetaData::V3(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer4, Subnet::SyncCommittee(3.into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer4, + Subnet::SyncCommittee(3.into()), + false, + ); // Perform the heartbeat. peer_manager.heartbeat(); @@ -2183,11 +2183,11 @@ mod tests { peer_info.update_sync_status(empty_synced_status()); } - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer, Subnet::DataColumn(subnet.into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer, + Subnet::DataColumn(subnet.into()), + false, + ); println!("{},{},{}", x, subnet, peer); peers.push(peer); } @@ -2304,7 +2304,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } println!("{},{}", x, peer); peers.push(peer); @@ -2408,7 +2408,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } peers.push(peer); } @@ -2507,7 +2507,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } println!("{},{}", peer_idx, peer); peers.push(peer); @@ -2679,7 +2679,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } peers.push(peer); } @@ -2746,11 +2746,11 @@ mod tests { .unwrap() .set_meta_data(MetaData::V3(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer, Subnet::Attestation((subnet as u64).into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer, + Subnet::Attestation((subnet as u64).into()), + false, + ); peers.push(peer); } @@ -2851,7 +2851,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } peers.push(peer); @@ -2937,7 +2937,7 @@ mod tests { } for subnet in peer_info.long_lived_subnets() { - peers_db.add_subscription(&peer, subnet); + peers_db.add_subscription(&peer, subnet, false); } peers.push(peer); @@ -3158,7 +3158,7 @@ mod tests { peer_info.set_custody_subnets(condition.custody_subnets.clone()); for subnet in peer_info.long_lived_subnets() { - peer_db.add_subscription(&condition.peer_id, subnet); + peer_db.add_subscription(&condition.peer_id, subnet, false); } } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 11ce785350..0a338bb011 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -714,9 +714,14 @@ impl PeerDB { /// Adds a gossipsub subscription to a peer in the peerdb. // VISIBILITY: The behaviour is able to adjust subscriptions. - pub(crate) fn add_subscription(&mut self, peer_id: &PeerId, subnet: Subnet) { + pub(crate) fn add_subscription( + &mut self, + peer_id: &PeerId, + subnet: Subnet, + supports_partials: bool, + ) { if let Some(info) = self.peers.get_mut(peer_id) { - info.insert_subnet(subnet); + info.insert_subnet(subnet, supports_partials); } } @@ -793,12 +798,39 @@ impl PeerDB { ); } - /// Updates the connection state. MUST ONLY BE USED IN TESTS. - pub fn __add_connected_peer_testing_only( + /// Adds a connected peer to the PeerDB and sets the custody subnets. + /// WARNING: This updates the connection state. MUST ONLY BE USED IN TESTS. + pub fn __add_connected_peer_with_custody_subnets( &mut self, supernode: bool, spec: &ChainSpec, enr_key: CombinedKey, + ) -> PeerId { + let peer_id = self.__add_connected_peer(supernode, enr_key, spec); + + let subnets = if supernode { + (0..spec.data_column_sidecar_subnet_count) + .map(|subnet_id| subnet_id.into()) + .collect() + } else { + let node_id = peer_id_to_node_id(&peer_id).expect("convert peer_id to node_id"); + compute_subnets_for_node::(node_id.raw(), spec.custody_requirement, spec) + .expect("should compute custody subnets") + }; + + let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); + peer_info.set_custody_subnets(subnets); + + peer_id + } + + /// Adds a connected peer to the PeerDB and updates the connection state. + /// MUST ONLY BE USED IN TESTS. + pub fn __add_connected_peer( + &mut self, + supernode: bool, + enr_key: CombinedKey, + spec: &ChainSpec, ) -> PeerId { let mut enr = Enr::builder().build(&enr_key).unwrap(); let peer_id = enr.peer_id(); @@ -835,24 +867,21 @@ impl PeerDB { }, ); - if supernode { - let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); - let all_subnets = (0..spec.data_column_sidecar_subnet_count) - .map(|subnet_id| subnet_id.into()) - .collect(); - peer_info.set_custody_subnets(all_subnets); - } else { - let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); - let node_id = peer_id_to_node_id(&peer_id).expect("convert peer_id to node_id"); - let subnets = - compute_subnets_for_node::(node_id.raw(), spec.custody_requirement, spec) - .expect("should compute custody subnets"); - peer_info.set_custody_subnets(subnets); - } - peer_id } + /// MUST ONLY BE USED IN TESTS. + pub fn __set_custody_subnets( + &mut self, + peer_id: &PeerId, + custody_subnets: HashSet, + ) -> Result<(), String> { + self.peers + .get_mut(peer_id) + .map(|info| info.set_custody_subnets(custody_subnets)) + .ok_or_else(|| "Cannot set custody subnets, peer not found".to_string()) + } + /// The connection state of the peer has been changed. Modify the peer in the db to ensure all /// variables are in sync with libp2p. /// Updating the state can lead to a `BanOperation` which needs to be processed via the peer diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index c289cb9a69..8ad7d10a88 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -41,6 +41,8 @@ pub struct PeerInfo { meta_data: Option>, /// Subnets the peer is connected to. subnets: HashSet, + /// Subnets the peer is connected to, requesting partial messages. + partial_message_subnets: HashSet, /// This is computed from either metadata or the ENR, and contains the subnets that the peer /// is *assigned* to custody, rather than *connected* to (different to `self.subnets`). /// Note: Another reason to keep this separate to `self.subnets` is an upcoming change to @@ -68,6 +70,7 @@ impl Default for PeerInfo { listening_addresses: Vec::new(), seen_multiaddrs: HashSet::new(), subnets: HashSet::new(), + partial_message_subnets: HashSet::new(), custody_subnets: HashSet::new(), sync_status: SyncStatus::Unknown, meta_data: None, @@ -428,18 +431,23 @@ impl PeerInfo { } /// Adds a known subnet for the peer. - pub(super) fn insert_subnet(&mut self, subnet: Subnet) { + pub(super) fn insert_subnet(&mut self, subnet: Subnet, supports_partials: bool) { self.subnets.insert(subnet); + if supports_partials { + self.partial_message_subnets.insert(subnet); + } } /// Removes a subnet from the peer. pub(super) fn remove_subnet(&mut self, subnet: &Subnet) { self.subnets.remove(subnet); + self.partial_message_subnets.remove(subnet); } /// Removes all subnets from the peer. pub(super) fn clear_subnets(&mut self) { - self.subnets.clear() + self.subnets.clear(); + self.partial_message_subnets.clear() } /// Applies decay rates to a non-trusted peer's score. diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 056ffc03b8..b444608468 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -726,20 +726,17 @@ pub fn rpc_data_column_limits( spec: &ChainSpec, ) -> RpcLimits { let fork_name = spec.fork_name_at_epoch(current_digest_epoch); + let max_blobs = spec.max_blobs_per_block(current_digest_epoch) as usize; if fork_name.gloas_enabled() { RpcLimits::new( DataColumnSidecarGloas::::min_size(), - DataColumnSidecarGloas::::max_size( - spec.max_blobs_per_block(current_digest_epoch) as usize - ), + DataColumnSidecarFulu::::max_size(max_blobs), ) } else { RpcLimits::new( DataColumnSidecarFulu::::min_size(), - DataColumnSidecarFulu::::max_size( - spec.max_blobs_per_block(current_digest_epoch) as usize - ), + DataColumnSidecarFulu::::max_size(max_blobs), ) } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 2429b813e9..4a8c6c55eb 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -21,8 +21,6 @@ pub struct SingleLookupReqId { pub enum SyncRequestId { /// Request searching for a block given a hash. SingleBlock { id: SingleLookupReqId }, - /// Request searching for a set of blobs given a hash. - SingleBlob { id: SingleLookupReqId }, /// Request searching for a payload envelope given a hash. SinglePayloadEnvelope { id: SingleLookupReqId }, /// Request searching for a set of data columns given a hash and list of column indices. @@ -33,6 +31,8 @@ pub enum SyncRequestId { BlobsByRange(BlobsByRangeRequestId), /// Data columns by range request DataColumnsByRange(DataColumnsByRangeRequestId), + /// Payload envelopes by range request + PayloadEnvelopesByRange(PayloadEnvelopesByRangeRequestId), } /// Request ID for data_columns_by_root requests. Block lookups do not issue this request directly. @@ -59,6 +59,12 @@ pub struct BlobsByRangeRequestId { pub parent_request_id: ComponentsByRangeRequestId, } +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct PayloadEnvelopesByRangeRequestId { + pub id: Id, + pub parent_request_id: ComponentsByRangeRequestId, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct DataColumnsByRangeRequestId { /// Id to identify this attempt at a data_columns_by_range request for `parent_request_id` @@ -261,6 +267,12 @@ macro_rules! impl_display { impl_display!(BlocksByRangeRequestId, "{}/{}", id, parent_request_id); impl_display!(BlobsByRangeRequestId, "{}/{}", id, parent_request_id); impl_display!(DataColumnsByRangeRequestId, "{}/{}", id, parent_request_id); +impl_display!( + PayloadEnvelopesByRangeRequestId, + "{}/{}", + id, + parent_request_id +); impl_display!(ComponentsByRangeRequestId, "{}/{}", id, requester); impl_display!(DataColumnsByRootRequestId, "{}/{}", id, requester); impl_display!(SingleLookupReqId, "{}/Lookup/{}", req_id, lookup_id); diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 41d937e324..862281c910 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -201,9 +201,7 @@ impl Network { // set up a collection of variables accessible outside of the network crate // Create an ENR or load from disk if appropriate - // Per [spec](https://github.com/ethereum/consensus-specs/blob/1baa05e71148b0975e28918ac6022d2256b56f4a/specs/fulu/p2p-interface.md?plain=1#L636-L637) - // `nfd` must be zero-valued when no next fork is scheduled. - let next_fork_digest = ctx.fork_context.next_fork_digest().unwrap_or_default(); + let next_fork_digest = ctx.fork_context.next_fork_digest(); let advertised_cgc = config .advertise_false_custody_group_count @@ -311,11 +309,8 @@ impl Network { let fork = ctx.chain_spec.fork_name_at_epoch(epoch); all_topics_at_fork::(fork, &ctx.chain_spec) .into_iter() - .map(|topic| { - Topic::new(GossipTopic::new(topic, GossipEncoding::default(), digest)) - .into() - }) - .collect::>() + .map(|topic| GossipTopic::new(topic, GossipEncoding::default(), digest)) + .collect::>() }) .collect::>(); @@ -368,11 +363,20 @@ impl Network { gossipsub.add_explicit_peer(&PeerId::from(explicit_peer.clone())); } + // Register topics with enabled partial messages + for topic in all_topics_for_digests.iter().flatten() { + if topic.kind().use_partial_messages(&config) { + gossipsub.enable_partials_for_topic(Topic::new(topic.clone()).hash(), true); + } + } + // If we are using metrics, then register which topics we want to make sure to keep // track of if ctx.libp2p_registry.is_some() { for topics in all_topics_for_digests { - gossipsub.register_topics_for_metrics(topics); + gossipsub.register_topics_for_metrics( + topics.into_iter().map(|t| Topic::new(t).hash()).collect(), + ); } } @@ -466,9 +470,13 @@ impl Network { } }; - // Set up the transport - tcp/quic with noise and mplex - let transport = build_transport(local_keypair.clone(), !config.disable_quic_support) - .map_err(|e| format!("Failed to build transport: {:?}", e))?; + // Set up the transport - tcp/quic with noise and yamux (mplex optional) + let transport = build_transport( + local_keypair.clone(), + !config.disable_quic_support, + config.enable_mplex, + ) + .map_err(|e| format!("Failed to build transport: {:?}", e))?; // use the executor for libp2p struct Executor(task_executor::TaskExecutor); @@ -819,18 +827,9 @@ impl Network { .write() .insert(topic.clone()); - let partial = topic - .kind() - .use_partial_messages(self.network_globals.config.as_ref()); let topic: Topic = topic.into(); - let subscribe_result = if partial { - self.gossipsub_mut().subscribe_partial(&topic, true) - } else { - self.gossipsub_mut().subscribe(&topic) - }; - - match subscribe_result { + match self.gossipsub_mut().subscribe(&topic) { Err(e) => { warn!(%topic, error = ?e, "Failed to subscribe to topic"); false @@ -1377,9 +1376,9 @@ impl Network { /* Sub-behaviour event handling functions */ /// Handle a gossipsub event. - fn inject_gs_event(&mut self, event: gossipsub::Event) -> Option> { + fn inject_gs_event(&mut self, event: Event) -> Option> { match event { - gossipsub::Event::Message { + Event::Message { propagation_source, message_id: id, message: gs_msg, @@ -1457,13 +1456,19 @@ impl Network { } } } - gossipsub::Event::Subscribed { peer_id, topic } => { + Event::Subscribed { + peer_id, + topic, + supports_partial, + .. + } => { if let Ok(topic) = GossipTopic::decode(topic.as_str()) { if let Some(subnet_id) = topic.subnet_id() { - self.network_globals - .peers - .write() - .add_subscription(&peer_id, subnet_id); + self.network_globals.peers.write().add_subscription( + &peer_id, + subnet_id, + supports_partial, + ); } // Try to send the cached messages for this topic if let Some(msgs) = self.gossip_cache.retrieve(&topic) { @@ -1509,7 +1514,7 @@ impl Network { } } } - gossipsub::Event::Unsubscribed { peer_id, topic } => { + Event::Unsubscribed { peer_id, topic } => { if let Some(subnet_id) = subnet_from_topic_hash(&topic) { self.network_globals .peers @@ -1517,7 +1522,7 @@ impl Network { .remove_subscription(&peer_id, &subnet_id); } } - gossipsub::Event::GossipsubNotSupported { peer_id } => { + Event::GossipsubNotSupported { peer_id } => { debug!(%peer_id, "Peer does not support gossipsub"); self.peer_manager_mut().report_peer( &peer_id, @@ -1527,7 +1532,7 @@ impl Network { "does_not_support_gossipsub", ); } - gossipsub::Event::SlowPeer { + Event::SlowPeer { peer_id, failed_messages, } => { diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index c7dabcb391..47629f4fd3 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -34,27 +34,39 @@ pub struct Context<'a> { type BoxedTransport = Boxed<(PeerId, StreamMuxerBox)>; /// The implementation supports TCP/IP, QUIC (experimental) over UDP, noise as the encryption layer, and -/// mplex/yamux as the multiplexing layer (when using TCP). +/// yamux as the multiplexing layer (when using TCP). Mplex can be optionally enabled. pub fn build_transport( local_private_key: Keypair, quic_support: bool, + enable_mplex: bool, ) -> std::io::Result { - // mplex config - let mut mplex_config = libp2p_mplex::Config::new(); - mplex_config.set_max_buffer_size(256); - mplex_config.set_max_buffer_behaviour(libp2p_mplex::MaxBufferBehaviour::Block); - // yamux config let yamux_config = yamux::Config::default(); + // Creates the TCP transport layer - let tcp = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) - .upgrade(core::upgrade::Version::V1) - .authenticate(generate_noise_config(&local_private_key)) - .multiplex(core::upgrade::SelectUpgrade::new( - yamux_config, - mplex_config, - )) - .timeout(Duration::from_secs(10)); + let tcp: BoxedTransport = if enable_mplex { + // Enable both yamux and mplex. + let mut mplex_config = libp2p_mplex::Config::new(); + mplex_config.set_max_num_streams(32); + mplex_config.set_max_buffer_behaviour(libp2p_mplex::MaxBufferBehaviour::ResetStream); + libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(core::upgrade::SelectUpgrade::new( + yamux_config, + mplex_config, + )) + .timeout(Duration::from_secs(10)) + .boxed() + } else { + // Yamux only + libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(yamux_config) + .timeout(Duration::from_secs(10)) + .boxed() + }; let transport = if quic_support { // Enables Quic // The default quic configuration suits us for now. diff --git a/beacon_node/lighthouse_network/src/types/partial.rs b/beacon_node/lighthouse_network/src/types/partial.rs index 26705b7106..4b5dcd8ad6 100644 --- a/beacon_node/lighthouse_network/src/types/partial.rs +++ b/beacon_node/lighthouse_network/src/types/partial.rs @@ -6,7 +6,7 @@ use ssz::{Decode, Encode}; use std::collections::HashSet; use std::fmt::Debug; use std::sync::Arc; -use tracing::{debug, error}; +use tracing::{error, trace}; use types::core::{EthSpec, Hash256}; use types::data::{ PartialDataColumn, PartialDataColumnHeader, PartialDataColumnPartsMetadata, @@ -168,7 +168,7 @@ impl Partial for OutgoingPartialColumn { Box::new(MaybeKnownMetadata::::Unknown) as Box, ) }); - debug!( + trace!( peer=%peer_id, group_id=%self.partial_column.block_root, column_index=self.partial_column.index, @@ -209,7 +209,7 @@ impl Partial for OutgoingPartialColumn { PartialError::InvalidFormat })? .map(|sidecar| { - debug!( + trace!( peer=%peer_id, group_id=%self.partial_column.block_root, column_index=self.partial_column.index, @@ -233,7 +233,7 @@ impl Partial for OutgoingPartialColumn { }); if send.is_none() { - debug!( + trace!( peer=%peer_id, group_id=%self.partial_column.block_root, column_index=self.partial_column.index, diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 043d1cfb88..d486ca5129 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -1,7 +1,7 @@ //! Handles the encoding and decoding of pubsub messages. use crate::types::{GossipEncoding, GossipKind, GossipTopic}; -use gossipsub::TopicHash; +use libp2p::gossipsub::{DataTransform, Message, RawMessage, TopicHash}; use snap::raw::{Decoder, Encoder, decompress_len}; use ssz::{Decode, Encode}; use std::io::{Error, ErrorKind}; @@ -73,12 +73,9 @@ impl SnappyTransform { } } -impl gossipsub::DataTransform for SnappyTransform { +impl DataTransform for SnappyTransform { // Provides the snappy decompression from RawGossipsubMessages - fn inbound_transform( - &self, - raw_message: gossipsub::RawMessage, - ) -> Result { + fn inbound_transform(&self, raw_message: RawMessage) -> Result { // first check the size of the compressed payload if raw_message.data.len() > self.max_compressed_len { return Err(Error::new( @@ -99,7 +96,7 @@ impl gossipsub::DataTransform for SnappyTransform { let decompressed_data = decoder.decompress_vec(&raw_message.data)?; // Build the GossipsubMessage struct - Ok(gossipsub::Message { + Ok(Message { source: raw_message.source, data: decompressed_data, sequence_number: raw_message.sequence_number, diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 65c95eff35..98c143eaeb 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -61,8 +61,8 @@ use beacon_processor::work_reprocessing_queue::QueuedColumnReconstruction; use beacon_processor::{ DuplicateCache, GossipAggregatePackage, GossipAttestationBatch, work_reprocessing_queue::{ - QueuedAggregate, QueuedGossipBlock, QueuedGossipEnvelope, QueuedLightClientUpdate, - QueuedUnaggregate, ReprocessQueueMessage, + QueuedAggregate, QueuedGossipBlock, QueuedGossipDataColumn, QueuedGossipEnvelope, + QueuedLightClientUpdate, QueuedUnaggregate, ReprocessQueueMessage, }, }; @@ -171,17 +171,6 @@ impl FailedAtt { } } -/// `MessageAcceptance` doesn't implement clone so we do a manual match here. -/// TODO: remove this once `Clone` is available on this type: -/// https://github.com/libp2p/rust-libp2p/pull/6445 -fn clone_message_acceptance(a: &MessageAcceptance) -> MessageAcceptance { - match a { - MessageAcceptance::Accept => MessageAcceptance::Accept, - MessageAcceptance::Reject => MessageAcceptance::Reject, - MessageAcceptance::Ignore => MessageAcceptance::Ignore, - } -} - impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -657,6 +646,7 @@ impl NetworkBeaconProcessor { subnet_id: DataColumnSubnetId, column_sidecar: Arc>, seen_duration: Duration, + allow_reprocess: bool, ) { let slot = column_sidecar.slot(); let block_root = column_sidecar.block_root(); @@ -719,36 +709,74 @@ impl NetworkBeaconProcessor { MessageAcceptance::Accept, ); } - GossipDataColumnError::ParentUnknown { parent_root, .. } => { + GossipDataColumnError::ParentUnknown { parent_root, slot } => { debug!( action = "requesting parent", %block_root, %parent_root, "Unknown parent hash for column" ); - self.send_sync_message(SyncMessage::UnknownParentDataColumn( + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { peer_id, - column_sidecar, - )); + block_root, + parent_root, + slot, + }); } GossipDataColumnError::BlockRootUnknown { block_root: unknown_block_root, .. } => { debug!( - action = "ignoring", + action = "queuing for reprocessing", %unknown_block_root, "Unknown block root for column" ); - // TODO(gloas): wire this into proper lookup sync. Sending - // `UnknownBlockHashFromAttestation` here is a Fulu-shaped fallback that - // mixes column processing with the attestation lookup path and is not - // the right primitive for Gloas column lookups. + // Data columns are only propagated once the block has been seen for both Fulu + // and Gloas. `UnknownBlockHashFromAttestation` declares that `peer_id` has + // imported `unknown_block_root`. + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + unknown_block_root, + )); self.propagate_validation_result( - message_id, + message_id.clone(), peer_id, MessageAcceptance::Ignore, ); + + if allow_reprocess { + // Queue the column for reprocessing when the block arrives. + let processor = self.clone(); + let reprocess_msg = ReprocessQueueMessage::UnknownBlockDataColumn( + QueuedGossipDataColumn { + beacon_block_root: unknown_block_root, + process_fn: Box::new(move || { + let _ = processor.send_gossip_data_column_sidecar( + message_id, + peer_id, + subnet_id, + column_sidecar, + seen_duration, + false, // Do not reprocess this message again. + ); + }), + }, + ); + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(reprocess_msg), + }) + .is_err() + { + debug!( + %unknown_block_root, + "Failed to queue data column for reprocessing" + ); + } + } } GossipDataColumnError::InvalidVariant | GossipDataColumnError::PubkeyCacheTimeout @@ -901,14 +929,6 @@ impl NetworkBeaconProcessor { &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, processing_start_time.elapsed().as_millis() as i64, ); - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If - // importing a block results in `Imported`, notify. Do not notify of data column errors. - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); } AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { trace!( @@ -1047,7 +1067,7 @@ impl NetworkBeaconProcessor { %parent_root, "Unknown parent hash for partial column" ); - self.send_sync_message(SyncMessage::UnknownParentPartialDataColumn { + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { peer_id, block_root, parent_root, @@ -1063,10 +1083,9 @@ impl NetworkBeaconProcessor { %unknown_block_root, "Unknown block root for partial column" ); - // TODO(gloas): wire this into proper lookup sync. Sending - // `UnknownBlockHashFromAttestation` here is a Fulu-shaped fallback that - // mixes column processing with the attestation lookup path and is not - // the right primitive for Gloas column lookups. + // Data columns are only propagated once the block has been seen for both Fulu + // and Gloas. `UnknownBlockHashFromAttestation` declares that `peer_id` has + // imported `unknown_block_root`. self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( peer_id, unknown_block_root, @@ -1142,7 +1161,7 @@ impl NetworkBeaconProcessor { metrics::inc_counter( &metrics::BEACON_PROCESSOR_GOSSIP_PARTIAL_DATA_COLUMN_SIDECAR_MISSING_HEADER_TOTAL, ); - warn!( + debug!( error = ?err, %block_root, %index, @@ -1333,16 +1352,6 @@ impl NetworkBeaconProcessor { // contributing to the partial. } } - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If a - // importing a block results in `Imported`, notify. Do not notify of data column errors. - if matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))) { - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } } async fn check_reconstruction_trigger(self: &Arc, slot: Slot, block_root: &Hash256) { @@ -1622,9 +1631,14 @@ impl NetworkBeaconProcessor { crit!(error = %e, "Internal block gossip validation error. Availability check during gossip validation"); return None; } - Err(e @ BlockError::InternalError(_)) - | Err(e @ BlockError::EnvelopeBlockRootUnknown(_)) - | Err(e @ BlockError::OptimisticSyncNotSupported { .. }) => { + // This error variant cannot be reached when doing gossip block validation: a block has + // no envelope to verify, and `BlockError::EnvelopeError` is only ever produced by the + // envelope import pipeline. + Err(e @ BlockError::EnvelopeError(_)) => { + crit!(error = %e, "Internal block gossip validation error. Envelope error during gossip validation"); + return None; + } + Err(e @ BlockError::InternalError(_)) => { error!(error = %e, "Internal block gossip validation error"); return None; } @@ -1872,11 +1886,6 @@ impl NetworkBeaconProcessor { if let Err(e) = &result { self.maybe_store_invalid_block(&invalid_block_storage, block_root, &block, e); } - - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))), - }); } pub fn process_gossip_voluntary_exit( @@ -1981,11 +1990,7 @@ impl NetworkBeaconProcessor { } }; - self.propagate_validation_result( - message_id, - peer_id, - clone_message_acceptance(&validation_result), - ); + self.propagate_validation_result(message_id, peer_id, validation_result); if let Some(slashing) = verified_slashing_opt { metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL); @@ -2047,11 +2052,7 @@ impl NetworkBeaconProcessor { } }; - self.propagate_validation_result( - message_id, - peer_id, - clone_message_acceptance(&validation_result), - ); + self.propagate_validation_result(message_id, peer_id, validation_result); if let Some(slashing) = verified_slashing_opt { metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL); @@ -2719,14 +2720,10 @@ impl NetworkBeaconProcessor { if allow_reprocess { // We don't know the block, get the sync manager to handle the block lookup, and // send the attestation to be scheduled for re-processing. - self.sync_tx - .send(SyncMessage::UnknownBlockHashFromAttestation( - peer_id, - *beacon_block_root, - )) - .unwrap_or_else(|_| { - warn!(msg = "UnknownBlockHash", "Failed to send to sync service") - }); + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + *beacon_block_root, + )); let msg = match failed_att { FailedAtt::Aggregate { attestation, @@ -3716,7 +3713,11 @@ impl NetworkBeaconProcessor { EnvelopeError::PriorToFinalization { .. } | EnvelopeError::BeaconChainError(_) | EnvelopeError::BeaconStateError(_) - | EnvelopeError::ImportError(_) => { + // The following variants are produced during envelope import, not gossip + // verification, so they cannot be reached here. Ignore them to be safe. + | EnvelopeError::OptimisticSyncNotSupported { .. } + | EnvelopeError::BlockRootNotInForkChoice(_) + | EnvelopeError::InternalError(_) => { self.propagate_validation_result( message_id, peer_id, @@ -3995,13 +3996,17 @@ impl NetworkBeaconProcessor { | PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. } => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } - PayloadAttestationError::UnknownHeadBlock { .. } => { + PayloadAttestationError::UnknownHeadBlock { beacon_block_root } => { debug!( %peer_id, %message_slot, "Payload attestation references unknown block" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + *beacon_block_root, + )) } PayloadAttestationError::NotInPTC { .. } => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 97673aa8b8..a9579caaeb 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -6,6 +6,7 @@ use beacon_chain::data_column_verification::{ GossipDataColumnError, KzgVerifiedCustodyDataColumn, observe_gossip_data_column, }; use beacon_chain::fetch_blobs::{FetchEngineBlobError, fetch_and_process_engine_blobs}; +use beacon_chain::partial_data_column_assembler::AssemblyColumn; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; use beacon_processor::{ @@ -36,14 +37,13 @@ use { slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, }; -pub use sync_methods::ChainSegmentProcessId; -use types::data::FixedBlobSidecarList; +pub use sync_methods::{BlockProcessingResult, ChainSegmentProcessId}; pub type Error = TrySendError>; mod gossip_methods; mod rpc_methods; -mod sync_methods; +pub(crate) mod sync_methods; mod tests; pub(crate) const FUTURE_SLOT_TOLERANCE: u64 = 1; @@ -202,6 +202,7 @@ impl NetworkBeaconProcessor { subnet_id: DataColumnSubnetId, column_sidecar: Arc>, seen_timestamp: Duration, + allow_reprocess: bool, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = async move { @@ -212,6 +213,7 @@ impl NetworkBeaconProcessor { subnet_id, column_sidecar, seen_timestamp, + allow_reprocess, ) .await }; @@ -534,31 +536,6 @@ impl NetworkBeaconProcessor { }) } - /// Create a new `Work` event for some blobs, where the result from computation (if any) is - /// sent to the other side of `result_tx`. - pub fn send_rpc_blobs( - self: &Arc, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) -> Result<(), Error> { - let blob_count = blobs.iter().filter(|b| b.is_some()).count(); - if blob_count == 0 { - return Ok(()); - } - let process_fn = self.clone().generate_rpc_blobs_process_fn( - block_root, - blobs, - seen_timestamp, - process_type, - ); - self.try_send(BeaconWorkEvent { - drop_during_sync: false, - work: Work::RpcBlobs { process_fn }, - }) - } - /// Create a new `Work` event for an RPC-fetched payload envelope. `process_lookup_envelope` /// reports the result back to sync. pub fn send_lookup_envelope( @@ -996,14 +973,35 @@ impl NetworkBeaconProcessor { // Publish partial columns without eager send // TODO(gloas): implement publish partial columns without eager send if let Some(assembler) = self.chain.data_availability_checker.partial_assembler() { - let columns = assembler.get_partials_and_mark_as_local_fetched(block_root, &header); + let columns = assembler.get_columns_and_mark_as_local_fetched(block_root, &header); + // Republish both complete and incomplete columns as partials + let columns: Vec<_> = columns + .into_iter() + .filter_map(|column| match column { + AssemblyColumn::Incomplete(partial) => Some(partial.into_inner()), + AssemblyColumn::Complete(full) => { + let DataColumnSidecar::Fulu(fulu) = full.as_data_column() else { + return None; + }; + match fulu.to_partial() { + Ok(partial) => Some(Arc::new(partial)), + Err(err) => { + error!( + %block_root, + column_index = %full.index(), + ?err, + "Failed to convert complete column to partial for re-seeding" + ); + None + } + } + } + }) + .collect(); if !columns.is_empty() { debug!(block = %block_root, "Publishing all partials after getBlobs"); self.send_network_message(NetworkMessage::PublishPartialColumns { - columns: columns - .into_iter() - .map(|partial| partial.into_inner()) - .collect(), + columns, header, }); } else { diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index e3ba6fb3c4..e2226af094 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -3,12 +3,14 @@ use crate::network_beacon_processor::{FUTURE_SLOT_TOLERANCE, NetworkBeaconProces use crate::sync::BatchProcessResult; use crate::sync::manager::CustodyBatchProcessResult; use crate::sync::{ - ChainId, + ChainId, PeerGroup, SyncNetworkContext, manager::{BlockProcessType, SyncMessage}, }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; -use beacon_chain::data_availability_checker::AvailabilityCheckError; +use beacon_chain::data_availability_checker::{ + AvailabilityCheckError, AvailabilityCheckErrorCategory, +}; use beacon_chain::historical_data_columns::HistoricalDataColumnError; use beacon_chain::{ AvailabilityProcessingStatus, BeaconChainTypes, BlockError, ChainSegmentResult, @@ -20,15 +22,13 @@ use beacon_processor::{ }; use beacon_processor::{Work, WorkEvent}; use lighthouse_network::PeerAction; +use lighthouse_network::PeerId; use lighthouse_network::service::api_types::CustodyBackfillBatchId; use logging::crit; use std::sync::Arc; use std::time::Duration; -use store::KzgCommitment; use tracing::{debug, debug_span, error, info, instrument, warn}; -use types::data::FixedBlobSidecarList; -use types::kzg_ext::format_kzg_commitments; -use types::{BlockImportSource, DataColumnSidecarList, Epoch, Hash256}; +use types::{BlockImportSource, DataColumnSidecarList, Epoch, ExecutionBlockHash, Hash256}; /// Id associated to a batch processing request, either a sync batch or a parent lookup. #[derive(Clone, Debug, PartialEq)] @@ -90,10 +90,17 @@ impl NetworkBeaconProcessor { ); // A closure which will ignore the block. let ignore_fn = move || { + warn!( + ?process_type, + "Block processing task dropped, cpu might be overloaded" + ); // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: crate::sync::manager::BlockProcessingResult::Ignored, + result: BlockProcessingResult::Error { + penalty: None, + reason: "ignored_processor_overloaded".to_string(), + }, }); }; (process_fn, Box::new(ignore_fn)) @@ -241,114 +248,6 @@ impl NetworkBeaconProcessor { drop(handle); } - /// Returns an async closure which processes a list of blobs received via RPC. - /// - /// This separate function was required to prevent a cycle during compiler - /// type checking. - pub fn generate_rpc_blobs_process_fn( - self: Arc, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) -> AsyncFn { - let process_fn = async move { - self.clone() - .process_rpc_blobs(block_root, blobs, seen_timestamp, process_type) - .await; - }; - Box::pin(process_fn) - } - - /// Attempt to process a list of blobs received from a direct RPC request. - #[instrument( - name = "lh_process_rpc_blobs", - parent = None, - level = "debug", - skip_all, - fields(?block_root), - )] - pub async fn process_rpc_blobs( - self: Arc>, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) { - let Some(slot) = blobs - .iter() - .find_map(|blob| blob.as_ref().map(|blob| blob.slot())) - else { - return; - }; - - let (indices, commitments): (Vec, Vec) = blobs - .iter() - .filter_map(|blob_opt| { - blob_opt - .as_ref() - .map(|blob| (blob.index, blob.kzg_commitment)) - }) - .unzip(); - let commitments = format_kzg_commitments(&commitments); - - debug!( - ?indices, - %block_root, - %slot, - commitments, - "RPC blobs received" - ); - - if let Ok(current_slot) = self.chain.slot() - && current_slot == slot - { - // Note: this metric is useful to gauge how long it takes to receive blobs requested - // over rpc. Since we always send the request for block components at `get_unaggregated_attestation_due() / 2` - // we can use that as a baseline to measure against. - let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); - - metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); - } - - let result = self.chain.process_rpc_blobs(slot, block_root, blobs).await; - register_process_result_metrics(&result, metrics::BlockSource::Rpc, "blobs"); - - match &result { - Ok(AvailabilityProcessingStatus::Imported(hash)) => { - debug!( - result = "imported block and blobs", - %slot, - block_hash = %hash, - "Block components retrieved" - ); - self.chain.recompute_head_at_current_slot().await; - } - Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { - debug!( - block_hash = %block_root, - %slot, - "Missing components over rpc" - ); - } - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - block_hash = %block_root, - %slot, - "Blobs have already been imported" - ); - } - // Errors are handled and logged in `block_lookups` - Err(_) => {} - } - - // Sync handles these results - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type, - result: result.into(), - }); - } - #[instrument( name = "lh_process_rpc_custody_columns", parent = None, @@ -468,7 +367,7 @@ impl NetworkBeaconProcessor { ) .await } - Err(e) => Err(e), + Err(e) => Err(e.into()), }; // TODO(gloas): structured penalty classification arrives with the envelope lookup state @@ -783,10 +682,26 @@ impl NetworkBeaconProcessor { downloaded_blocks: Vec>, ) -> (usize, Result<(), ChainSegmentFailed>) { let total_blocks = downloaded_blocks.len(); - let available_blocks = downloaded_blocks + let available_blocks = match downloaded_blocks .into_iter() - .map(|block| block.into_available_block()) - .collect::>(); + .map(|block| { + block + .into_available_block() + .map(|(available, _envelope)| available) + }) + .collect::, _>>() + { + Ok(blocks) => blocks, + Err(e) => { + return ( + 0, + Err(ChainSegmentFailed { + peer_action: Some(PeerAction::LowToleranceError), + message: format!("Block failed availability construction: {:?}", e), + }), + ); + } + }; // TODO(gloas) when implementing backfill sync for gloas // we need a batch verify kzg function in the new da checker @@ -994,6 +909,17 @@ impl NetworkBeaconProcessor { peer_action: None, }) } + ref err @ BlockError::EnvelopeError(ref envelope_error) => { + debug!(error = ?err, "Invalid execution payload envelope"); + Err(ChainSegmentFailed { + message: format!("Invalid execution payload envelope: {err:?}"), + peer_action: if envelope_error.penalize_peer() { + Some(PeerAction::LowToleranceError) + } else { + None + }, + }) + } ref err @ BlockError::ExecutionPayloadError(ref epe) => { if !epe.penalize_peer() { // These errors indicate an issue with the EL and not the `ChainSegment`. @@ -1060,3 +986,144 @@ impl NetworkBeaconProcessor { } } } + +/// The classified outcome of submitting a block / blob / column for processing, ready for the +/// lookup state machine to act on without re-inspecting `BlockError`. +#[derive(Debug, Clone)] +pub enum BlockProcessingResult { + /// `fully_imported` is true if the lookup is complete; false if `MissingComponents` (the + /// lookup must keep fetching). `info` is a stable label for logs / metrics. + Imported(bool, &'static str), + ParentUnknown { + parent_root: Hash256, + parent_block_hash: Option, + }, + /// Processing failed. `penalty` is `Some` when an attributable peer should be downscored; + /// the third tuple element is the `report_peer` telemetry msg. `reason` is for logs only. + Error { + penalty: Option<(PeerAction, WhichPeerToPenalize, &'static str)>, + reason: String, + }, +} + +impl From> for BlockProcessingResult { + fn from(result: Result) -> Self { + fn block_peer_penalty>( + err: E, + ) -> Option<(PeerAction, WhichPeerToPenalize, &'static str)> { + Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::BlockPeer, + err.into(), + )) + } + match result { + Ok(AvailabilityProcessingStatus::Imported(_)) => Self::Imported(true, "imported"), + Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { + Self::Imported(false, "missing_components") + } + Err(e) => { + let penalty = match &e { + BlockError::DuplicateFullyImported(_) => { + return Self::Imported(true, "duplicate"); + } + BlockError::GenesisBlock => return Self::Imported(true, "genesis"), + BlockError::ParentUnknown { + parent_root, + parent_block_hash, + } => { + return Self::ParentUnknown { + parent_root: *parent_root, + parent_block_hash: *parent_block_hash, + }; + } + BlockError::BeaconChainError(_) | BlockError::InternalError(_) => None, + BlockError::DuplicateImportStatusUnknown(_) => None, + BlockError::AvailabilityCheck(inner) => match inner { + AvailabilityCheckError::InvalidColumn((Some(idx), _)) => Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::CustodyPeerForColumn(*idx), + (&e).into(), + )), + inner => match inner.category() { + AvailabilityCheckErrorCategory::Internal => None, + AvailabilityCheckErrorCategory::Malicious => block_peer_penalty(inner), + }, + }, + BlockError::ExecutionPayloadError(epe) => { + if epe.penalize_peer() { + block_peer_penalty(epe) + } else { + None + } + } + BlockError::EnvelopeError(epe) => { + if epe.penalize_peer() { + Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::BlockPeer, + (&e).into(), + )) + } else { + None + } + } + // Remaining invalid blocks: penalize the block peer. Listed explicitly so a + // new `BlockError` variant forces a compile error here. + BlockError::FutureSlot { .. } + | BlockError::StateRootMismatch { .. } + | BlockError::WouldRevertFinalizedSlot { .. } + | BlockError::NotFinalizedDescendant { .. } + | BlockError::BlockSlotLimitReached + | BlockError::IncorrectBlockProposer { .. } + | BlockError::UnknownValidator(_) + | BlockError::InvalidSignature(_) + | BlockError::BlockIsNotLaterThanParent { .. } + | BlockError::NonLinearParentRoots + | BlockError::NonLinearSlots + | BlockError::PerBlockProcessingError(_) + | BlockError::WeakSubjectivityConflict + | BlockError::InconsistentFork(_) + | BlockError::ParentExecutionPayloadInvalid { .. } + | BlockError::KnownInvalidExecutionPayload(_) + | BlockError::Slashable + | BlockError::InvalidBlobCount { .. } + | BlockError::BidParentRootMismatch { .. } => block_peer_penalty(&e), + }; + Self::Error { + penalty, + reason: format!("{e:?}"), + } + } + } + } +} + +/// Selector for which peer(s) in a `PeerGroup` to downscore. +#[derive(Debug, Clone, Copy)] +pub enum WhichPeerToPenalize { + /// All peers in the group (block peer, or all data peers). + BlockPeer, + /// Only the peer(s) that served the given column index. + CustodyPeerForColumn(u64), +} + +impl WhichPeerToPenalize { + pub fn apply( + self, + action: PeerAction, + peer_group: &PeerGroup, + msg: &'static str, + cx: &mut SyncNetworkContext, + ) { + let peers: Vec = match self { + WhichPeerToPenalize::BlockPeer => peer_group.all().copied().collect(), + WhichPeerToPenalize::CustodyPeerForColumn(idx) => { + peer_group.of_index(idx as usize).copied().collect() + } + }; + for peer in peers { + cx.report_peer(peer, action, msg); + } + } +} diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 42d3b8f33d..6b7c623230 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -6,7 +6,7 @@ use crate::{ ChainSegmentProcessId, DuplicateCache, InvalidBlockStorage, NetworkBeaconProcessor, }, service::NetworkMessage, - sync::{SyncMessage, manager::BlockProcessType}, + sync::manager::BlockProcessType, }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; @@ -41,15 +41,12 @@ use std::iter::Iterator; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; +use types::data::BlobIdentifier; use types::{ - AttesterSlashing, BlobSidecar, ChainSpec, DataColumnSidecarList, DataColumnSubnetId, Epoch, - EthSpec, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, Hash256, - MainnetEthSpec, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, -}; -use types::{ - BlobSidecarList, - data::{BlobIdentifier, FixedBlobSidecarList}, + AttesterSlashing, ChainSpec, DataColumnSidecarList, DataColumnSubnetId, Epoch, EthSpec, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec, + ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, }; type E = MainnetEthSpec; @@ -69,7 +66,6 @@ const STANDARD_TIMEOUT: Duration = Duration::from_secs(10); struct TestRig { chain: Arc>, next_block: Arc>, - next_blobs: Option>, next_data_columns: Option>, attestations: Vec<(SingleAttestation, SubnetId)>, next_block_attestations: Vec<(SingleAttestation, SubnetId)>, @@ -80,7 +76,6 @@ struct TestRig { beacon_processor_tx: BeaconProcessorSend, work_journal_rx: mpsc::Receiver<&'static str>, network_rx: mpsc::UnboundedReceiver>, - sync_rx: mpsc::UnboundedReceiver>, duplicate_cache: DuplicateCache, network_beacon_processor: Arc>, _harness: BeaconChainHarness, @@ -274,7 +269,7 @@ impl TestRig { beacon_processor_rx, } = BeaconProcessorChannels::new(&beacon_processor_config); - let (sync_tx, sync_rx) = mpsc::unbounded_channel(); + let (sync_tx, _sync_rx) = mpsc::unbounded_channel(); // Default metadata let meta_data = if spec.is_peer_das_scheduled() { @@ -341,7 +336,7 @@ impl TestRig { assert!(beacon_processor.is_ok()); let block = next_block_tuple.0; - let (blob_sidecars, data_columns) = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { + let data_columns = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { let kzg = get_kzg(&chain.spec); let epoch = block.slot().epoch(E::slots_per_epoch()); @@ -358,20 +353,17 @@ impl TestRig { .filter(|c| sampling_indices.contains(c.index())) .collect::>(); - (None, Some(custody_columns)) + Some(custody_columns) } else { - let blob_sidecars = - BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap(); - (Some(blob_sidecars), None) + None } } else { - (None, None) + None }; Self { chain, next_block: block, - next_blobs: blob_sidecars, next_data_columns: data_columns, attestations, next_block_attestations, @@ -382,7 +374,6 @@ impl TestRig { beacon_processor_tx, work_journal_rx, network_rx, - sync_rx, duplicate_cache, network_beacon_processor, _harness: harness, @@ -419,6 +410,7 @@ impl TestRig { DataColumnSubnetId::from_column_index(*data_column.index(), &self.chain.spec), data_column.clone(), Duration::from_secs(0), + true, ) .unwrap(); } @@ -448,20 +440,6 @@ impl TestRig { .unwrap(); } - pub fn enqueue_single_lookup_rpc_blobs(&self) { - if let Some(blobs) = self.next_blobs.clone() { - let blobs = FixedBlobSidecarList::new(blobs.into_iter().map(Some).collect::>()); - self.network_beacon_processor - .send_rpc_blobs( - self.next_block.canonical_root(), - blobs, - std::time::Duration::default(), - BlockProcessType::SingleBlob { id: 1 }, - ) - .unwrap(); - } - } - pub fn enqueue_single_lookup_rpc_data_columns(&self) { if let Some(data_columns) = self.next_data_columns.clone() { self.network_beacon_processor @@ -864,45 +842,6 @@ impl TestRig { Some(events) } } - - /// Listen for sync messages and collect them for a specified duration or until reaching a count. - /// - /// Returns None if no messages were received, or Some(Vec) containing the received messages. - pub async fn receive_sync_messages_with_timeout( - &mut self, - timeout: Duration, - count: Option, - ) -> Option>> { - let mut events = vec![]; - - let timeout_future = tokio::time::sleep(timeout); - tokio::pin!(timeout_future); - - loop { - // Break if we've received the requested count of messages - if let Some(target_count) = count - && events.len() >= target_count - { - break; - } - - tokio::select! { - _ = &mut timeout_future => break, - maybe_msg = self.sync_rx.recv() => { - match maybe_msg { - Some(msg) => events.push(msg), - None => break, // Channel closed - } - } - } - } - - if events.is_empty() { - None - } else { - Some(events) - } - } } fn junk_peer_id() -> PeerId { @@ -968,7 +907,10 @@ async fn data_column_reconstruction_at_slot_start() { // reconstruction deadline. #[tokio::test] async fn data_column_reconstruction_at_deadline() { - if test_spec::().fulu_fork_epoch.is_none() { + let spec = test_spec::(); + // Pre-Gloas data-column path: a Gloas block carries its columns in the payload envelope, so the + // harness produces no block-level data columns and this gossip/reconstruction flow doesn't apply. + if spec.fulu_fork_epoch.is_none() || spec.gloas_fork_epoch.is_some() { return; }; @@ -1155,7 +1097,11 @@ async fn import_gossip_block_unacceptably_early() { /// Data columns that have already been processed but unobserved should be propagated without re-importing. #[tokio::test] async fn accept_processed_gossip_data_columns_without_import() { - if test_spec::().fulu_fork_epoch.is_none() { + let spec = test_spec::(); + // Pre-Gloas data-column path: a Gloas block carries its columns in the payload envelope, so the + // harness produces no block-level data columns and this gossip flow doesn't apply. + // TODO(gloas): re-enable this test + if spec.fulu_fork_epoch.is_none() || spec.gloas_fork_epoch.is_some() { return; }; @@ -1278,7 +1224,6 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod ); // Send the block and ensure that the attestation is received back and imported. - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); let mut events = vec![]; match import_method { @@ -1293,10 +1238,6 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod BlockImportMethod::Rpc => { rig.enqueue_lookup_block(); events.push(WorkType::RpcBlock); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - events.push(WorkType::RpcBlobs); - } if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); events.push(WorkType::RpcCustodyColumn); @@ -1360,7 +1301,6 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod ); // Send the block and ensure that the attestation is received back and imported. - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); let mut events = vec![]; match import_method { @@ -1375,10 +1315,6 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod BlockImportMethod::Rpc => { rig.enqueue_lookup_block(); events.push(WorkType::RpcBlock); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - events.push(WorkType::RpcBlobs); - } if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); events.push(WorkType::RpcCustodyColumn); @@ -1565,19 +1501,13 @@ async fn import_misc_gossip_ops() { async fn test_rpc_block_reprocessing() { let mut rig = TestRig::new(SMALL_CHAIN).await; let next_block_root = rig.next_block.canonical_root(); + // Insert the next block into the duplicate cache manually let handle = rig.duplicate_cache.check_and_insert(next_block_root); rig.enqueue_single_lookup_block(); rig.assert_event_journal_completes(&[WorkType::RpcBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - rig.assert_event_journal_completes(&[WorkType::RpcBlobs]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); @@ -1898,65 +1828,6 @@ async fn test_blobs_by_root_post_fulu_should_return_empty() { assert_eq!(0, actual_count); } -/// Ensure that data column processing that results in block import sends a sync notification -#[tokio::test] -async fn test_data_column_import_notifies_sync() { - if test_spec::().fulu_fork_epoch.is_none() { - return; - } - - let mut rig = TestRig::new(SMALL_CHAIN).await; - let block_root = rig.next_block.canonical_root(); - - // Enqueue the block first to prepare for data column processing - rig.enqueue_gossip_block(); - rig.assert_event_journal_completes(&[WorkType::GossipBlock]) - .await; - rig.receive_sync_messages_with_timeout(Duration::from_millis(100), Some(1)) - .await - .expect("should receive sync message"); - - // Enqueue data columns which should trigger block import when complete - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); - if num_data_columns > 0 { - for i in 0..num_data_columns { - rig.enqueue_gossip_data_columns(i); - rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) - .await; - } - - // Verify block import succeeded - assert_eq!( - rig.head_root(), - block_root, - "block should be imported and become head" - ); - - // Check that sync was notified of the successful import - let sync_messages = rig - .receive_sync_messages_with_timeout(Duration::from_millis(100), Some(1)) - .await - .expect("should receive sync message"); - - // Verify we received the expected GossipBlockProcessResult message - assert_eq!( - sync_messages.len(), - 1, - "should receive exactly one sync message" - ); - match &sync_messages[0] { - SyncMessage::GossipBlockProcessResult { - block_root: msg_block_root, - imported, - } => { - assert_eq!(*msg_block_root, block_root, "block root should match"); - assert!(*imported, "block should be marked as imported"); - } - other => panic!("expected GossipBlockProcessResult, got {:?}", other), - } - } -} - #[tokio::test] async fn test_data_columns_by_range_request_only_returns_requested_columns() { if test_spec::().fulu_fork_epoch.is_none() { @@ -2119,6 +1990,11 @@ async fn test_payload_envelopes_by_range() { // Manually store payload envelopes for each block in the range let mut expected_roots = Vec::new(); for slot in start_slot..slot_count { + // Genesis (slot 0) has no canonical execution payload, so the by-range handler filters it + // out via `block_has_canonical_payload` even if an envelope is stored for it. + if slot == 0 { + continue; + } if let Some(root) = rig .chain .block_root_at_slot(Slot::new(slot), WhenSlotSkipped::None) @@ -2212,14 +2088,10 @@ async fn test_payload_envelopes_by_root_unknown_root_returns_empty() { let mut rig = TestRig::new(64).await; - // Request envelope for a root that has no stored envelope - let block_root = rig - .chain - .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) - .unwrap() - .unwrap(); + // Use a root with no block: the harness persists an envelope for every block it produces, so a + // real block root would already have one. An unknown root has no stored envelope. + let block_root = Hash256::repeat_byte(0xaa); - // Don't store any envelope — the handler should return 0 envelopes let roots = RuntimeVariableList::new(vec![block_root], 1).unwrap(); rig.enqueue_payload_envelopes_by_root_request(roots); diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index d2098d341e..315ec9387d 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -340,8 +340,8 @@ impl Router { Response::BlobsByRange(blob) => { self.on_blobs_by_range_response(peer_id, app_request_id, blob); } - Response::BlobsByRoot(blob) => { - self.on_blobs_by_root_response(peer_id, app_request_id, blob); + Response::BlobsByRoot(_) => { + crit!(%peer_id, "Unexpected BlobsByRoot response; lookup blob requests removed"); } Response::DataColumnsByRoot(data_column) => { self.on_data_columns_by_root_response(peer_id, app_request_id, data_column); @@ -352,10 +352,8 @@ impl Router { Response::PayloadEnvelopesByRoot(envelope) => { self.on_payload_envelopes_by_root_response(peer_id, app_request_id, envelope); } - // TODO(EIP-7732): implement outgoing payload envelopes by range responses - // once sync manager requests them. - Response::PayloadEnvelopesByRange(_) => { - debug!("Requesting envelopes by range not supported yet"); + Response::PayloadEnvelopesByRange(envelope) => { + self.on_payload_envelopes_by_range_response(peer_id, app_request_id, envelope); } // Lighthouse currently only serves BlocksByHead and does not issue it as a client, // so receiving a response is unexpected. Drop it without crashing. @@ -422,6 +420,7 @@ impl Router { subnet_id, column_sidecar, seen_timestamp, + true, ), ) } @@ -721,40 +720,6 @@ impl Router { }); } - /// Handle a `BlobsByRoot` response from the peer. - pub fn on_blobs_by_root_response( - &mut self, - peer_id: PeerId, - app_request_id: AppRequestId, - blob_sidecar: Option>>, - ) { - let sync_request_id = match app_request_id { - AppRequestId::Sync(sync_id) => match sync_id { - id @ SyncRequestId::SingleBlob { .. } => id, - other => { - crit!(request = ?other, "BlobsByRoot response on incorrect request"); - return; - } - }, - AppRequestId::Router => { - crit!(%peer_id, "All BlobsByRoot requests belong to sync"); - return; - } - AppRequestId::Internal => unreachable!("Handled internally"), - }; - - trace!( - %peer_id, - "Received BlobsByRoot Response" - ); - self.send_to_sync(SyncMessage::RpcBlob { - sync_request_id, - peer_id, - blob_sidecar, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - }); - } - /// Handle a `DataColumnsByRoot` response from the peer. pub fn on_data_columns_by_root_response( &mut self, @@ -835,6 +800,29 @@ impl Router { }); } + /// Handle a `PayloadEnvelopesByRange` response from the peer. + pub fn on_payload_envelopes_by_range_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + envelope: Option>>, + ) { + let sync_request_id = match app_request_id { + AppRequestId::Sync(id @ SyncRequestId::PayloadEnvelopesByRange { .. }) => id, + other => { + crit!(request = ?other, %peer_id, "PayloadEnvelopesByRange response on incorrect request"); + return; + } + }; + + self.send_to_sync(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope, + seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), + }); + } + fn handle_beacon_processor_send_result( &mut self, result: Result<(), crate::network_beacon_processor::Error>, diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index ce54ffc38f..c2e79fe9e8 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -883,10 +883,7 @@ impl NetworkService { fork_context.update_current_fork(*new_fork_name, new_fork_digest, current_epoch); if self.beacon_chain.spec.is_peer_das_scheduled() { - let next_fork_digest = fork_context - .next_fork_digest() - .unwrap_or_else(|| fork_context.current_fork_digest()); - self.libp2p.update_nfd(next_fork_digest); + self.libp2p.update_nfd(fork_context.next_fork_digest()); } self.libp2p.update_fork_version(new_enr_fork_id); diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 0f80138d24..c8bb17243e 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -351,6 +351,9 @@ impl BackFillSync { CouplingError::BlobPeerFailure(msg) => { debug!(?batch_id, msg, "Blob peer failure"); } + CouplingError::EnvelopePeerFailure(msg) => { + debug!(?batch_id, msg, "Envelope peer failure"); + } CouplingError::InternalError(msg) => { error!(?batch_id, msg, "Block components coupling internal error"); } @@ -1229,7 +1232,7 @@ mod tests { fn request_batches_should_not_loop_infinitely() { let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() - .deterministic_keypairs(4) + .deterministic_keypairs(8) .fresh_ephemeral_store() .build(); @@ -1247,7 +1250,7 @@ mod tests { let peer_id = network_globals .peers .write() - .__add_connected_peer_testing_only( + .__add_connected_peer_with_custody_subnets( true, &beacon_chain.spec, k256::ecdsa::SigningKey::random(&mut rng).into(), diff --git a/beacon_node/network/src/sync/batch.rs b/beacon_node/network/src/sync/batch.rs index 10af1bf503..8d40ec8b7f 100644 --- a/beacon_node/network/src/sync/batch.rs +++ b/beacon_node/network/src/sync/batch.rs @@ -34,6 +34,7 @@ pub type BatchId = Epoch; pub enum ByRangeRequestType { BlocksAndColumns, BlocksAndBlobs, + BlocksAndEnvelopesAndColumns, Blocks, Columns(HashSet), } diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs deleted file mode 100644 index edd99345b4..0000000000 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::sync::block_lookups::single_block_lookup::{ - LookupRequestError, SingleBlockLookup, SingleLookupRequestState, -}; -use crate::sync::block_lookups::{ - BlobRequestState, BlockRequestState, CustodyRequestState, PeerId, -}; -use crate::sync::manager::BlockProcessType; -use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; -use beacon_chain::BeaconChainTypes; -use lighthouse_network::service::api_types::Id; -use parking_lot::RwLock; -use std::collections::HashSet; -use std::sync::Arc; -use types::data::FixedBlobSidecarList; -use types::{DataColumnSidecarList, SignedBeaconBlock}; - -use super::SingleLookupId; -use super::single_block_lookup::{ComponentRequests, DownloadResult}; - -#[derive(Debug, Copy, Clone)] -pub enum ResponseType { - Block, - Blob, - CustodyColumn, -} - -/// This trait unifies common single block lookup functionality across blocks and blobs. This -/// includes making requests, verifying responses, and handling processing results. A -/// `SingleBlockLookup` includes both a `BlockRequestState` and a `BlobRequestState`, this trait is -/// implemented for each. -/// -/// The use of the `ResponseType` associated type gives us a degree of type -/// safety when handling a block/blob response ensuring we only mutate the correct corresponding -/// state. -pub trait RequestState { - /// The type created after validation. - type VerifiedResponseType: Clone; - - /// Request the network context to prepare a request of a component of `block_root`. If the - /// request is not necessary because the component is already known / processed, return false. - /// Return true if it sent a request and we can expect an event back from the network. - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - expected_blobs: usize, - cx: &mut SyncNetworkContext, - ) -> Result; - - /* Response handling methods */ - - /// Send the response to the beacon processor. - fn send_for_processing( - id: Id, - result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError>; - - /* Utility methods */ - - /// Returns the `ResponseType` associated with this trait implementation. Useful in logging. - fn response_type() -> ResponseType; - - /// A getter for the `BlockRequestState` or `BlobRequestState` associated with this trait. - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str>; - - /// A getter for a reference to the `SingleLookupRequestState` associated with this trait. - fn get_state(&self) -> &SingleLookupRequestState; - - /// A getter for a mutable reference to the SingleLookupRequestState associated with this trait. - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState; -} - -impl RequestState for BlockRequestState { - type VerifiedResponseType = Arc>; - - fn make_request( - &self, - id: SingleLookupId, - lookup_peers: Arc>>, - _: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.block_lookup_request(id, lookup_peers, self.requested_block_root) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: SingleLookupId, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_block_for_processing(id, block_root, value, seen_timestamp) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::Block - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - Ok(&mut request.block_request_state) - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} - -impl RequestState for BlobRequestState { - type VerifiedResponseType = FixedBlobSidecarList; - - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - expected_blobs: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.blob_lookup_request(id, lookup_peers, self.block_root, expected_blobs) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: Id, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_blobs_for_processing(id, block_root, value, seen_timestamp) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::Blob - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - match &mut request.component_requests { - ComponentRequests::WaitingForBlock => Err("waiting for block"), - ComponentRequests::ActiveBlobRequest(request, _) => Ok(request), - ComponentRequests::ActiveCustodyRequest { .. } => Err("expecting custody request"), - ComponentRequests::NotNeeded { .. } => Err("not needed"), - } - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} - -impl RequestState for CustodyRequestState { - type VerifiedResponseType = DataColumnSidecarList; - - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - _: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.custody_lookup_request(id, self.block_root, lookup_peers) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: Id, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_custody_columns_for_processing( - id, - block_root, - value, - seen_timestamp, - BlockProcessType::SingleCustodyColumn(id), - ) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::CustodyColumn - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - match &mut request.component_requests { - ComponentRequests::WaitingForBlock => Err("waiting for block"), - ComponentRequests::ActiveBlobRequest { .. } => Err("expecting blob request"), - ComponentRequests::ActiveCustodyRequest(request) => Ok(request), - ComponentRequests::NotNeeded { .. } => Err("not needed"), - } - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index ff3bf6f998..d403382e9e 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -22,32 +22,31 @@ use self::parent_chain::{NodeChain, compute_parent_chains}; pub use self::single_block_lookup::DownloadResult; -use self::single_block_lookup::{LookupRequestError, LookupResult, SingleBlockLookup}; -use super::manager::{BlockProcessType, BlockProcessingResult, SLOT_IMPORT_TOLERANCE}; -use super::network_context::{PeerGroup, RpcResponseError, SyncNetworkContext}; +use self::single_block_lookup::{LookupRequestError, PeerType, SingleBlockLookup}; +use super::manager::{BlockProcessType, SLOT_IMPORT_TOLERANCE}; +use super::network_context::{RpcResponseError, SyncNetworkContext}; use crate::metrics; +use crate::network_beacon_processor::BlockProcessingResult; use crate::sync::SyncMessage; -use crate::sync::block_lookups::common::ResponseType; use crate::sync::block_lookups::parent_chain::find_oldest_fork_ancestor; -use beacon_chain::block_verification_types::AsBlock; -use beacon_chain::data_availability_checker::{ - AvailabilityCheckError, AvailabilityCheckErrorCategory, +use crate::sync::block_lookups::single_block_lookup::{ + AwaitingParent, ImportedParent, LookupResult, }; -use beacon_chain::{AvailabilityProcessingStatus, BeaconChainTypes, BlockError}; -pub use common::RequestState; +use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; +use lighthouse_network::PeerId; use lighthouse_network::service::api_types::SingleLookupReqId; -use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; -pub use single_block_lookup::{BlobRequestState, BlockRequestState, CustodyRequestState}; use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; use store::Hash256; use tracing::{debug, error, warn}; -use types::{EthSpec, SignedBeaconBlock}; +use types::{ + DataColumnSidecarList, EthSpec, ExecutionBlockHash, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, +}; -pub mod common; pub mod parent_chain; mod single_block_lookup; @@ -77,39 +76,19 @@ const LOOKUP_MAX_DURATION_NO_PEERS_SECS: u64 = 10; /// take at most 2 GB. 200 lookups allow 3 parallel chains of depth 64 (current maximum). const MAX_LOOKUPS: usize = 200; -/// The values for `Blob`, `DataColumn` and `PartialDataColumn` is the parent root of the column. +type BlockDownloadResponse = Result>>, RpcResponseError>; +type CustodyDownloadResponse = + Result>, RpcResponseError>; +type PayloadDownloadResponse = + Result>>, RpcResponseError>; + pub enum BlockComponent { Block(DownloadResult>>), - DataColumn(DownloadResult), - PartialDataColumn(DownloadResult), -} - -impl BlockComponent { - fn parent_root(&self) -> Hash256 { - match self { - BlockComponent::Block(block) => block.value.parent_root(), - BlockComponent::DataColumn(parent_root) - | BlockComponent::PartialDataColumn(parent_root) => parent_root.value, - } - } - fn get_type(&self) -> &'static str { - match self { - BlockComponent::Block(_) => "block", - BlockComponent::DataColumn(_) => "data_column", - BlockComponent::PartialDataColumn(_) => "partial_data_column", - } - } + Sidecar, } pub type SingleLookupId = u32; -enum Action { - Retry, - ParentUnknown { parent_root: Hash256 }, - Drop(/* reason: */ String), - Continue, -} - pub struct BlockLookups { /// A cache of block roots that must be ignored for some time to prevent useless searches. For /// example if a chain is too long, its lookup chain is dropped, and range sync is expected to @@ -197,24 +176,30 @@ impl BlockLookups { &mut self, block_root: Hash256, block_component: BlockComponent, + parent_root: Hash256, + parent_block_hash: Option, peer_id: PeerId, cx: &mut SyncNetworkContext, ) -> bool { - let parent_root = block_component.parent_root(); - - let parent_lookup_exists = - self.search_parent_of_child(parent_root, block_root, &[peer_id], cx); + let parent_lookup_exists = self.search_parent_of_child( + parent_root, + &PeerType::new(parent_block_hash), + block_root, + &[peer_id], + cx, + ); // Only create the child lookup if the parent exists if parent_lookup_exists { // `search_parent_of_child` ensures that the parent lookup exists so we can safely wait for it self.new_current_lookup( block_root, Some(block_component), - Some(parent_root), - // On a `UnknownParentBlock` or `UnknownParentDataColumn` event the peer is not + Some(AwaitingParent::new(parent_root, parent_block_hash)), + // On a `UnknownParentBlock` or `UnknownParentSidecarHeader` event the peer is not // required to have the rest of the block components. Create the lookup with zero // peers to house the block components. &[], + &PeerType::Block, cx, ) } else { @@ -222,7 +207,7 @@ impl BlockLookups { } } - /// Seach a block whose parent root is unknown. + /// Search a block whose parent root is unknown. /// /// Returns true if the lookup is created or already exists #[must_use = "only reference the new lookup if returns true"] @@ -232,7 +217,7 @@ impl BlockLookups { peer_source: &[PeerId], cx: &mut SyncNetworkContext, ) -> bool { - self.new_current_lookup(block_root, None, None, peer_source, cx) + self.new_current_lookup(block_root, None, None, peer_source, &PeerType::Block, cx) } /// A block or blob triggers the search of a parent. @@ -245,6 +230,7 @@ impl BlockLookups { pub fn search_parent_of_child( &mut self, block_root_to_search: Hash256, + peer_type: &PeerType, child_block_root_trigger: Hash256, peers: &[PeerId], cx: &mut SyncNetworkContext, @@ -337,7 +323,7 @@ impl BlockLookups { } // `block_root_to_search` is a failed chain check happens inside new_current_lookup - self.new_current_lookup(block_root_to_search, None, None, peers, cx) + self.new_current_lookup(block_root_to_search, None, None, peers, peer_type, cx) } /// Searches for a single block hash. If the blocks parent is unknown, a chain of blocks is @@ -348,8 +334,9 @@ impl BlockLookups { &mut self, block_root: Hash256, block_component: Option>, - awaiting_parent: Option, + awaiting_parent: Option, peers: &[PeerId], + peer_type: &PeerType, cx: &mut SyncNetworkContext, ) -> bool { // If this block or it's parent is part of a known ignored chain, ignore it. @@ -365,17 +352,14 @@ impl BlockLookups { .find(|(_id, lookup)| lookup.is_for_block(block_root)) { if let Some(block_component) = block_component { - let component_type = block_component.get_type(); let imported = lookup.add_child_components(block_component); if !imported { - debug!( - ?block_root, - component_type, "Lookup child component ignored" - ); + debug!(?block_root, "Lookup child component ignored"); } } - if let Err(e) = self.add_peers_to_lookup_and_ancestors(lookup_id, peers, cx) { + if let Err(e) = self.add_peers_to_lookup_and_ancestors(lookup_id, peers, peer_type, cx) + { warn!(error = ?e, "Error adding peers to ancestor lookup"); } @@ -387,7 +371,7 @@ impl BlockLookups { && !self .single_block_lookups .iter() - .any(|(_, lookup)| lookup.is_for_block(awaiting_parent)) + .any(|(_, lookup)| lookup.block_root() == awaiting_parent.parent_root()) { warn!(block_root = ?awaiting_parent, "Ignoring child lookup parent lookup not found"); return false; @@ -402,7 +386,8 @@ impl BlockLookups { // If we know that this lookup has unknown parent (is awaiting a parent lookup to resolve), // signal here to hold processing downloaded data. - let mut lookup = SingleBlockLookup::new(block_root, peers, cx.next_id(), awaiting_parent); + let mut lookup = + SingleBlockLookup::new(block_root, peers, peer_type, cx.next_id(), awaiting_parent); let _guard = lookup.span.clone().entered(); // Add block components to the new request @@ -423,9 +408,7 @@ impl BlockLookups { debug!( ?peers, ?block_root, - awaiting_parent = awaiting_parent - .map(|root| root.to_string()) - .unwrap_or("none".to_owned()), + ?awaiting_parent, id = lookup.id, "Created block lookup" ); @@ -443,88 +426,50 @@ impl BlockLookups { /* Lookup responses */ - /// Process a block or blob response received from a single lookup request. - pub fn on_download_response>( + /// Process a block response received from a single lookup request. + pub fn on_block_download_response( &mut self, id: SingleLookupReqId, - response: Result<(R::VerifiedResponseType, PeerGroup, Duration), RpcResponseError>, + response: BlockDownloadResponse, cx: &mut SyncNetworkContext, ) { - let result = self.on_download_response_inner::(id, response, cx); - self.on_lookup_result(id.lookup_id, result, "download_response", cx); + let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { + debug!(?id, "Block returned for single block lookup not present"); + return; + }; + let result = lookup.on_block_download_response(id.req_id, response, cx); + self.on_lookup_result(id.lookup_id, result, "block_download_response", cx); } - /// Process a block or blob response received from a single lookup request. - pub fn on_download_response_inner>( + pub fn on_custody_download_response( &mut self, id: SingleLookupReqId, - response: Result<(R::VerifiedResponseType, PeerGroup, Duration), RpcResponseError>, + response: CustodyDownloadResponse, cx: &mut SyncNetworkContext, - ) -> Result { - // Note: no need to downscore peers here, already downscored on network context - - let response_type = R::response_type(); + ) { let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { - // We don't have the ability to cancel in-flight RPC requests. So this can happen - // if we started this RPC request, and later saw the block/blobs via gossip. - debug!(?id, "Block returned for single block lookup not present"); - return Err(LookupRequestError::UnknownLookup); + debug!(?id, "Custody returned for single block lookup not present"); + return; }; + let result = lookup.on_custody_download_response(id.req_id, response, cx); + self.on_lookup_result(id.lookup_id, result, "custody_download_response", cx); + } - let block_root = lookup.block_root(); - let request_state = R::request_state_mut(lookup) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))? - .get_state_mut(); - - match response { - Ok((response, peer_group, seen_timestamp)) => { - debug!( - ?block_root, - ?id, - ?peer_group, - ?response_type, - "Received lookup download success" - ); - - // Here we could check if response extends a parent chain beyond its max length. - // However we defer that check to the handling of a processing error ParentUnknown. - // - // Here we could check if there's already a lookup for parent_root of `response`. In - // that case we know that sending the response for processing will likely result in - // a `ParentUnknown` error. However, for simplicity we choose to not implement this - // optimization. - - // Register the download peer here. Once we have received some data over the wire we - // attribute it to this peer for scoring latter regardless of how the request was - // done. - request_state.on_download_success( - id.req_id, - DownloadResult { - value: response, - block_root, - seen_timestamp, - peer_group, - }, - )?; - // continue_request will send for processing as the request state is AwaitingProcessing - } - Err(e) => { - // No need to log peer source here. When sending a DataColumnsByRoot request we log - // the peer and the request ID which is linked to this `id` value here. - debug!( - ?block_root, - ?id, - ?response_type, - error = ?e, - "Received lookup download failure" - ); - - request_state.on_download_failure(id.req_id)?; - // continue_request will retry a download as the request state is AwaitingDownload - } - } - - lookup.continue_requests(cx) + pub fn on_payload_download_response( + &mut self, + id: SingleLookupReqId, + response: PayloadDownloadResponse, + cx: &mut SyncNetworkContext, + ) { + let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { + debug!( + ?id, + "Payload envelope returned for a lookup id that doesn't exist" + ); + return; + }; + let result = lookup.on_payload_download_response(id.req_id, response, cx); + self.on_lookup_result(id.lookup_id, result, "payload_download_response", cx); } /* Error responses */ @@ -546,254 +491,77 @@ impl BlockLookups { result: BlockProcessingResult, cx: &mut SyncNetworkContext, ) { - let lookup_result = match process_type { - BlockProcessType::SingleBlock { id } => { - self.on_processing_result_inner::>(id, result, cx) - } - BlockProcessType::SingleBlob { id } => { - self.on_processing_result_inner::>(id, result, cx) - } - BlockProcessType::SingleCustodyColumn(id) => { - self.on_processing_result_inner::>(id, result, cx) - } - // TODO(gloas): route into the payload envelope lookup state machine. - BlockProcessType::SinglePayloadEnvelope(_) => Ok(LookupResult::Pending), - }; - self.on_lookup_result(process_type.id(), lookup_result, "processing_result", cx); - } - - pub fn on_processing_result_inner>( - &mut self, - lookup_id: SingleLookupId, - result: BlockProcessingResult, - cx: &mut SyncNetworkContext, - ) -> Result { + let lookup_id = process_type.id(); let Some(lookup) = self.single_block_lookups.get_mut(&lookup_id) else { debug!(id = lookup_id, "Unknown single block lookup"); - return Err(LookupRequestError::UnknownLookup); + return; }; - let block_root = lookup.block_root(); - let request_state = R::request_state_mut(lookup) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))? - .get_state_mut(); - debug!( - component = ?R::response_type(), - ?block_root, + block_root = ?lookup.block_root(), id = lookup_id, + ?process_type, ?result, "Received lookup processing result" ); - let action = match result { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(_)) - | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) - | BlockProcessingResult::Err(BlockError::GenesisBlock) => { - // Successfully imported - request_state.on_processing_success()?; - Action::Continue - } - - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents { - .. - }) => { - // `on_processing_success` is called here to ensure the request state is updated prior to checking - // if both components have been processed. - request_state.on_processing_success()?; - - if lookup.all_components_processed() { - // We don't request for other block components until being sure that the block has - // data. If we request blobs / columns to a peer we are sure those must exist. - // Therefore if all components are processed and we still receive `MissingComponents` - // it indicates an internal bug. - return Err(LookupRequestError::MissingComponentsAfterAllProcessed); + let lookup_result = match process_type { + BlockProcessType::SingleBlock { .. } => { + // Update the result of the lookup first, here we may start the download of Gloas + // payload, which may error. + let lookup_result = lookup.on_block_processing_result(result.clone(), cx); + let lookup_is_awaiting_event = lookup.is_awaiting_event(); + let block_root = lookup.block_root(); + // Then, as a side-effect continue the EMPTY children of this lookup. Only if the + // block just imported which ensures we just do it once per lookup. + if let BlockProcessingResult::Imported(..) = result + && let Some(bid_block_hash) = lookup.peek_downloaded_bid_block_hash() + { + self.continue_child_lookups( + block_root, + ImportedParent::OnlyGloasBlock(bid_block_hash), + cx, + ); + } + // Then if this lookup happens to have only empty children we can remove it now. We + // must make sure that no other lookup is awaiting this one, and that no requests + // are on-going. + if !lookup_is_awaiting_event && !self.has_any_awaiting_children(block_root) { + Ok(LookupResult::Completed) } else { - // Continue request, potentially request blobs - Action::Retry + lookup_result } } - BlockProcessingResult::Err(BlockError::DuplicateImportStatusUnknown(..)) => { - // This is unreachable because RPC blocks do not undergo gossip verification, and - // this error can *only* come from gossip verification. - error!(?block_root, "Single block lookup hit unreachable condition"); - Action::Drop("DuplicateImportStatusUnknown".to_owned()) + BlockProcessType::SingleCustodyColumn(_) => { + lookup.on_data_processing_result(result, cx) } - BlockProcessingResult::Ignored => { - // Beacon processor signalled to ignore the block processing result. - // This implies that the cpu is overloaded. Drop the request. - warn!( - component = ?R::response_type(), - "Lookup component processing ignored, cpu might be overloaded" - ); - Action::Drop("Block processing ignored".to_owned()) - } - BlockProcessingResult::Err(e) => { - match e { - BlockError::BeaconChainError(e) => { - // Internal error - error!(%block_root, error = ?e, "Beacon chain error processing lookup component"); - Action::Drop(format!("{e:?}")) - } - BlockError::ParentUnknown { parent_root, .. } => { - // Reverts the status of this request to `AwaitingProcessing` holding the - // downloaded data. A future call to `continue_requests` will re-submit it - // once there are no pending parent requests. - // Note: `BlockError::ParentUnknown` is only returned when processing - // blocks, not blobs. - request_state.revert_to_awaiting_processing()?; - Action::ParentUnknown { parent_root } - } - ref e @ BlockError::ExecutionPayloadError(ref epe) if !epe.penalize_peer() => { - // These errors indicate that the execution layer is offline - // and failed to validate the execution payload. Do not downscore peer. - debug!( - ?block_root, - error = ?e, - "Single block lookup failed. Execution layer is offline / unsynced / misconfigured" - ); - Action::Drop(format!("{e:?}")) - } - BlockError::AvailabilityCheck(e) - if e.category() == AvailabilityCheckErrorCategory::Internal => - { - // There errors indicate internal problems and should not downscore the peer - warn!(?block_root, error = ?e, "Internal availability check failure"); - - // Here we choose *not* to call `on_processing_failure` because this could result in a bad - // lookup state transition. This error invalidates both blob and block requests, and we don't know the - // state of both requests. Blobs may have already successfullly processed for example. - // We opt to drop the lookup instead. - Action::Drop(format!("{e:?}")) - } - other => { - debug!( - ?block_root, - component = ?R::response_type(), - error = ?other, - "Invalid lookup component" - ); - let peer_group = request_state.on_processing_failure()?; - let peers_to_penalize: Vec<_> = match other { - // Note: currenlty only InvalidColumn errors have index granularity, - // but future errors may follow the same pattern. Generalize this - // pattern with https://github.com/sigp/lighthouse/pull/6321 - BlockError::AvailabilityCheck( - AvailabilityCheckError::InvalidColumn((index_opt, _)), - ) => { - match index_opt { - Some(index) => peer_group.of_index(index as usize).collect(), - // If no index supplied this is an un-attributable fault. In practice - // this should never happen. - None => vec![], - } - } - _ => peer_group.all().collect(), - }; - for peer in peers_to_penalize { - cx.report_peer( - *peer, - PeerAction::MidToleranceError, - match R::response_type() { - ResponseType::Block => "lookup_block_processing_failure", - ResponseType::Blob => "lookup_blobs_processing_failure", - ResponseType::CustodyColumn => { - "lookup_custody_column_processing_failure" - } - }, - ); - } - - Action::Retry - } - } + BlockProcessType::SinglePayloadEnvelope(_) => { + lookup.on_payload_processing_result(result, cx) } }; - - match action { - Action::Retry => { - // Trigger download for all components in case `MissingComponents` failed the blob - // request. Also if blobs are `AwaitingProcessing` and need to be progressed - lookup.continue_requests(cx) - } - Action::ParentUnknown { parent_root } => { - let peers = lookup.all_peers(); - // Mark lookup as awaiting **before** creating the parent lookup. At this point the - // lookup maybe inconsistent. - lookup.set_awaiting_parent(parent_root); - let parent_lookup_exists = - self.search_parent_of_child(parent_root, block_root, &peers, cx); - if parent_lookup_exists { - // The parent lookup exist or has been created. It's safe for `lookup` to - // reference the parent as awaiting. - debug!( - id = lookup_id, - ?block_root, - ?parent_root, - "Marking lookup as awaiting parent" - ); - Ok(LookupResult::Pending) - } else { - // The parent lookup is faulty and was not created, we must drop the `lookup` as - // it's in an inconsistent state. We must drop all of its children too. - Err(LookupRequestError::Failed(format!( - "Parent lookup is faulty {parent_root:?}" - ))) - } - } - Action::Drop(reason) => { - // Drop with noop - Err(LookupRequestError::Failed(reason)) - } - Action::Continue => { - // Drop this completed lookup only - Ok(LookupResult::Completed) - } - } + self.on_lookup_result(lookup_id, lookup_result, "processing_result", cx); } - pub fn on_external_processing_result( - &mut self, - block_root: Hash256, - imported: bool, - cx: &mut SyncNetworkContext, - ) { - let Some((id, lookup)) = self - .single_block_lookups - .iter_mut() - .find(|(_, lookup)| lookup.is_for_block(block_root)) - else { - // Ok to ignore gossip process events - return; - }; - - let lookup_result = if imported { - Ok(LookupResult::Completed) - } else { - // A lookup may be in the following state: - // - Block awaiting processing from a different source - // - Blobs downloaded processed, and inserted into the da_checker - // - // At this point the block fails processing (e.g. execution engine offline) and it is - // removed from the da_checker. Note that ALL components are removed from the da_checker - // so when we re-download and process the block we get the error - // MissingComponentsAfterAllProcessed and get stuck. - lookup.reset_requests(); - lookup.continue_requests(cx) - }; - let id = *id; - self.on_lookup_result(id, lookup_result, "external_processing_result", cx); + pub fn has_any_awaiting_children(&self, block_root: Hash256) -> bool { + self.single_block_lookups + .iter() + .any(|(_, lookup)| lookup.is_awaiting_block(block_root)) } /// Makes progress on the immediate children of `block_root` - pub fn continue_child_lookups(&mut self, block_root: Hash256, cx: &mut SyncNetworkContext) { + pub fn continue_child_lookups( + &mut self, + parent_root: Hash256, + imported_parent: ImportedParent, + cx: &mut SyncNetworkContext, + ) { let mut lookup_results = vec![]; // < need to buffer lookup results to not re-borrow &mut self for (id, lookup) in self.single_block_lookups.iter_mut() { - if lookup.awaiting_parent() == Some(block_root) { + if lookup.is_awaiting_parent(parent_root, imported_parent) { lookup.resolve_awaiting_parent(); debug!( - parent_root = ?block_root, + ?imported_parent, id, block_root = ?lookup.block_root(), "Continuing child lookup" @@ -826,7 +594,7 @@ impl BlockLookups { let child_lookups = self .single_block_lookups .iter() - .filter(|(_, lookup)| lookup.awaiting_parent() == Some(dropped_lookup.block_root())) + .filter(|(_, lookup)| lookup.is_awaiting_block(dropped_lookup.block_root())) .map(|(id, _)| *id) .collect::>(); @@ -846,7 +614,27 @@ impl BlockLookups { cx: &mut SyncNetworkContext, ) -> bool { match result { - Ok(LookupResult::Pending) => true, // no action + Ok(LookupResult::Pending) => true, + Ok(LookupResult::ParentUnknown { + parent_root, + parent_block_hash, + block_root, + peers, + }) => { + if self.search_parent_of_child( + parent_root, + &PeerType::new(parent_block_hash), + block_root, + &peers, + cx, + ) { + true + } else { + self.drop_lookup_and_children(id, "Failed"); + self.update_metrics(); + false + } + } Ok(LookupResult::Completed) => { if let Some(lookup) = self.single_block_lookups.remove(&id) { debug!( @@ -857,16 +645,17 @@ impl BlockLookups { metrics::inc_counter(&metrics::SYNC_LOOKUP_COMPLETED); self.metrics.completed_lookups += 1; // Block imported, continue the requests of pending child blocks - self.continue_child_lookups(lookup.block_root(), cx); + self.continue_child_lookups( + lookup.block_root(), + ImportedParent::LookupComplete, + cx, + ); self.update_metrics(); } else { debug!(id, "Attempting to drop non-existent lookup"); } false } - // If UnknownLookup do not log the request error. No need to drop child lookups nor - // update metrics because the lookup does not exist. - Err(LookupRequestError::UnknownLookup) => false, Err(error) => { debug!(id, source, ?error, "Dropping lookup on request error"); self.drop_lookup_and_children(id, error.into()); @@ -998,7 +787,7 @@ impl BlockLookups { if let Some(lookup) = self .single_block_lookups .values() - .find(|l| l.block_root() == awaiting_parent) + .find(|l| l.block_root() == awaiting_parent.parent_root()) { self.find_oldest_ancestor_lookup(lookup) } else { @@ -1012,12 +801,14 @@ impl BlockLookups { } /// Adds peers to a lookup and its ancestors recursively. + /// /// Note: Takes a `lookup_id` as argument to allow recursion on mutable lookups, without having /// to duplicate the code to add peers to a lookup fn add_peers_to_lookup_and_ancestors( &mut self, lookup_id: SingleLookupId, peers: &[PeerId], + peer_type: &PeerType, cx: &mut SyncNetworkContext, ) -> Result<(), String> { let lookup = self @@ -1027,7 +818,7 @@ impl BlockLookups { let mut added_some_peer = false; for peer in peers { - if lookup.add_peer(*peer) { + if lookup.add_peer(*peer, peer_type) { added_some_peer = true; debug!( block_root = ?lookup.block_root(), @@ -1037,15 +828,21 @@ impl BlockLookups { } } - if let Some(parent_root) = lookup.awaiting_parent() { - if let Some((&child_id, _)) = self + if let Some(&awaiting_parent) = lookup.awaiting_parent() { + // Regardless of gloas full/empty the lookup to add peers to is keyed by block_root + if let Some((&parent_id, _)) = self .single_block_lookups .iter() - .find(|(_, l)| l.block_root() == parent_root) + .find(|(_, l)| l.block_root() == awaiting_parent.parent_root()) { - self.add_peers_to_lookup_and_ancestors(child_id, peers, cx) + self.add_peers_to_lookup_and_ancestors( + parent_id, + peers, + &awaiting_parent.into_peer_type(), + cx, + ) } else { - Err(format!("Lookup references unknown parent {parent_root:?}")) + Err(format!("Lookup references unknown {awaiting_parent:?}")) } } else if added_some_peer { // If this lookup is not awaiting a parent and we added at least one peer, attempt to diff --git a/beacon_node/network/src/sync/block_lookups/parent_chain.rs b/beacon_node/network/src/sync/block_lookups/parent_chain.rs index 5deea1dd94..120ce5b1cc 100644 --- a/beacon_node/network/src/sync/block_lookups/parent_chain.rs +++ b/beacon_node/network/src/sync/block_lookups/parent_chain.rs @@ -13,7 +13,7 @@ impl From<&SingleBlockLookup> for Node { fn from(value: &SingleBlockLookup) -> Self { Self { block_root: value.block_root(), - parent_root: value.awaiting_parent(), + parent_root: value.awaiting_parent().map(|a| a.parent_root()), } } } 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 d54480e8e5..f03eed1638 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 @@ -1,22 +1,28 @@ use super::{BlockComponent, PeerId, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS}; -use crate::sync::block_lookups::common::RequestState; -use crate::sync::network_context::{ - LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, SendErrorProcessor, - SyncNetworkContext, +use crate::network_beacon_processor::BlockProcessingResult; +use crate::sync::block_lookups::{ + BlockDownloadResponse, CustodyDownloadResponse, PayloadDownloadResponse, }; -use beacon_chain::{BeaconChainTypes, BlockProcessStatus}; +use crate::sync::manager::BlockProcessType; +use crate::sync::network_context::{ + LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, RpcResponseError, + SendErrorProcessor, SyncNetworkContext, +}; +use beacon_chain::BeaconChainTypes; +use beacon_chain::block_verification_types::AsBlock; use educe::Educe; use lighthouse_network::service::api_types::Id; use parking_lot::RwLock; -use std::collections::HashSet; -use std::fmt::Debug; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, Instant}; use store::Hash256; use strum::IntoStaticStr; use tracing::{Span, debug_span}; -use types::data::FixedBlobSidecarList; -use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock, Slot}; +use types::{ + DataColumnSidecarList, EthSpec, ExecutionBlockHash, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, +}; // Dedicated enum for LookupResult to force its usage #[must_use = "LookupResult must be handled with on_lookup_result"] @@ -25,15 +31,19 @@ pub enum LookupResult { Completed, /// Lookup is expecting some future event from the network Pending, + /// Block's parent is not known to fork-choice, a parent lookup is needed + ParentUnknown { + parent_root: Hash256, + parent_block_hash: Option, + block_root: Hash256, + peers: Vec, + }, } #[derive(Debug, PartialEq, Eq, IntoStaticStr)] pub enum LookupRequestError { /// Too many failed attempts - TooManyAttempts { - /// The failed attempts were primarily due to processing failures. - cannot_process: bool, - }, + TooManyAttempts, /// Error sending event to network SendFailedNetwork(RpcRequestSendError), /// Error sending event to processor @@ -42,11 +52,6 @@ pub enum LookupRequestError { BadState(String), /// Lookup failed for some other reason and should be dropped Failed(/* reason: */ String), - /// Received MissingComponents when all components have been processed. This should never - /// happen, and indicates some internal bug - MissingComponentsAfterAllProcessed, - /// Attempted to retrieve a not known lookup id - UnknownLookup, /// Received a download result for a different request id than the in-flight request. /// There should only exist a single request at a time. Having multiple requests is a bug and /// can result in undefined state, so it's treated as a hard error and the lookup is dropped. @@ -56,40 +61,159 @@ pub enum LookupRequestError { }, } +#[derive(Debug, Clone, Copy)] +pub struct AwaitingParent { + parent_root: Hash256, + parent_block_hash: Option, +} + +impl AwaitingParent { + pub fn new(parent_root: Hash256, parent_block_hash: Option) -> Self { + Self { + parent_root, + parent_block_hash, + } + } + + pub fn parent_root(&self) -> Hash256 { + self.parent_root + } + + pub fn into_peer_type(self) -> PeerType { + PeerType::new(self.parent_block_hash) + } +} + +type PeerSet = Arc>>; + +#[derive(Debug)] +struct BlockRequest { + state: SingleLookupRequestState>>, +} + +impl BlockRequest { + fn new() -> Self { + Self { + state: SingleLookupRequestState::new(), + } + } + + fn is_complete(&self) -> bool { + self.state.is_processed() + } +} + +#[derive(Debug)] +enum DataRequest { + WaitingForBlock, + Request { + slot: Slot, + /// Peers to fetch the data columns from. Pre-Gloas this is the lookup's `peers`; for FULL + /// Gloas blocks this is the `gloas_child_peers` set proven to hold the columns. + peers: PeerSet, + state: SingleLookupRequestState>, + }, + NoData, +} + +impl DataRequest { + fn is_complete(&self) -> bool { + match &self { + DataRequest::WaitingForBlock => false, + DataRequest::Request { state, .. } => state.is_processed(), + DataRequest::NoData => true, + } + } +} + +/// Tracks the download + processing of a Gloas execution payload envelope. For FULL Gloas blocks the +/// execution payload arrives as a separate `SignedExecutionPayloadEnvelope`, mirroring the way data +/// columns are fetched and processed by `DataRequest`. +#[derive(Debug)] +enum PayloadRequest { + /// Block not yet downloaded, can't tell if a payload is needed. + WaitingForBlock, + /// Post-Gloas block: an execution payload envelope must be fetched and processed *if* the block + /// is FULL. We can't tell FULL from EMPTY from the block alone: only a FULL child of this block + /// proves a payload was published, which is signalled by `peers` becoming non-empty. + Request { + peers: PeerSet, + state: SingleLookupRequestState>>, + }, + /// Pre-Gloas block: no payload envelope exists, nothing to fetch. + PreGloas, +} + +impl PayloadRequest { + fn is_complete(&self) -> bool { + match &self { + PayloadRequest::WaitingForBlock => false, + PayloadRequest::Request { state, .. } => state.is_processed(), + PayloadRequest::PreGloas => true, + } + } +} + +/// Classifies how a peer relates to a lookup, controlling which peer set it is added to. +pub enum PeerType { + /// The peer can serve the looked-up block and (pre-Gloas) its data columns. + Block, + /// The peer claims to have imported a FULL child of this block whose bid references + /// `ExecutionBlockHash` as its parent. Such peers can serve this block's payload envelope and + /// data columns. + PayloadEnvelope(ExecutionBlockHash), +} + +impl PeerType { + /// `PayloadEnvelope` when the block's bid `parent_block_hash` is known (post-Gloas), else `Block`. + pub fn new(parent_block_hash: Option) -> Self { + match parent_block_hash { + Some(execution_hash) => PeerType::PayloadEnvelope(execution_hash), + None => PeerType::Block, + } + } +} + +/// Used by `is_awaiting_parent` to decide if it can resolve its awaiting parent status +#[derive(Debug, Clone, Copy)] +pub enum ImportedParent { + /// All requests of a lookup are complete, both for pre and post Gloas + LookupComplete, + /// Only post-Gloas, the block request has just been completed. Includes the bid block hash + OnlyGloasBlock(ExecutionBlockHash), +} + #[derive(Educe)] #[educe(Debug(bound(T: BeaconChainTypes)))] pub struct SingleBlockLookup { pub id: Id, - pub block_request_state: BlockRequestState, - pub component_requests: ComponentRequests, + block_root: Hash256, + block_request: BlockRequest, + data_request: DataRequest, + payload_request: PayloadRequest, /// Peers that claim to have imported this set of block components. This state is shared with /// the custody request to have an updated view of the peers that claim to have imported the /// block associated with this lookup. The peer set of a lookup can change rapidly, and faster /// than the lifetime of a custody request. #[educe(Debug(method(fmt_peer_set_as_len)))] - peers: Arc>>, - block_root: Hash256, - awaiting_parent: Option, + peers: PeerSet, + /// Post-Gloas only: peers that claim to have imported a FULL child of this block, keyed by the + /// child's bid `parent_block_hash`. These (not `peers`) are the peers proven to hold this + /// block's payload envelope and data columns. + #[educe(Debug(method(fmt_peer_map_as_len)))] + gloas_child_peers: HashMap, + awaiting_parent: Option, created: Instant, pub(crate) span: Span, } -#[derive(Debug)] -pub(crate) enum ComponentRequests { - WaitingForBlock, - ActiveBlobRequest(BlobRequestState, usize), - ActiveCustodyRequest(CustodyRequestState), - // When printing in debug this state display the reason why it's not needed - #[allow(dead_code)] - NotNeeded(&'static str), -} - impl SingleBlockLookup { pub fn new( requested_block_root: Hash256, peers: &[PeerId], + peer_type: &PeerType, id: Id, - awaiting_parent: Option, + awaiting_parent: Option, ) -> Self { let lookup_span = debug_span!( "lh_single_block_lookup", @@ -97,45 +221,72 @@ impl SingleBlockLookup { id = id, ); + let block_peers: PeerSet = Arc::new(RwLock::new(peers.iter().copied().collect())); + let mut gloas_child_peers = HashMap::new(); + match peer_type { + PeerType::Block => {} + PeerType::PayloadEnvelope(execution_hash) => { + gloas_child_peers.insert(*execution_hash, block_peers.clone()); + } + } + Self { id, - block_request_state: BlockRequestState::new(requested_block_root), - component_requests: ComponentRequests::WaitingForBlock, - peers: Arc::new(RwLock::new(HashSet::from_iter(peers.iter().copied()))), block_root: requested_block_root, + block_request: BlockRequest::new(), + data_request: DataRequest::WaitingForBlock, + payload_request: PayloadRequest::WaitingForBlock, + peers: block_peers, + gloas_child_peers, awaiting_parent, created: Instant::now(), span: lookup_span, } } - /// Reset the status of all internal requests - pub fn reset_requests(&mut self) { - self.block_request_state = BlockRequestState::new(self.block_root); - self.component_requests = ComponentRequests::WaitingForBlock; - } - - /// Return the slot of this lookup's block if it's currently cached as `AwaitingProcessing` + /// Return the slot of this lookup's block if it's currently cached pub fn peek_downloaded_block_slot(&self) -> Option { - self.block_request_state + self.block_request .state .peek_downloaded_data() .map(|block| block.slot()) } + pub fn peek_downloaded_bid_block_hash(&self) -> Option { + self.block_request + .state + .peek_downloaded_data() + .and_then(|block| { + block + .message() + .body() + .signed_execution_payload_bid() + .ok() + .map(|bid| bid.message.block_hash) + }) + } + /// Get the block root that is being requested. pub fn block_root(&self) -> Hash256 { self.block_root } - pub fn awaiting_parent(&self) -> Option { - self.awaiting_parent + pub fn awaiting_parent(&self) -> Option<&AwaitingParent> { + self.awaiting_parent.as_ref() + } + + pub fn is_awaiting_block(&self, block_root: Hash256) -> bool { + if let Some(awaiting_parent) = &self.awaiting_parent { + awaiting_parent.parent_root() == block_root + } else { + false + } } /// Mark this lookup as awaiting a parent lookup from being processed. Meanwhile don't send /// components for processing. - pub fn set_awaiting_parent(&mut self, parent_root: Hash256) { - self.awaiting_parent = Some(parent_root) + pub fn set_awaiting_parent(&mut self, parent: AwaitingParent) { + self.awaiting_parent = Some(parent); } /// Mark this lookup as no longer awaiting a parent lookup. Components can be sent for @@ -144,6 +295,37 @@ impl SingleBlockLookup { self.awaiting_parent = None; } + /// Check if this lookup awaiting_parent status can be resolved given that `parent_root` and + /// `imported_parent` have just been imported + pub fn is_awaiting_parent( + &mut self, + parent_root: Hash256, + imported_parent: ImportedParent, + ) -> bool { + let Some(awaiting_parent) = self.awaiting_parent else { + return false; + }; + if awaiting_parent.parent_root() != parent_root { + return false; + } + match imported_parent { + ImportedParent::LookupComplete => true, + ImportedParent::OnlyGloasBlock(bid_block_hash) => { + if let Some(parent_block_hash) = awaiting_parent.parent_block_hash { + // This lookup is the execution child of `parent_execution_hash`. If the + // parent hash the same `bid_block_hash` this is FULL child and we must wait + // for the entire parent lookup to be imported. Otherwise it's a EMPTY child + // and we can import now. + parent_block_hash != bid_block_hash + } else { + // A parent that's gloas imported and this lookup claims to be before gloas. + debug_assert!(false, "Received post-gloas action for pre-gloas lookup"); + false + } + } + } + } + /// Returns the time elapsed since this lookup was created pub fn elapsed_since_created(&self) -> Duration { self.created.elapsed() @@ -152,15 +334,12 @@ impl SingleBlockLookup { /// Maybe insert a verified response into this lookup. Returns true if imported pub fn add_child_components(&mut self, block_component: BlockComponent) -> bool { match block_component { - BlockComponent::Block(block) => self - .block_request_state - .state - .insert_verified_response(block), - BlockComponent::DataColumn(_) | BlockComponent::PartialDataColumn(_) => { - // For now ignore single blobs and columns, as the blob request state assumes all blobs are - // attributed to the same peer = the peer serving the remaining blobs. Ignoring this - // block component has a minor effect, causing the node to re-request this blob - // once the parent chain is successfully resolved + BlockComponent::Block(block) => { + self.block_request.state.insert_verified_response(block) + } + BlockComponent::Sidecar => { + // There's nothing to do here, there's no component to insert. The lookup downloads + // its required data columns itself once it has the block. false } } @@ -171,33 +350,19 @@ impl SingleBlockLookup { self.block_root() == block_root } - /// Returns true if the block has already been downloaded. - pub fn all_components_processed(&self) -> bool { - self.block_request_state.state.is_processed() - && match &self.component_requests { - ComponentRequests::WaitingForBlock => false, - ComponentRequests::ActiveBlobRequest(request, _) => request.state.is_processed(), - ComponentRequests::ActiveCustodyRequest(request) => request.state.is_processed(), - ComponentRequests::NotNeeded { .. } => true, - } - } - /// Returns true if this request is expecting some event to make progress pub fn is_awaiting_event(&self) -> bool { self.awaiting_parent.is_some() - || self.block_request_state.state.is_awaiting_event() - || match &self.component_requests { - // If components are waiting for the block request to complete, here we should - // check if the`block_request_state.state.is_awaiting_event(). However we already - // checked that above, so `WaitingForBlock => false` is equivalent. - ComponentRequests::WaitingForBlock => false, - ComponentRequests::ActiveBlobRequest(request, _) => { - request.state.is_awaiting_event() - } - ComponentRequests::ActiveCustodyRequest(request) => { - request.state.is_awaiting_event() - } - ComponentRequests::NotNeeded { .. } => false, + || self.block_request.state.is_awaiting_event() + || match &self.data_request { + DataRequest::WaitingForBlock => true, + DataRequest::Request { state, .. } => state.is_awaiting_event(), + DataRequest::NoData => false, + } + || match &self.payload_request { + PayloadRequest::WaitingForBlock => true, + PayloadRequest::Request { state, .. } => state.is_awaiting_event(), + PayloadRequest::PreGloas => false, } } @@ -208,147 +373,281 @@ impl SingleBlockLookup { cx: &mut SyncNetworkContext, ) -> Result { let _guard = self.span.clone().entered(); - // TODO: Check what's necessary to download, specially for blobs - self.continue_request::>(cx, 0)?; - if let ComponentRequests::WaitingForBlock = self.component_requests { - let downloaded_block = self - .block_request_state - .state - .peek_downloaded_data() - .cloned(); + // === Block request === + self.block_request.state.maybe_start_downloading(|| { + cx.block_lookup_request(self.id, self.peers.clone(), self.block_root) + })?; + if self.awaiting_parent.is_none() + && let Some(data) = self.block_request.state.maybe_start_processing() + { + cx.send_block_for_processing(self.id, self.block_root, data.value, data.seen_timestamp) + .map_err(LookupRequestError::SendFailedProcessor)?; + } - if let Some(block) = downloaded_block.or_else(|| { - // If the block is already being processed or fully validated, retrieve how many blobs - // it expects. Consider any stage of the block. If the block root has been validated, we - // can assert that this is the correct value of `blob_kzg_commitments_count`. - match cx.chain.get_block_process_status(&self.block_root) { - BlockProcessStatus::Unknown => None, - BlockProcessStatus::NotValidated(block, _) - | BlockProcessStatus::ExecutionValidated(block) => Some(block.clone()), + // === Data request === + loop { + match &mut self.data_request { + DataRequest::WaitingForBlock => { + if let Some(block) = self.block_request.state.peek_downloaded_data() { + let block_epoch = block + .slot() + .epoch(::EthSpec::slots_per_epoch()); + self.data_request = if block.num_expected_blobs() == 0 { + DataRequest::NoData + } else if cx.chain.should_fetch_custody_columns(block_epoch) { + DataRequest::Request { + slot: block.slot(), + peers: self.get_data_peers(block.payload_bid_block_hash().ok()), + state: SingleLookupRequestState::new(), + } + } else { + DataRequest::NoData + }; + } else { + break; + } } - }) { - let expected_blobs = block.num_expected_blobs(); - let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); - if expected_blobs == 0 { - self.component_requests = ComponentRequests::NotNeeded("no data"); - } else if cx.chain.should_fetch_blobs(block_epoch) { - self.component_requests = ComponentRequests::ActiveBlobRequest( - BlobRequestState::new(self.block_root), - expected_blobs, - ); - } else if cx.chain.should_fetch_custody_columns(block_epoch) { - self.component_requests = ComponentRequests::ActiveCustodyRequest( - CustodyRequestState::new(self.block_root), - ); - } else { - self.component_requests = ComponentRequests::NotNeeded("outside da window"); + DataRequest::Request { slot, peers, state } => { + state.maybe_start_downloading(|| { + cx.custody_lookup_request(self.id, self.block_root, *slot, peers.clone()) + })?; + // Wait for the current block and parent to be imported, data column processing result handle does + // not support `ParentUnknown`. + if self.block_request.state.is_processed() + && self.awaiting_parent.is_none() + && let Some(data) = state.maybe_start_processing() + { + cx.send_custody_columns_for_processing( + self.id, + self.block_root, + data.value, + data.seen_timestamp, + BlockProcessType::SingleCustodyColumn(self.id), + ) + .map_err(LookupRequestError::SendFailedProcessor)?; + } + break; } - } else { - // Wait to download the block before downloading blobs. Then we can be sure that the - // block has data, so there's no need to do "blind" requests for all possible blobs and - // latter handle the case where if the peer sent no blobs, penalize. - // - // Lookup sync event safety: Reaching this code means that a block is not in any pre-import - // cache nor in the request state of this lookup. Therefore, the block must either: (1) not - // be downloaded yet or (2) the block is already imported into the fork-choice. - // In case (1) the lookup must either successfully download the block or get dropped. - // In case (2) the block will be downloaded, processed, reach `DuplicateFullyImported` - // and get dropped as completed. + DataRequest::NoData => break, } } - match &self.component_requests { - ComponentRequests::WaitingForBlock => {} // do nothing - ComponentRequests::ActiveBlobRequest(_, expected_blobs) => { - self.continue_request::>(cx, *expected_blobs)? + // === Payload request (Gloas only) === + loop { + match &mut self.payload_request { + PayloadRequest::WaitingForBlock => { + if let Some(block) = self.block_request.state.peek_downloaded_data() { + self.payload_request = if block.fork_name_unchecked().gloas_enabled() { + PayloadRequest::Request { + peers: self.get_data_peers(block.payload_bid_block_hash().ok()), + state: SingleLookupRequestState::new(), + } + } else { + PayloadRequest::PreGloas + }; + } else { + break; + } + } + PayloadRequest::Request { peers, state } => { + state.maybe_start_downloading(|| { + cx.payload_lookup_request(self.id, peers.clone(), self.block_root) + })?; + // The envelope can only be verified once the block itself is imported; + // otherwise processing returns `BlockRootUnknown` and the lookup burns retries + // until `TooManyAttempts` while the block is parked awaiting its parent. + if self.block_request.state.is_processed() + && let Some(data) = state.maybe_start_processing() + { + cx.send_payload_for_processing( + self.block_root, + data.value, + data.seen_timestamp, + BlockProcessType::SinglePayloadEnvelope(self.id), + ) + .map_err(LookupRequestError::SendFailedProcessor)?; + } + break; + } + PayloadRequest::PreGloas => break, } - ComponentRequests::ActiveCustodyRequest(_) => { - self.continue_request::>(cx, 0)? - } - ComponentRequests::NotNeeded { .. } => {} // do nothing } // If all components of this lookup are already processed, there will be no future events // that can make progress so it must be dropped. Consider the lookup completed. // This case can happen if we receive the components from gossip during a retry. - if self.all_components_processed() { - self.span = Span::none(); - Ok(LookupResult::Completed) + if self.block_request.is_complete() + && self.data_request.is_complete() + && self.payload_request.is_complete() + { + return Ok(LookupResult::Completed); + } + + Ok(LookupResult::Pending) + } + + /// Returns the peers that should serve this block's data columns and payload envelope. For FULL + /// Gloas blocks these are the peers that claimed to have imported a FULL child of this block + /// (keyed by this block's bid `block_hash`). Pre-Gloas blocks carry no bid, so this returns the + /// lookup's `peers` unchanged. + fn get_data_peers(&mut self, bid_block_hash: Option) -> PeerSet { + if let Some(bid_block_hash) = bid_block_hash { + // Gloas: the child-attested peer set for this bid is the canonical peer set. DO NOT + // default to `self.peers`: post-Gloas `self.peers` have not claimed to import this + // block's data nor its payload. This set may remain empty until a FULL child arrives. + self.gloas_child_peers + .entry(bid_block_hash) + .or_default() + .clone() } else { - Ok(LookupResult::Pending) + self.peers.clone() } } - /// Potentially makes progress on this request if it's in a progress-able state - fn continue_request>( + /// Handle block processing result. Advances the lookup state machine. + pub fn on_block_processing_result( &mut self, + result: BlockProcessingResult, cx: &mut SyncNetworkContext, - expected_blobs: usize, - ) -> Result<(), LookupRequestError> { - let id = self.id; - let awaiting_parent = self.awaiting_parent.is_some(); - let request = - R::request_state_mut(self).map_err(|e| LookupRequestError::BadState(e.to_owned()))?; - - // Attempt to progress awaiting downloads - if request.get_state().is_awaiting_download() { - // Verify the current request has not exceeded the maximum number of attempts. - let request_state = request.get_state(); - if request_state.failed_attempts() >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { - let cannot_process = request_state.more_failed_processing_attempts(); - return Err(LookupRequestError::TooManyAttempts { cannot_process }); + ) -> Result { + match result { + BlockProcessingResult::Imported(_fully_imported, _info) => { + self.block_request.state.on_processing_success()?; } - - let peers = self.peers.clone(); - let request = R::request_state_mut(self) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))?; - - match request.make_request(id, peers, expected_blobs, cx)? { - LookupRequestResult::RequestSent(req_id) => { - // Lookup sync event safety: If make_request returns `RequestSent`, we are - // guaranteed that `BlockLookups::on_download_response` will be called exactly - // with this `req_id`. - request.get_state_mut().on_download_start(req_id)? - } - LookupRequestResult::NoRequestNeeded(reason) => { - // Lookup sync event safety: Advances this request to the terminal `Processed` - // state. If all requests reach this state, the request is marked as completed - // in `Self::continue_requests`. - request.get_state_mut().on_completed_request(reason)? - } - // Sync will receive a future event to make progress on the request, do nothing now - LookupRequestResult::Pending(reason) => { - // Lookup sync event safety: Refer to the code paths constructing - // `LookupRequestResult::Pending` - request - .get_state_mut() - .update_awaiting_download_status(reason); - return Ok(()); + BlockProcessingResult::ParentUnknown { + parent_root, + parent_block_hash, + } => { + // `BlockError::ParentUnknown` is only returned when processing blocks. Revert the + // block request to `Downloaded` and park this lookup until the parent resolves; a + // future call to `continue_requests` will re-submit the block for processing once + // the parent lookup completes. + self.block_request.state.revert_to_awaiting_processing()?; + self.set_awaiting_parent(AwaitingParent { + parent_root, + parent_block_hash, + }); + return Ok(LookupResult::ParentUnknown { + parent_root, + parent_block_hash, + block_root: self.block_root, + peers: self.all_peers(), + }); + } + BlockProcessingResult::Error { penalty, .. } => { + let peers = self.block_request.state.on_processing_failure()?; + if let Some((action, whom, msg)) = penalty { + whom.apply(action, &peers, msg, cx); } } - - // Otherwise, attempt to progress awaiting processing - // If this request is awaiting a parent lookup to be processed, do not send for processing. - // The request will be rejected with unknown parent error. - } else if !awaiting_parent { - // maybe_start_processing returns Some if state == AwaitingProcess. This pattern is - // useful to conditionally access the result data. - if let Some(result) = request.get_state_mut().maybe_start_processing() { - // Lookup sync event safety: If `send_for_processing` returns Ok() we are guaranteed - // that `BlockLookups::on_processing_result` will be called exactly once with this - // lookup_id - return R::send_for_processing(id, result, cx); - } - // Lookup sync event safety: If the request is not in `AwaitingDownload` or - // `AwaitingProcessing` state it is guaranteed to receive some event to make progress. } + self.continue_requests(cx) + } - // Lookup sync event safety: If a lookup is awaiting a parent we are guaranteed to either: - // (1) attempt to make progress with `BlockLookups::continue_child_lookups` if the parent - // lookup completes, or (2) get dropped if the parent fails and is dropped. + /// Handle data processing result + pub fn on_data_processing_result( + &mut self, + result: BlockProcessingResult, + cx: &mut SyncNetworkContext, + ) -> Result { + let DataRequest::Request { state, .. } = &mut self.data_request else { + return Err(LookupRequestError::BadState("no data_request".to_owned())); + }; - Ok(()) + match result { + BlockProcessingResult::Imported(_fully_imported, _info) => { + state.on_processing_success()?; + } + BlockProcessingResult::ParentUnknown { .. } => { + return Err(LookupRequestError::BadState( + "data processing returned ParentUnknown".to_owned(), + )); + } + BlockProcessingResult::Error { penalty, .. } => { + let peers = state.on_processing_failure()?; + if let Some((action, whom, msg)) = penalty { + whom.apply(action, &peers, msg, cx); + } + } + } + self.continue_requests(cx) + } + + /// Handle payload envelope processing result (Gloas only). + pub fn on_payload_processing_result( + &mut self, + result: BlockProcessingResult, + cx: &mut SyncNetworkContext, + ) -> Result { + let PayloadRequest::Request { state, .. } = &mut self.payload_request else { + return Err(LookupRequestError::BadState( + "no payload_request".to_owned(), + )); + }; + + match result { + BlockProcessingResult::Imported(_fully_imported, _info) => { + state.on_processing_success()?; + } + BlockProcessingResult::ParentUnknown { .. } => { + return Err(LookupRequestError::BadState( + "payload processing returned ParentUnknown".to_owned(), + )); + } + BlockProcessingResult::Error { penalty, .. } => { + let peers = state.on_processing_failure()?; + if let Some((action, whom, msg)) = penalty { + whom.apply(action, &peers, msg, cx); + } + } + } + self.continue_requests(cx) + } + + /// Handle a block download response. Updates download state and advances the lookup. + pub fn on_block_download_response( + &mut self, + req_id: ReqId, + result: BlockDownloadResponse, + cx: &mut SyncNetworkContext, + ) -> Result { + self.block_request + .state + .on_download_response(req_id, result)?; + self.continue_requests(cx) + } + + /// Handle a custody columns download response. Updates download state and advances the lookup. + pub fn on_custody_download_response( + &mut self, + req_id: ReqId, + result: CustodyDownloadResponse, + cx: &mut SyncNetworkContext, + ) -> Result { + let DataRequest::Request { state, .. } = &mut self.data_request else { + return Err(LookupRequestError::BadState("no data_request".to_owned())); + }; + + state.on_download_response(req_id, result)?; + self.continue_requests(cx) + } + + /// Handle a payload envelope download response. Updates download state and advances the lookup. + pub fn on_payload_download_response( + &mut self, + req_id: ReqId, + result: PayloadDownloadResponse, + cx: &mut SyncNetworkContext, + ) -> Result { + let PayloadRequest::Request { state, .. } = &mut self.payload_request else { + return Err(LookupRequestError::BadState( + "no payload_request".to_owned(), + )); + }; + + state.on_download_response(req_id, result)?; + self.continue_requests(cx) } /// Get all unique peers that claim to have imported this set of block components @@ -357,72 +656,54 @@ impl SingleBlockLookup { } /// Add peer to all request states. The peer must be able to serve this request. - /// Returns true if the peer was newly inserted into some request state. - pub fn add_peer(&mut self, peer_id: PeerId) -> bool { - self.peers.write().insert(peer_id) + /// Returns true if the peer was newly inserted into any peer set. + pub fn add_peer(&mut self, peer_id: PeerId, peer_type: &PeerType) -> bool { + let mut added = false; + match peer_type { + PeerType::PayloadEnvelope(execution_hash) => { + // This peer claims to have imported a FULL child of this block whose bid references + // `execution_hash` as its parent. It is therefore proven to hold this block's + // payload envelope and data columns. + added |= self + .gloas_child_peers + .entry(*execution_hash) + .or_default() + .write() + .insert(peer_id); + } + PeerType::Block => {} + } + // Always add to the main block peers, they can at least serve the block. + added |= self.peers.write().insert(peer_id); + added } /// Remove peer from available peers. pub fn remove_peer(&mut self, peer_id: &PeerId) { self.peers.write().remove(peer_id); + for set in self.gloas_child_peers.values() { + set.write().remove(peer_id); + } } /// Returns true if this lookup has zero peers pub fn has_no_peers(&self) -> bool { - self.peers.read().is_empty() - } -} - -/// The state of the blob request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct BlobRequestState { - #[educe(Debug(ignore))] - pub block_root: Hash256, - pub state: SingleLookupRequestState>, -} - -impl BlobRequestState { - pub fn new(block_root: Hash256) -> Self { - Self { - block_root, - state: SingleLookupRequestState::new(), - } - } -} - -/// The state of the custody request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct CustodyRequestState { - #[educe(Debug(ignore))] - pub block_root: Hash256, - pub state: SingleLookupRequestState>, -} - -impl CustodyRequestState { - pub fn new(block_root: Hash256) -> Self { - Self { - block_root, - state: SingleLookupRequestState::new(), - } - } -} - -/// The state of the block request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct BlockRequestState { - #[educe(Debug(ignore))] - pub requested_block_root: Hash256, - pub state: SingleLookupRequestState>>, -} - -impl BlockRequestState { - pub fn new(block_root: Hash256) -> Self { - Self { - requested_block_root: block_root, - state: SingleLookupRequestState::new(), + if self.block_request.is_complete() + && let Some(block) = self.block_request.state.peek_downloaded_data() + && let Ok(bid_block_hash) = block.payload_bid_block_hash() + { + // Gloas block request complete, the main peer set is irrelevant. Check only the gloas + // child peers + match self.gloas_child_peers.get(&bid_block_hash) { + Some(set) => set.read().is_empty(), + None => false, + } + } else { + self.peers.read().is_empty() + && self + .gloas_child_peers + .values() + .all(|set| set.read().is_empty()) } } } @@ -430,11 +711,20 @@ impl BlockRequestState { #[derive(Debug, Clone)] pub struct DownloadResult { pub value: T, - pub block_root: Hash256, pub seen_timestamp: Duration, pub peer_group: PeerGroup, } +impl DownloadResult { + pub fn new(value: T, peer_group: PeerGroup, seen_timestamp: Duration) -> Self { + Self { + value, + seen_timestamp, + peer_group, + } + } +} + #[derive(IntoStaticStr)] pub enum State { AwaitingDownload(/* reason */ &'static str), @@ -443,7 +733,7 @@ pub enum State { /// Request is processing, sent by lookup sync Processing(DownloadResult), /// Request is processed - Processed(/* reason */ &'static str), + Processed(/* reason */ &'static str, T), } /// Object representing the state of a single block or blob lookup request. @@ -510,10 +800,29 @@ impl SingleLookupRequestState { State::Downloading { .. } => None, State::AwaitingProcess(result) => Some(&result.value), State::Processing(result) => Some(&result.value), - State::Processed { .. } => None, + State::Processed(_, value) => Some(value), } } + /// Drive download: check max attempts, issue request, handle result. + fn maybe_start_downloading( + &mut self, + request_fn: impl FnOnce() -> Result, RpcRequestSendError>, + ) -> Result<(), LookupRequestError> { + if self.is_awaiting_download() { + match request_fn().map_err(LookupRequestError::SendFailedNetwork)? { + LookupRequestResult::RequestSent(req_id) => self.on_download_start(req_id)?, + LookupRequestResult::NoRequestNeeded(reason, value) => { + self.on_completed_request(reason, value)? + } + LookupRequestResult::Pending(reason) => { + self.update_awaiting_download_status(reason) + } + } + } + Ok(()) + } + /// Switch to `AwaitingProcessing` if the request is in `AwaitingDownload` state, otherwise /// ignore. pub fn insert_verified_response(&mut self, result: DownloadResult) -> bool { @@ -546,6 +855,17 @@ impl SingleLookupRequestState { } } + pub fn on_download_response( + &mut self, + req_id: ReqId, + result: Result, RpcResponseError>, + ) -> Result<(), LookupRequestError> { + match result { + Ok(result) => self.on_download_success(req_id, result), + Err(_) => self.on_download_failure(req_id), + } + } + /// Registers a failure in downloading a block. This might be a peer disconnection or a wrong /// block. pub fn on_download_failure(&mut self, req_id: ReqId) -> Result<(), LookupRequestError> { @@ -558,6 +878,10 @@ impl SingleLookupRequestState { }); } self.failed_downloading = self.failed_downloading.saturating_add(1); + if self.failed_downloading >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { + return Err(LookupRequestError::TooManyAttempts); + } + self.state = State::AwaitingDownload("not started"); Ok(()) } @@ -622,6 +946,9 @@ impl SingleLookupRequestState { State::Processing(result) => { let peers_source = result.peer_group.clone(); self.failed_processing = self.failed_processing.saturating_add(1); + if self.failed_processing >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { + return Err(LookupRequestError::TooManyAttempts); + } self.state = State::AwaitingDownload("not started"); Ok(peers_source) } @@ -633,8 +960,8 @@ impl SingleLookupRequestState { pub fn on_processing_success(&mut self) -> Result<(), LookupRequestError> { match &self.state { - State::Processing(_) => { - self.state = State::Processed("processing success"); + State::Processing(data) => { + self.state = State::Processed("processing success", data.value.clone()); Ok(()) } other => Err(LookupRequestError::BadState(format!( @@ -644,10 +971,14 @@ impl SingleLookupRequestState { } /// Mark a request as complete without any download or processing - pub fn on_completed_request(&mut self, reason: &'static str) -> Result<(), LookupRequestError> { + pub fn on_completed_request( + &mut self, + reason: &'static str, + value: T, + ) -> Result<(), LookupRequestError> { match &self.state { State::AwaitingDownload { .. } => { - self.state = State::Processed(reason); + self.state = State::Processed(reason, value); Ok(()) } other => Err(LookupRequestError::BadState(format!( @@ -655,15 +986,6 @@ impl SingleLookupRequestState { ))), } } - - /// The total number of failures, whether it be processing or downloading. - pub fn failed_attempts(&self) -> u8 { - self.failed_processing + self.failed_downloading - } - - pub fn more_failed_processing_attempts(&self) -> bool { - self.failed_processing >= self.failed_downloading - } } // Display is used in the BadState assertions above @@ -680,16 +1002,24 @@ impl std::fmt::Debug for State { match self { Self::AwaitingDownload(reason) => write!(f, "AwaitingDownload({})", reason), Self::Downloading(req_id) => write!(f, "Downloading({:?})", req_id), - Self::AwaitingProcess(d) => write!(f, "AwaitingProcess({:?})", d.peer_group), - Self::Processing(d) => write!(f, "Processing({:?})", d.peer_group), - Self::Processed(reason) => write!(f, "Processed({})", reason), + Self::AwaitingProcess(_) => write!(f, "AwaitingProcess"), + Self::Processing(_) => write!(f, "Processing"), + Self::Processed(reason, _) => write!(f, "Processed({})", reason), } } } fn fmt_peer_set_as_len( - peer_set: &Arc>>, + peer_set: &PeerSet, f: &mut std::fmt::Formatter, ) -> Result<(), std::fmt::Error> { write!(f, "{}", peer_set.read().len()) } + +fn fmt_peer_map_as_len( + peer_map: &HashMap, + f: &mut std::fmt::Formatter, +) -> Result<(), std::fmt::Error> { + let total = peer_map.values().map(|set| set.read().len()).sum::(); + write!(f, "{}", total) +} diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index bb43396473..b64ae4a4c5 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1,3 +1,4 @@ +use beacon_chain::payload_envelope_verification::AvailableEnvelope; use beacon_chain::{ BeaconChainTypes, block_verification_types::{AvailableBlockData, RangeSyncBlock}, @@ -9,14 +10,15 @@ use lighthouse_network::{ PeerId, service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, DataColumnsByRangeRequestId, + PayloadEnvelopesByRangeRequestId, }, }; use ssz_types::RuntimeVariableList; use std::{collections::HashMap, sync::Arc}; -use tracing::{Span, debug}; +use tracing::{Span, debug, warn}; use types::{ BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - Hash256, SignedBeaconBlock, + Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, }; use crate::sync::network_context::MAX_COLUMN_RETRIES; @@ -37,6 +39,13 @@ pub struct RangeBlockComponentsRequest { blocks_request: ByRangeRequest>>>, /// Sidecars we have received awaiting for their corresponding block. block_data_request: RangeBlockDataRequest, + /// Payload envelopes for Gloas blocks. + payloads_request: Option< + ByRangeRequest< + PayloadEnvelopesByRangeRequestId, + Vec>>, + >, + >, /// Span to track the range request and all children range requests. pub(crate) request_span: Span, } @@ -62,7 +71,7 @@ enum RangeBlockDataRequest { } #[derive(Debug)] -pub(crate) enum CouplingError { +pub enum CouplingError { InternalError(String), /// The peer we requested the columns from was faulty/malicious DataColumnPeerFailure { @@ -71,6 +80,7 @@ pub(crate) enum CouplingError { exceeded_retries: bool, }, BlobPeerFailure(String), + EnvelopePeerFailure(String), } impl RangeBlockComponentsRequest { @@ -88,6 +98,7 @@ impl RangeBlockComponentsRequest { Vec<(DataColumnsByRangeRequestId, Vec)>, Vec, )>, + payloads_req_id: Option, request_span: Span, ) -> Self { let block_data_request = if let Some(blobs_req_id) = blobs_req_id { @@ -110,6 +121,7 @@ impl RangeBlockComponentsRequest { Self { blocks_request: ByRangeRequest::Active(blocks_req_id), block_data_request, + payloads_request: payloads_req_id.map(ByRangeRequest::Active), request_span, } } @@ -191,6 +203,17 @@ impl RangeBlockComponentsRequest { } } + pub fn add_payload_envelopes( + &mut self, + req_id: PayloadEnvelopesByRangeRequestId, + envelopes: Vec>>, + ) -> Result<(), String> { + match &mut self.payloads_request { + Some(req) => req.finish(req_id, envelopes), + None => Err("received payload envelopes but none were expected".to_owned()), + } + } + /// Attempts to construct RPC blocks from all received components. /// /// Returns `None` if not all expected requests have completed. @@ -208,6 +231,11 @@ impl RangeBlockComponentsRequest { return None; }; + // Check if payload envelopes are still pending + if let Some(ByRangeRequest::Active(_)) = &self.payloads_request { + return None; + } + // Increment the attempt once this function returns the response or errors match &mut self.block_data_request { RangeBlockDataRequest::NoData => Some(Self::responses_with_blobs( @@ -254,6 +282,12 @@ impl RangeBlockComponentsRequest { } } + let payload_envelopes = self.payloads_request.as_ref().and_then(|request| { + request + .to_finished() + .map(|payload_envelopes| payload_envelopes.to_vec()) + }); + let resp = Self::responses_with_custody_columns( blocks.to_vec(), data_columns, @@ -262,6 +296,7 @@ impl RangeBlockComponentsRequest { *attempt, da_checker, spec, + payload_envelopes, ); if let Err(CouplingError::DataColumnPeerFailure { @@ -352,6 +387,7 @@ impl RangeBlockComponentsRequest { Ok(responses) } + #[allow(clippy::too_many_arguments)] fn responses_with_custody_columns( blocks: Vec>>, data_columns: DataColumnSidecarList, @@ -360,10 +396,19 @@ impl RangeBlockComponentsRequest { attempt: usize, da_checker: Arc>, spec: Arc, + payload_envelopes: Option>>>, ) -> Result>, CouplingError> where T: BeaconChainTypes, { + // Index envelopes by beacon_block_root for correct coupling. + let mut envelopes_by_block_root = payload_envelopes.map(|envelopes| { + envelopes + .into_iter() + .map(|e| (e.beacon_block_root(), e)) + .collect::>() + }); + // Group data columns by block_root and index let mut data_columns_by_block = HashMap::>>>::new(); @@ -393,7 +438,7 @@ impl RangeBlockComponentsRequest { let exceeded_retries = attempt >= MAX_COLUMN_RETRIES; for block in blocks { let block_root = get_block_root(&block); - range_sync_blocks.push(if block.num_expected_blobs() > 0 { + let custody_columns = if block.num_expected_blobs() > 0 { let Some(mut data_columns_by_index) = data_columns_by_block.remove(&block_root) else { let responsible_peers = column_to_peer.iter().map(|c| (*c.0, *c.1)).collect(); @@ -401,7 +446,6 @@ impl RangeBlockComponentsRequest { error: format!("No columns for block {block_root:?} with data"), faulty_peers: responsible_peers, exceeded_retries, - }); }; @@ -415,16 +459,21 @@ impl RangeBlockComponentsRequest { custody_columns.push(CustodyDataColumn::from_asserted_custody(data_column)); } else { let Some(responsible_peer) = column_to_peer.get(index) else { - return Err(CouplingError::InternalError(format!("Internal error, no request made for column {}", index))); + return Err(CouplingError::InternalError(format!( + "Internal error, no request made for column {}", + index + ))); }; naughty_peers.push((*index, *responsible_peer)); } } if !naughty_peers.is_empty() { return Err(CouplingError::DataColumnPeerFailure { - error: format!("Peers did not return column for block_root {block_root:?} {naughty_peers:?}"), + error: format!( + "Peers did not return column for block_root {block_root:?} {naughty_peers:?}" + ), faulty_peers: naughty_peers, - exceeded_retries + exceeded_retries, }); } @@ -439,15 +488,31 @@ impl RangeBlockComponentsRequest { ); } - let block_data = AvailableBlockData::new_with_data_columns(custody_columns.iter().map(|c| c.as_data_column().clone()).collect::>()); - - RangeSyncBlock::new(block, block_data, &da_checker, spec.clone()) - .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? + custody_columns + .iter() + .map(|c| c.as_data_column().clone()) + .collect::>() } else { - // Block has no data, expects zero columns + vec![] + }; + let range_sync_block = if let Some(envelopes_by_block_root) = + envelopes_by_block_root.as_mut() + { + let envelope = envelopes_by_block_root.remove(&block_root); + let available_envelope = + envelope.map(|env| AvailableEnvelope::new(env, custody_columns)); + + RangeSyncBlock::new_gloas(block, available_envelope) + .map_err(CouplingError::EnvelopePeerFailure)? + } else if custody_columns.is_empty() { RangeSyncBlock::new(block, AvailableBlockData::NoData, &da_checker, spec.clone()) .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? - }); + } else { + let block_data = AvailableBlockData::new_with_data_columns(custody_columns); + RangeSyncBlock::new(block, block_data, &da_checker, spec.clone()) + .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? + }; + range_sync_blocks.push(range_sync_block); } // Assert that there are no columns left for other blocks @@ -458,6 +523,13 @@ impl RangeBlockComponentsRequest { debug!(?remaining_roots, "Not all columns consumed for block"); } + // Recoverable error, log and continue + if let Some(envelopes_by_block_root) = envelopes_by_block_root + && !envelopes_by_block_root.is_empty() + { + warn!("Peer returned extra envelopes not matching any block"); + } + Ok(range_sync_blocks) } } @@ -489,21 +561,28 @@ mod tests { use crate::sync::network_context::MAX_COLUMN_RETRIES; use super::RangeBlockComponentsRequest; + use beacon_chain::block_verification_types::RangeSyncBlock; use beacon_chain::custody_context::NodeCustodyType; + use beacon_chain::data_availability_checker::DataAvailabilityChecker; use beacon_chain::test_utils::{ - NumBlobs, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, - test_da_checker, test_spec, + EphemeralHarnessType, NumBlobs, generate_rand_block_and_blobs, + generate_rand_block_and_data_columns, test_da_checker, test_spec, }; + use bls::Signature; use lighthouse_network::{ PeerId, service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, - DataColumnsByRangeRequestId, DataColumnsByRangeRequester, Id, RangeRequestId, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, Id, + PayloadEnvelopesByRangeRequestId, RangeRequestId, }, }; use std::{collections::HashMap, sync::Arc}; use tracing::Span; - use types::{Epoch, ForkName, MinimalEthSpec as E, SignedBeaconBlock}; + use types::{ + ChainSpec, DataColumnSidecarList, Epoch, ExecutionPayloadEnvelope, ForkName, + MinimalEthSpec as E, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + }; fn components_id() -> ComponentsByRangeRequestId { ComponentsByRangeRequestId { @@ -515,6 +594,15 @@ mod tests { } } + /// The custody-column coupling tests below build Fulu data-column sidecars directly, which is + /// incompatible with a Gloas genesis (Gloas columns have a different structure). Skip them when + /// `FORK_NAME` schedules Gloas at genesis. TODO(gloas): port the harness to build Gloas columns. + fn skip_under_gloas() -> bool { + test_spec::() + .fork_name_at_epoch(Epoch::new(0)) + .gloas_enabled() + } + fn blocks_id(parent_request_id: ComponentsByRangeRequestId) -> BlocksByRangeRequestId { BlocksByRangeRequestId { id: 1, @@ -529,6 +617,15 @@ mod tests { } } + fn payloads_id( + parent_request_id: ComponentsByRangeRequestId, + ) -> PayloadEnvelopesByRangeRequestId { + PayloadEnvelopesByRangeRequestId { + id: 1, + parent_request_id, + } + } + fn columns_id( id: Id, parent_request_id: DataColumnsByRangeRequester, @@ -546,8 +643,166 @@ mod tests { info.responses(da_checker, spec).is_some() } + fn gloas_spec() -> ChainSpec { + let mut spec = test_spec::(); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + spec + } + + fn matching_envelope(block: &SignedBeaconBlock) -> Arc> { + let bid = &block + .message() + .body() + .signed_execution_payload_bid() + .expect("Gloas block should have payload bid") + .message; + let mut envelope = SignedExecutionPayloadEnvelope { + message: ExecutionPayloadEnvelope::empty(), + signature: Signature::empty(), + }; + envelope.message.beacon_block_root = block.canonical_root(); + envelope.message.parent_beacon_block_root = block.parent_root(); + envelope.message.builder_index = bid.builder_index; + envelope.message.payload.slot_number = block.slot(); + envelope.message.payload.parent_hash = bid.parent_block_hash; + envelope.message.payload.block_hash = bid.block_hash; + Arc::new(envelope) + } + + #[allow(clippy::type_complexity)] + fn make_gloas_blocks_and_columns( + count: usize, + spec: &ChainSpec, + ) -> Vec<( + Arc>, + DataColumnSidecarList, + Arc>, + )> { + let mut u = types::test_utils::test_unstructured(); + (0..count) + .map(|_| { + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut u, + spec, + ) + .unwrap(); + let envelope = matching_envelope(&block); + (Arc::new(block), data_columns, envelope) + }) + .collect() + } + + #[allow(clippy::type_complexity)] + fn add_all_columns( + info: &mut RangeBlockComponentsRequest, + blocks: &[( + Arc>, + DataColumnSidecarList, + Arc>, + )], + columns_req_id: &[(DataColumnsByRangeRequestId, Vec)], + expected_custody_columns: &[u64], + ) { + for (i, &column_index) in expected_custody_columns.iter().enumerate() { + let (req, _columns) = columns_req_id.get(i).unwrap(); + info.add_custody_columns( + *req, + blocks + .iter() + .flat_map(|(_, columns, _)| { + columns + .iter() + .filter(|column| *column.index() == column_index) + .cloned() + }) + .collect(), + ) + .unwrap(); + } + } + + #[allow(clippy::type_complexity)] + struct GloasSetup { + info: RangeBlockComponentsRequest, + da_checker: Arc>>, + spec: Arc, + blocks: Vec<( + Arc>, + DataColumnSidecarList, + Arc>, + )>, + payloads_req_id: PayloadEnvelopesByRangeRequestId, + expected_custody_columns: Vec, + } + + /// Builds a Gloas coupling request with `count` blocks and all custody columns added, + /// ready for the per-test payload-envelope step. + fn setup_gloas_coupling(count: usize) -> GloasSetup { + let spec = Arc::new(gloas_spec()); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let expected_custody_columns = da_checker + .custody_context() + .sampling_columns_for_epoch(Epoch::new(0), &spec) + .to_vec(); + let blocks = make_gloas_blocks_and_columns(count, &spec); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let payloads_req_id = payloads_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Some(payloads_req_id), + Span::none(), + ); + + info.add_blocks( + blocks_req_id, + blocks.iter().map(|(block, _, _)| block.clone()).collect(), + ) + .unwrap(); + add_all_columns( + &mut info, + &blocks, + &columns_req_id, + &expected_custody_columns, + ); + + GloasSetup { + info, + da_checker, + spec, + blocks, + payloads_req_id, + expected_custody_columns, + } + } + #[test] fn no_blobs_into_responses() { + // This exercises the pre-Gloas blobs/no-data coupling path. Gloas coupling is covered + // by the dedicated `setup_gloas_coupling` tests below. + if skip_under_gloas() { + return; + } let spec = Arc::new(test_spec::()); let mut u = types::test_utils::test_unstructured(); @@ -566,7 +821,7 @@ mod tests { let blocks_req_id = blocks_id(components_id()); let mut info = - RangeBlockComponentsRequest::::new(blocks_req_id, None, None, Span::none()); + RangeBlockComponentsRequest::::new(blocks_req_id, None, None, None, Span::none()); // Send blocks and complete terminate response info.add_blocks(blocks_req_id, blocks).unwrap(); @@ -597,6 +852,7 @@ mod tests { blocks_req_id, Some(blobs_req_id), None, + None, Span::none(), ); @@ -607,15 +863,21 @@ mod tests { let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); + // Pin to pre-PeerDAS so this exercises the blob (not custody-column) path under any + // FORK_NAME. + spec.fulu_fork_epoch = None; let spec = Arc::new(spec); let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); - // Assert response is finished and RpcBlocks cannot be constructed, because blobs weren't returned. + // Blobs are no longer required for availability, so the response succeeds without them. let result = info.responses(da_checker, spec).unwrap(); - assert!(result.is_err()) + assert!(result.is_ok()) } #[test] fn rpc_block_with_custody_columns() { + if skip_under_gloas() { + return; + } let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0)); @@ -657,6 +919,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expects_custody_columns.clone())), + None, Span::none(), ); // Send blocks and complete terminate response @@ -694,6 +957,9 @@ mod tests { #[test] fn rpc_block_with_custody_columns_batched() { + if skip_under_gloas() { + return; + } let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0)); @@ -733,6 +999,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -786,8 +1053,120 @@ mod tests { info.responses(da_checker, spec).unwrap().unwrap(); } + #[test] + fn gloas_payload_envelopes_must_complete_before_responses() { + let GloasSetup { + mut info, + da_checker, + spec, + .. + } = setup_gloas_coupling(2); + + // No payload envelopes added yet, so the request must not be complete. + assert!(info.responses(da_checker, spec).is_none()); + } + + #[test] + fn gloas_payload_envelopes_are_coupled_by_block_root() { + let GloasSetup { + mut info, + da_checker, + spec, + blocks, + payloads_req_id, + expected_custody_columns, + } = setup_gloas_coupling(2); + + // Supply envelopes in reverse order to prove coupling is by block root, not position. + info.add_payload_envelopes( + payloads_req_id, + blocks + .iter() + .rev() + .map(|(_, _, envelope)| envelope.clone()) + .collect(), + ) + .unwrap(); + + let responses = info.responses(da_checker, spec).unwrap().unwrap(); + assert_eq!(responses.len(), blocks.len()); + for response in responses { + match response { + RangeSyncBlock::Gloas { + block, + envelope: Some(envelope), + } => { + assert_eq!( + envelope.envelope().beacon_block_root(), + block.canonical_root() + ); + assert_eq!(envelope.columns.len(), expected_custody_columns.len()); + } + other => panic!("expected Gloas block with envelope, got {other:?}"), + } + } + } + + #[test] + fn gloas_payload_envelopes_allow_missing_envelopes() { + let GloasSetup { + mut info, + da_checker, + spec, + blocks, + payloads_req_id, + .. + } = setup_gloas_coupling(2); + + // Supply an envelope for only one of the two blocks. + info.add_payload_envelopes(payloads_req_id, vec![blocks[0].2.clone()]) + .unwrap(); + + let responses = info.responses(da_checker, spec).unwrap().unwrap(); + let count_with = |with_envelope: bool| { + responses + .iter() + .filter(|response| { + matches!(response, RangeSyncBlock::Gloas { envelope, .. } if envelope.is_some() == with_envelope) + }) + .count() + }; + assert_eq!(count_with(true), 1); + assert_eq!(count_with(false), 1); + } + + #[test] + fn gloas_payload_envelope_mismatch_fails_coupling() { + let GloasSetup { + mut info, + da_checker, + spec, + blocks, + payloads_req_id, + .. + } = setup_gloas_coupling(1); + + let mut bad_envelope = (*blocks[0].2).clone(); + bad_envelope.message.payload.slot_number += 1; + info.add_payload_envelopes(payloads_req_id, vec![Arc::new(bad_envelope)]) + .unwrap(); + + let result = info.responses(da_checker, spec).unwrap(); + assert!( + matches!( + result, + Err(super::CouplingError::EnvelopePeerFailure(ref error)) + if error.contains("SlotMismatch") + ), + "expected envelope slot mismatch, got {result:?}" + ); + } + #[test] fn missing_custody_columns_from_faulty_peers() { + if skip_under_gloas() { + return; + } // GIVEN: A request expecting sampling columns from multiple peers let spec = Arc::new(test_spec::()); let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); @@ -827,6 +1206,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -883,6 +1263,9 @@ mod tests { #[test] fn retry_logic_after_peer_failures() { + if skip_under_gloas() { + return; + } // GIVEN: A request expecting sampling columns where some peers initially fail let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); @@ -925,6 +1308,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -999,6 +1383,9 @@ mod tests { #[test] fn max_retries_exceeded_behavior() { + if skip_under_gloas() { + return; + } // GIVEN: A request where peers consistently fail to provide required columns let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); @@ -1041,6 +1428,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 534e0bc7c8..8e7b8cd05a 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -40,18 +40,16 @@ use super::network_context::{ }; use super::peer_sync_info::{PeerSyncType, remote_sync_type}; use super::range_sync::{EPOCHS_PER_BATCH, RangeSync, RangeSyncType}; -use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; +use crate::network_beacon_processor::{ + BlockProcessingResult, ChainSegmentProcessId, NetworkBeaconProcessor, +}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; -use crate::sync::block_lookups::{ - BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, -}; +use crate::sync::block_lookups::{BlockComponent, DownloadResult}; use crate::sync::custody_backfill_sync::CustodyBackFillSync; use crate::sync::network_context::{PeerGroup, RpcResponseResult}; use beacon_chain::block_verification_types::AsBlock; -use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, EngineState, -}; +use beacon_chain::{BeaconChain, BeaconChainTypes, EngineState}; use futures::StreamExt; use lighthouse_network::SyncInfo; use lighthouse_network::rpc::RPCError; @@ -59,7 +57,8 @@ use lighthouse_network::service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, - DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, + DataColumnsByRootRequester, Id, PayloadEnvelopesByRangeRequestId, SingleLookupReqId, + SyncRequestId, }; use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::{PeerAction, PeerId}; @@ -73,8 +72,8 @@ use strum::IntoStaticStr; use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; use types::{ - BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, Slot, + BlobSidecar, DataColumnSidecar, EthSpec, ExecutionBlockHash, ForkContext, Hash256, + SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync @@ -144,11 +143,9 @@ pub enum SyncMessage { /// A block with an unknown parent has been received. UnknownParentBlock(PeerId, Arc>, Hash256), - /// A data column with an unknown parent has been received. - UnknownParentDataColumn(PeerId, Arc>), - - /// A partial data column with an unknown parent has been received. - UnknownParentPartialDataColumn { + /// A sidecar (full/partial data column) with an unknown parent has been received. Carries only the header + /// info needed to trigger a parent lookup, decoupled from the concrete sidecar type. + UnknownParentSidecarHeader { peer_id: PeerId, block_root: Hash256, parent_root: Hash256, @@ -186,18 +183,12 @@ pub enum SyncMessage { process_type: BlockProcessType, result: BlockProcessingResult, }, - - /// A gossip-received component has completed processing and the block may now be imported. - /// In Fulu this is sent after block or blob processing. In Gloas this is also sent after - /// data column or payload envelope processing triggers availability. - GossipBlockProcessResult { block_root: Hash256, imported: bool }, } /// The type of processing specified for a received block. #[derive(Debug, Clone)] pub enum BlockProcessType { SingleBlock { id: Id }, - SingleBlob { id: Id }, SingleCustodyColumn(Id), SinglePayloadEnvelope(Id), } @@ -206,20 +197,12 @@ impl BlockProcessType { pub fn id(&self) -> Id { match self { BlockProcessType::SingleBlock { id } - | BlockProcessType::SingleBlob { id } | BlockProcessType::SingleCustodyColumn(id) | BlockProcessType::SinglePayloadEnvelope(id) => *id, } } } -#[derive(Debug)] -pub enum BlockProcessingResult { - Ok(AvailabilityProcessingStatus), - Err(BlockError), - Ignored, -} - /// The result of processing multiple blocks (a chain segment). #[derive(Debug)] pub enum BatchProcessResult { @@ -507,9 +490,6 @@ impl SyncManager { SyncRequestId::SingleBlock { id } => { self.on_single_block_response(id, peer_id, RpcEvent::RPCError(error)) } - SyncRequestId::SingleBlob { id } => { - self.on_single_blob_response(id, peer_id, RpcEvent::RPCError(error)) - } SyncRequestId::SinglePayloadEnvelope { id } => { self.on_single_payload_envelope_response(id, peer_id, RpcEvent::RPCError(error)) } @@ -525,6 +505,8 @@ impl SyncManager { SyncRequestId::DataColumnsByRange(req_id) => { self.on_data_columns_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)) } + SyncRequestId::PayloadEnvelopesByRange(req_id) => self + .on_payload_envelopes_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)), } } @@ -873,72 +855,37 @@ impl SyncManager { SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); + let parent_block_hash = block.payload_bid_parent_block_hash().ok(); debug!(%block_root, %parent_root, "Received unknown parent block message"); self.handle_unknown_parent( peer_id, block_root, parent_root, + parent_block_hash, block_slot, BlockComponent::Block(DownloadResult { value: block.block_cloned(), - block_root, seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), peer_group: PeerGroup::from_single(peer_id), }), ); } - SyncMessage::UnknownParentDataColumn(peer_id, data_column) => { - let data_column_slot = data_column.slot(); - let block_root = data_column.block_root(); - match data_column.as_ref() { - DataColumnSidecar::Fulu(column) => { - let parent_root = column.block_parent_root(); - debug!(%block_root, %parent_root, "Received unknown parent data column message"); - self.handle_unknown_parent( - peer_id, - block_root, - parent_root, - data_column_slot, - BlockComponent::DataColumn(DownloadResult { - value: parent_root, - block_root, - seen_timestamp: self - .chain - .slot_clock - .now_duration() - .unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), - ); - } - DataColumnSidecar::Gloas(_) => { - // TODO(gloas): proper lookup sync for Gloas. Routing into - // `handle_unknown_block_root` here mixes column processing with the - // single-block-lookup path; the Gloas column-arrives-before-block - // case wants its own queue/wakeup. - debug!(%block_root, "Received unknown block data column message"); - self.handle_unknown_block_root(peer_id, block_root); - } - } - } - SyncMessage::UnknownParentPartialDataColumn { + SyncMessage::UnknownParentSidecarHeader { peer_id, block_root, parent_root, slot, } => { - debug!(%block_root, %parent_root, "Received unknown parent partial column message"); + debug!(%block_root, %parent_root, "Received unknown parent sidecar header message"); self.handle_unknown_parent( peer_id, block_root, parent_root, + // The event `UnknownParentSidecarHeader` only fires for pre-Gloas data + // structues, so the bid parent hash is None. + None, slot, - BlockComponent::PartialDataColumn(DownloadResult { - value: parent_root, - block_root, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), + BlockComponent::Sidecar, ); } SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_root) => { @@ -963,14 +910,6 @@ impl SyncManager { } => self .block_lookups .on_processing_result(process_type, result, &mut self.network), - SyncMessage::GossipBlockProcessResult { - block_root, - imported, - } => self.block_lookups.on_external_processing_result( - block_root, - imported, - &mut self.network, - ), SyncMessage::BatchProcessed { sync_type, result } => match sync_type { ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { self.range_sync.handle_block_process_result( @@ -1020,6 +959,7 @@ impl SyncManager { peer_id: PeerId, block_root: Hash256, parent_root: Hash256, + parent_block_hash: Option, slot: Slot, block_component: BlockComponent, ) { @@ -1028,6 +968,8 @@ impl SyncManager { if self.block_lookups.search_child_and_parent( block_root, block_component, + parent_root, + parent_block_hash, peer_id, &mut self.network, ) { @@ -1178,14 +1120,13 @@ impl SyncManager { block: RpcEvent>>, ) { if let Some(resp) = self.network.on_single_block_response(id, peer_id, block) { - self.block_lookups - .on_download_response::>( - id, - resp.map(|(value, seen_timestamp)| { - (value, PeerGroup::from_single(peer_id), seen_timestamp) - }), - &mut self.network, - ) + self.block_lookups.on_block_download_response( + id, + resp.map(|(value, seen_timestamp)| { + DownloadResult::new(value, PeerGroup::from_single(peer_id), seen_timestamp) + }), + &mut self.network, + ) } } @@ -1197,11 +1138,6 @@ impl SyncManager { seen_timestamp: Duration, ) { match sync_request_id { - SyncRequestId::SingleBlob { id } => self.on_single_blob_response( - id, - peer_id, - RpcEvent::from_chunk(blob, seen_timestamp), - ), SyncRequestId::BlobsByRange(id) => self.on_blobs_by_range_response( id, peer_id, @@ -1213,7 +1149,6 @@ impl SyncManager { } } - // TODO(gloas): dispatch into block_lookups once the envelope lookup state machine lands. fn rpc_payload_envelope_received( &mut self, sync_request_id: SyncRequestId, @@ -1228,6 +1163,13 @@ impl SyncManager { peer_id, RpcEvent::from_chunk(envelope, seen_timestamp), ), + SyncRequestId::PayloadEnvelopesByRange(req_id) => { + self.on_payload_envelopes_by_range_response( + req_id, + peer_id, + RpcEvent::from_chunk(envelope, seen_timestamp), + ); + } _ => { crit!(%peer_id, "bad request id for payload envelope"); } @@ -1268,31 +1210,35 @@ impl SyncManager { peer_id: PeerId, envelope: RpcEvent>>, ) { - if let Some(_resp) = self + if let Some(resp) = self .network .on_single_payload_envelope_response(id, peer_id, envelope) { - // TODO(gloas): dispatch into - // `block_lookups.on_download_response::>(...)` once - // the envelope lookup state machine lands. + self.block_lookups.on_payload_download_response( + id, + resp.map(|(value, seen_timestamp)| { + DownloadResult::new(value, PeerGroup::from_single(peer_id), seen_timestamp) + }), + &mut self.network, + ) } } - fn on_single_blob_response( + fn on_payload_envelopes_by_range_response( &mut self, - id: SingleLookupReqId, + id: PayloadEnvelopesByRangeRequestId, peer_id: PeerId, - blob: RpcEvent>>, + envelope: RpcEvent>>, ) { - if let Some(resp) = self.network.on_single_blob_response(id, peer_id, blob) { - self.block_lookups - .on_download_response::>( - id, - resp.map(|(value, seen_timestamp)| { - (value, PeerGroup::from_single(peer_id), seen_timestamp) - }), - &mut self.network, - ) + if let Some(resp) = self + .network + .on_payload_envelopes_by_range_response(id, peer_id, envelope) + { + self.on_range_components_response( + id.parent_request_id, + peer_id, + RangeBlockComponent::PayloadEnvelope(id, resp), + ); } } @@ -1384,11 +1330,7 @@ impl SyncManager { response: CustodyByRootResult, ) { self.block_lookups - .on_download_response::>( - requester.0, - response, - &mut self.network, - ); + .on_custody_download_response(requester.0, response, &mut self.network); } /// Handles receiving a response for a range sync request that should have both blocks and @@ -1495,18 +1437,3 @@ impl SyncManager { } } } - -impl From> for BlockProcessingResult { - fn from(result: Result) -> Self { - match result { - Ok(status) => BlockProcessingResult::Ok(status), - Err(e) => BlockProcessingResult::Err(e), - } - } -} - -impl From for BlockProcessingResult { - fn from(e: BlockError) -> Self { - BlockProcessingResult::Err(e) - } -} diff --git a/beacon_node/network/src/sync/mod.rs b/beacon_node/network/src/sync/mod.rs index 054bab654c..f121c1f1b7 100644 --- a/beacon_node/network/src/sync/mod.rs +++ b/beacon_node/network/src/sync/mod.rs @@ -15,4 +15,5 @@ mod range_sync; mod tests; pub use manager::{BatchProcessResult, SyncMessage}; +pub use network_context::{PeerGroup, SyncNetworkContext}; pub use range_sync::ChainId; diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 9d5ac40c0a..8e8abd4fa6 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -16,31 +16,33 @@ use crate::network_beacon_processor::TestBeaconChainType; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::batch::ByRangeRequestType; -use crate::sync::block_lookups::SingleLookupId; +use crate::sync::block_lookups::{DownloadResult, SingleLookupId}; use crate::sync::block_sidecar_coupling::CouplingError; -use crate::sync::network_context::requests::BlobsByRootSingleBlockRequest; use crate::sync::range_data_column_batch_request::RangeDataColumnBatchRequest; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; use fnv::FnvHashMap; -use lighthouse_network::rpc::methods::{BlobsByRangeRequest, DataColumnsByRangeRequest}; +use lighthouse_network::rpc::methods::{ + BlobsByRangeRequest, DataColumnsByRangeRequest, PayloadEnvelopesByRangeRequest, +}; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError, RequestType}; pub use lighthouse_network::service::api_types::RangeRequestId; use lighthouse_network::service::api_types::{ AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, - DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, + DataColumnsByRootRequester, Id, PayloadEnvelopesByRangeRequestId, SingleLookupReqId, + SyncRequestId, }; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; use parking_lot::RwLock; pub use requests::LookupVerifyError; use requests::{ - ActiveRequests, BlobsByRangeRequestItems, BlobsByRootRequestItems, BlocksByRangeRequestItems, - BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, - PayloadEnvelopesByRootRequestItems, + ActiveRequests, BlobsByRangeRequestItems, BlocksByRangeRequestItems, BlocksByRootRequestItems, + DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, + PayloadEnvelopesByRangeRequestItems, PayloadEnvelopesByRootRequestItems, }; #[cfg(test)] use slot_clock::SlotClock; @@ -53,9 +55,8 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Span, debug, debug_span, error, warn}; -use types::data::FixedBlobSidecarList; use types::{ - BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, + BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkContext, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; @@ -97,9 +98,10 @@ pub type RpcResponseResult = Result<(T, Duration), RpcResponseError>; /// Duration = latest seen timestamp of all received data columns pub type CustodyByRootResult = - Result<(DataColumnSidecarList, PeerGroup, Duration), RpcResponseError>; + Result>, RpcResponseError>; #[derive(Debug)] +#[allow(private_interfaces)] pub enum RpcResponseError { RpcError(#[allow(dead_code)] RPCError), VerifyError(LookupVerifyError), @@ -178,13 +180,13 @@ impl PeerGroup { /// Sequential ID that uniquely identifies ReqResp outgoing requests pub type ReqId = u32; -pub enum LookupRequestResult { +pub enum LookupRequestResult { /// A request is sent. Sync MUST receive an event from the network in the future for either: /// completed response or failed request RequestSent(I), /// No request is sent, and no further action is necessary to consider this request completed. /// Includes a reason why this request is not needed. - NoRequestNeeded(&'static str), + NoRequestNeeded(&'static str, T), /// No request is sent, but the request is not completed. Sync MUST receive some future event /// that makes progress on the request. For example: request is processing from a different /// source (i.e. block received from gossip) and sync MUST receive an event with that processing @@ -203,8 +205,6 @@ pub struct SyncNetworkContext { /// A mapping of active BlocksByRoot requests, including both current slot and parent lookups. blocks_by_root_requests: ActiveRequests>, - /// A mapping of active BlobsByRoot requests, including both current slot and parent lookups. - blobs_by_root_requests: ActiveRequests>, /// A mapping of active PayloadEnvelopesByRoot requests payload_envelopes_by_root_requests: ActiveRequests>, @@ -220,6 +220,11 @@ pub struct SyncNetworkContext { /// A mapping of active DataColumnsByRange requests data_columns_by_range_requests: ActiveRequests>, + /// A mapping of active PayloadEnvelopesByRange requests + payload_envelopes_by_range_requests: ActiveRequests< + PayloadEnvelopesByRangeRequestId, + PayloadEnvelopesByRangeRequestItems, + >, /// Mapping of active custody column requests for a block root custody_by_root_requests: FnvHashMap>, @@ -257,6 +262,10 @@ pub enum RangeBlockComponent { DataColumnsByRangeRequestId, RpcResponseResult>>>, ), + PayloadEnvelope( + PayloadEnvelopesByRangeRequestId, + RpcResponseResult>>>, + ), } #[cfg(test)] @@ -300,12 +309,12 @@ impl SyncNetworkContext { execution_engine_state: EngineState::Online, // always assume `Online` at the start request_id: 1, blocks_by_root_requests: ActiveRequests::new("blocks_by_root"), - blobs_by_root_requests: ActiveRequests::new("blobs_by_root"), payload_envelopes_by_root_requests: ActiveRequests::new("payload_envelopes_by_root"), data_columns_by_root_requests: ActiveRequests::new("data_columns_by_root"), blocks_by_range_requests: ActiveRequests::new("blocks_by_range"), blobs_by_range_requests: ActiveRequests::new("blobs_by_range"), data_columns_by_range_requests: ActiveRequests::new("data_columns_by_range"), + payload_envelopes_by_range_requests: ActiveRequests::new("payload_envelopes_by_range"), custody_by_root_requests: <_>::default(), components_by_range_requests: FnvHashMap::default(), custody_backfill_data_column_batch_requests: FnvHashMap::default(), @@ -315,6 +324,10 @@ impl SyncNetworkContext { } } + pub fn spec(&self) -> &ChainSpec { + &self.chain.spec + } + pub fn send_sync_message(&mut self, sync_message: SyncMessage) { self.network_beacon_processor .send_sync_message(sync_message); @@ -329,12 +342,12 @@ impl SyncNetworkContext { network_send: _, request_id: _, blocks_by_root_requests, - blobs_by_root_requests, payload_envelopes_by_root_requests, data_columns_by_root_requests, blocks_by_range_requests, blobs_by_range_requests, data_columns_by_range_requests, + payload_envelopes_by_range_requests, // custody_by_root_requests is a meta request of data_columns_by_root_requests custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests @@ -350,10 +363,6 @@ impl SyncNetworkContext { .active_requests_of_peer(peer_id) .into_iter() .map(|id| SyncRequestId::SingleBlock { id: *id }); - let blobs_by_root_ids = blobs_by_root_requests - .active_requests_of_peer(peer_id) - .into_iter() - .map(|id| SyncRequestId::SingleBlob { id: *id }); let payload_envelopes_by_root_ids = payload_envelopes_by_root_requests .active_requests_of_peer(peer_id) .into_iter() @@ -374,13 +383,17 @@ impl SyncNetworkContext { .active_requests_of_peer(peer_id) .into_iter() .map(|req_id| SyncRequestId::DataColumnsByRange(*req_id)); + let payload_envelope_by_range_ids = payload_envelopes_by_range_requests + .active_requests_of_peer(peer_id) + .into_iter() + .map(|req_id| SyncRequestId::PayloadEnvelopesByRange(*req_id)); blocks_by_root_ids - .chain(blobs_by_root_ids) .chain(payload_envelopes_by_root_ids) .chain(data_column_by_root_ids) .chain(blocks_by_range_ids) .chain(blobs_by_range_ids) .chain(data_column_by_range_ids) + .chain(payload_envelope_by_range_ids) .collect() } @@ -432,12 +445,12 @@ impl SyncNetworkContext { network_send: _, request_id: _, blocks_by_root_requests, - blobs_by_root_requests, payload_envelopes_by_root_requests, data_columns_by_root_requests, blocks_by_range_requests, blobs_by_range_requests, data_columns_by_range_requests, + payload_envelopes_by_range_requests, // custody_by_root_requests is a meta request of data_columns_by_root_requests custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests @@ -455,12 +468,12 @@ impl SyncNetworkContext { for peer_id in blocks_by_root_requests .iter_request_peers() - .chain(blobs_by_root_requests.iter_request_peers()) .chain(payload_envelopes_by_root_requests.iter_request_peers()) .chain(data_columns_by_root_requests.iter_request_peers()) .chain(blocks_by_range_requests.iter_request_peers()) .chain(blobs_by_range_requests.iter_request_peers()) .chain(data_columns_by_range_requests.iter_request_peers()) + .chain(payload_envelopes_by_range_requests.iter_request_peers()) { *active_request_count_by_peer.entry(peer_id).or_default() += 1; } @@ -593,24 +606,26 @@ impl SyncNetworkContext { }; // Attempt to find all required custody peers before sending any request or creating an ID - let columns_by_range_peers_to_request = - if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { - let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); - let column_indexes = self - .chain - .sampling_columns_for_epoch(epoch) - .iter() - .cloned() - .collect(); - Some(self.select_columns_by_range_peers_to_request( - &column_indexes, - column_peers, - active_request_count_by_peer, - peers_to_deprioritize, - )?) - } else { - None - }; + let columns_by_range_peers_to_request = if matches!( + batch_type, + ByRangeRequestType::BlocksAndColumns | ByRangeRequestType::BlocksAndEnvelopesAndColumns + ) { + let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); + let column_indexes = self + .chain + .sampling_columns_for_epoch(epoch) + .iter() + .cloned() + .collect(); + Some(self.select_columns_by_range_peers_to_request( + &column_indexes, + column_peers, + active_request_count_by_peer, + peers_to_deprioritize, + )?) + } else { + None + }; // Create the overall components_by_range request ID before its individual components let id = ComponentsByRangeRequestId { @@ -674,6 +689,26 @@ impl SyncNetworkContext { }) .transpose()?; + let payloads_req_id = + if matches!(batch_type, ByRangeRequestType::BlocksAndEnvelopesAndColumns) { + Some(self.send_payload_envelopes_by_range_request( + block_peer, + PayloadEnvelopesByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + }, + id, + new_range_request_span!( + self, + "outgoing_envelopes_by_range", + range_request_span.clone(), + block_peer + ), + )?) + } else { + None + }; + let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); let info = RangeBlockComponentsRequest::new( blocks_req_id, @@ -684,6 +719,7 @@ impl SyncNetworkContext { self.chain.sampling_columns_for_epoch(epoch).to_vec(), ) }), + payloads_req_id, range_request_span, ); self.components_by_range_requests.insert(id, info); @@ -786,6 +822,17 @@ impl SyncNetworkContext { }) }) } + RangeBlockComponent::PayloadEnvelope(req_id, resp) => { + resp.and_then(|(envelopes, _)| { + request + .add_payload_envelopes(req_id, envelopes) + .map_err(|e| { + RpcResponseError::BlockComponentCouplingError( + CouplingError::InternalError(e), + ) + }) + }) + } } } { entry.remove(); @@ -833,7 +880,7 @@ impl SyncNetworkContext { lookup_id: SingleLookupId, lookup_peers: Arc>>, block_root: Hash256, - ) -> Result { + ) -> Result>>, RpcRequestSendError> { let active_request_count_by_peer = self.active_request_count_by_peer(); let Some(peer_id) = lookup_peers .read() @@ -862,31 +909,21 @@ impl SyncNetworkContext { match self.chain.get_block_process_status(&block_root) { // Unknown block, continue request to download BlockProcessStatus::Unknown => {} - // Block is known and currently processing. Imports from gossip and HTTP API insert the - // block in the da_cache. However, HTTP API is unable to notify sync when it completes - // block import. Returning `Pending` here will result in stuck lookups if the block is - // importing from sync. - BlockProcessStatus::NotValidated(_, source) => match source { - BlockImportSource::Gossip => { - // Lookup sync event safety: If the block is currently in the processing cache, we - // are guaranteed to receive a `SyncMessage::GossipBlockProcessResult` that will - // make progress on this lookup - return Ok(LookupRequestResult::Pending("block in processing cache")); - } - BlockImportSource::Lookup - | BlockImportSource::RangeSync - | BlockImportSource::HttpApi => { - // Lookup, RangeSync or HttpApi block import don't emit the GossipBlockProcessResult - // event. If a lookup happens to be created during block import from one of - // those sources just import the block twice. Otherwise the lookup will get - // stuck. Double imports are fine, they just waste resources. - } - }, + // Block is known but processing. The block may turn out to be invalid, so we want sync to + // NOT mark the request as complete yet. The ideal flow would be: + // - Wait for processing to complete + // - Only if there is an error re-download and re-process + // But implementing this introduces complexity and the risk for the lookup to get stuck. + // Instead we always re-download the block eagerly and de-duplicate the processing. So in + // the happy case we just download the block again if the lookup is created while execution + // processing the block. + BlockProcessStatus::NotValidated(..) => {} // Block is fully validated. If it's not yet imported it's waiting for missing block // components. Consider this request completed and do nothing. - BlockProcessStatus::ExecutionValidated { .. } => { + BlockProcessStatus::ExecutionValidated(block) => { return Ok(LookupRequestResult::NoRequestNeeded( "block execution validated", + block, )); } } @@ -944,18 +981,23 @@ impl SyncNetworkContext { } /// Request a payload envelope for a block root via PayloadEnvelopesByRoot RPC. - #[allow(dead_code)] pub fn payload_lookup_request( &mut self, lookup_id: SingleLookupId, lookup_peers: Arc>>, block_root: Hash256, - ) -> Result { + ) -> Result< + LookupRequestResult>>, + RpcRequestSendError, + > { // Skip the download if fork-choice already saw this envelope (e.g. imported via gossip - // before the lookup got here). - if self.chain.envelope_is_known_to_fork_choice(&block_root) { + // before the lookup got here). Return the cached envelope so the request completes. + if self.chain.envelope_is_known_to_fork_choice(&block_root) + && let Ok(Some(envelope)) = self.chain.get_payload_envelope(&block_root) + { return Ok(LookupRequestResult::NoRequestNeeded( "envelope already known to fork-choice", + Arc::new(envelope), )); } @@ -1017,109 +1059,6 @@ impl SyncNetworkContext { Ok(LookupRequestResult::RequestSent(id.req_id)) } - - /// Request necessary blobs for `block_root`. Requests only the necessary blobs by checking: - /// - If we have a downloaded but not yet processed block - /// - If the da_checker has a pending block - /// - If the da_checker has pending blobs from gossip - /// - /// Returns false if no request was made, because we don't need to import (more) blobs. - pub fn blob_lookup_request( - &mut self, - lookup_id: SingleLookupId, - lookup_peers: Arc>>, - block_root: Hash256, - expected_blobs: usize, - ) -> Result { - let active_request_count_by_peer = self.active_request_count_by_peer(); - let Some(peer_id) = lookup_peers - .read() - .iter() - .map(|peer| { - ( - // Prefer peers with less overall requests - active_request_count_by_peer.get(peer).copied().unwrap_or(0), - // Random factor to break ties, otherwise the PeerID breaks ties - rand::random::(), - peer, - ) - }) - .min() - .map(|(_, _, peer)| *peer) - else { - // Allow lookup to not have any peers and do nothing. This is an optimization to not - // lose progress of lookups created from a block with unknown parent before we receive - // attestations for said block. - // Lookup sync event safety: If a lookup requires peers to make progress, and does - // not receive any new peers for some time it will be dropped. If it receives a new - // peer it must attempt to make progress. - return Ok(LookupRequestResult::Pending("no peers")); - }; - - let imported_blob_indexes = self - .chain - .data_availability_checker - .cached_blob_indexes(&block_root) - .unwrap_or_default(); - // Include only the blob indexes not yet imported (received through gossip) - let indices = (0..expected_blobs as u64) - .filter(|index| !imported_blob_indexes.contains(index)) - .collect::>(); - - if indices.is_empty() { - // No blobs required, do not issue any request - return Ok(LookupRequestResult::NoRequestNeeded("no indices to fetch")); - } - - let id = SingleLookupReqId { - lookup_id, - req_id: self.next_id(), - }; - - let request = BlobsByRootSingleBlockRequest { - block_root, - indices: indices.clone(), - }; - - // Lookup sync event safety: Refer to `Self::block_lookup_request` `network_send.send` call - let network_request = RequestType::BlobsByRoot( - request - .clone() - .into_request(&self.fork_context) - .map_err(RpcRequestSendError::InternalError)?, - ); - self.network_send - .send(NetworkMessage::SendRequest { - peer_id, - request: network_request, - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), - }) - .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; - - debug!( - method = "BlobsByRoot", - ?block_root, - blob_indices = ?indices, - peer = %peer_id, - %id, - "Sync RPC request sent" - ); - - self.blobs_by_root_requests.insert( - id, - peer_id, - // true = enforce max_requests are returned for blobs_by_root. We only issue requests for - // blocks after we know the block has data, and only request peers after they claim to - // have imported the block+blobs. - true, - BlobsByRootRequestItems::new(request), - // Not implemented - Span::none(), - ); - - Ok(LookupRequestResult::RequestSent(id.req_id)) - } - /// Request to send a single `data_columns_by_root` request to the network. pub fn data_column_lookup_request( &mut self, @@ -1127,7 +1066,7 @@ impl SyncNetworkContext { peer_id: PeerId, request: DataColumnsByRootSingleBlockRequest, expect_max_responses: bool, - ) -> Result, &'static str> { + ) -> Result, &'static str> { let id = DataColumnsByRootRequestId { id: self.next_id(), requester, @@ -1174,34 +1113,25 @@ impl SyncNetworkContext { &mut self, lookup_id: SingleLookupId, block_root: Hash256, + block_slot: Slot, lookup_peers: Arc>>, - ) -> Result { - let slot = self - .chain - .canonical_head - .fork_choice_read_lock() - .get_block(&block_root) - .map(|block| block.slot) - .or_else(|| self.chain.slot().ok()) - .ok_or_else(|| { - RpcRequestSendError::InternalError(format!( - "Unable to determine slot for block {block_root:?}" - )) - })?; + ) -> Result>, RpcRequestSendError> { + // Code below will issue column requests even if `lookup_peers` is empty. This is not okay, + // as we want to have at least one signal that some of our peers has already seen the + // block's data. + if lookup_peers.read().is_empty() { + return Ok(LookupRequestResult::Pending("no peers")); + } let custody_indexes_imported = self .chain - .cached_data_column_indexes(&block_root, slot) + .cached_data_column_indexes(&block_root, block_slot) .unwrap_or_default(); - let current_epoch = self.chain.epoch().map_err(|e| { - RpcRequestSendError::InternalError(format!("Unable to read slot clock {:?}", e)) - })?; - // Include only the blob indexes not yet imported (received through gossip) let mut custody_indexes_to_fetch = self .chain - .sampling_columns_for_epoch(current_epoch) + .sampling_columns_for_epoch(block_slot.epoch(T::EthSpec::slots_per_epoch())) .iter() .copied() .filter(|index| !custody_indexes_imported.contains(index)) @@ -1210,7 +1140,10 @@ impl SyncNetworkContext { if custody_indexes_to_fetch.is_empty() { // No indexes required, do not issue any request - return Ok(LookupRequestResult::NoRequestNeeded("no indices to fetch")); + return Ok(LookupRequestResult::NoRequestNeeded( + "no indices to fetch", + vec![], + )); } let id = SingleLookupReqId { @@ -1391,6 +1324,43 @@ impl SyncNetworkContext { Ok((id, requested_columns)) } + fn send_payload_envelopes_by_range_request( + &mut self, + peer_id: PeerId, + request: PayloadEnvelopesByRangeRequest, + parent_request_id: ComponentsByRangeRequestId, + request_span: Span, + ) -> Result { + let id = PayloadEnvelopesByRangeRequestId { + id: self.next_id(), + parent_request_id, + }; + + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: RequestType::PayloadEnvelopesByRange(request.clone()), + app_request_id: AppRequestId::Sync(SyncRequestId::PayloadEnvelopesByRange(id)), + }) + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; + + debug!( + method = "PayloadEnvelopesByRange", + slots = request.count, + peer = %peer_id, + %id, + "Sync RPC request sent" + ); + + self.payload_envelopes_by_range_requests.insert( + id, + peer_id, + false, + PayloadEnvelopesByRangeRequestItems::new(request), + request_span, + ); + Ok(id) + } + pub fn is_execution_engine_online(&self) -> bool { self.execution_engine_state == EngineState::Online } @@ -1471,7 +1441,10 @@ impl SyncNetworkContext { "To deal with alignment with deneb boundaries, batches need to be of just one epoch" ); - if self + if self.chain.spec.fork_name_at_epoch(epoch).gloas_enabled() { + // TODO(gloas): Not precise and we can be post-gloas and not require columns + ByRangeRequestType::BlocksAndEnvelopesAndColumns + } else if self .chain .data_availability_checker .data_columns_required_for_epoch(epoch) @@ -1538,35 +1511,6 @@ impl SyncNetworkContext { self.on_rpc_response_result(resp, peer_id) } - pub(crate) fn on_single_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - rpc_event: RpcEvent>>, - ) -> Option>> { - let resp = self.blobs_by_root_requests.on_response(id, rpc_event); - let resp = resp.map(|res| { - res.and_then(|(blobs, seen_timestamp)| { - if let Some(max_len) = blobs - .first() - .map(|blob| self.chain.spec.max_blobs_per_block(blob.epoch()) as usize) - { - match to_fixed_blob_sidecar_list(blobs, max_len) { - Ok(blobs) => Ok((blobs, seen_timestamp)), - Err(e) => Err(e.into()), - } - } else { - Err(RpcResponseError::VerifyError( - LookupVerifyError::InternalError( - "Requested blobs for a block that has no blobs".to_string(), - ), - )) - } - }) - }); - self.on_rpc_response_result(resp, peer_id) - } - pub(crate) fn on_single_payload_envelope_response( &mut self, id: SingleLookupReqId, @@ -1636,6 +1580,19 @@ impl SyncNetworkContext { self.on_rpc_response_result(resp, peer_id) } + #[allow(clippy::type_complexity)] + pub(crate) fn on_payload_envelopes_by_range_response( + &mut self, + id: PayloadEnvelopesByRangeRequestId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) -> Option>>>> { + let resp = self + .payload_envelopes_by_range_requests + .on_response(id, rpc_event); + self.on_rpc_response_result(resp, peer_id) + } + /// Common handler for consistent scoring of RpcResponseError fn on_rpc_response_result( &mut self, @@ -1689,8 +1646,8 @@ impl SyncNetworkContext { // Convert a result from internal format of `ActiveCustodyRequest` (error first to use ?) to // an Option first to use in an `if let Some() { act on result }` block. match result.as_ref() { - Some(Ok((columns, peer_group, _))) => { - debug!(?id, count = columns.len(), peers = ?peer_group, "Custody request success, removing") + Some(Ok(data)) => { + debug!(?id, count = data.value.len(), peers = ?data.peer_group, "Custody request success, removing") } Some(Err(e)) => { debug!(?id, error = ?e, "Custody request failure, removing" ) @@ -1734,37 +1691,6 @@ impl SyncNetworkContext { }) } - pub fn send_blobs_for_processing( - &self, - id: Id, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - ) -> Result<(), SendErrorProcessor> { - let beacon_processor = self - .beacon_processor_if_enabled() - .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; - - debug!(?block_root, ?id, "Sending blobs for processing"); - // Lookup sync event safety: If `beacon_processor.send_rpc_blobs` returns Ok() sync - // must receive a single `SyncMessage::BlockComponentProcessed` event with this process type - beacon_processor - .send_rpc_blobs( - block_root, - blobs, - seen_timestamp, - BlockProcessType::SingleBlob { id }, - ) - .map_err(|e| { - error!( - error = ?e, - "Failed to send sync blobs to processor" - ); - SendErrorProcessor::SendError - }) - } - - #[allow(dead_code)] pub fn send_payload_for_processing( &self, block_root: Hash256, @@ -1930,7 +1856,6 @@ impl SyncNetworkContext { pub(crate) fn register_metrics(&self) { for (id, count) in [ ("blocks_by_root", self.blocks_by_root_requests.len()), - ("blobs_by_root", self.blobs_by_root_requests.len()), ( "data_columns_by_root", self.data_columns_by_root_requests.len(), @@ -1951,17 +1876,3 @@ impl SyncNetworkContext { } } } - -fn to_fixed_blob_sidecar_list( - blobs: Vec>>, - max_len: usize, -) -> Result, LookupVerifyError> { - let mut fixed_list = FixedBlobSidecarList::new(vec![None; max_len]); - for blob in blobs.into_iter() { - let index = blob.index as usize; - *fixed_list - .get_mut(index) - .ok_or(LookupVerifyError::UnrequestedIndex(index as u64))? = Some(blob) - } - Ok(fixed_list) -} diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 2b96800e37..b1a4b52867 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,3 +1,4 @@ +use crate::sync::block_lookups::DownloadResult; use crate::sync::network_context::{ DataColumnsByRootRequestId, DataColumnsByRootSingleBlockRequest, }; @@ -56,8 +57,7 @@ struct ActiveBatchColumnsRequest { span: Span, } -pub type CustodyRequestResult = - Result, PeerGroup, Duration)>, Error>; +pub type CustodyRequestResult = Result>>, Error>; impl ActiveCustodyRequest { pub(crate) fn new( @@ -227,7 +227,11 @@ impl ActiveCustodyRequest { .into_iter() .max() .unwrap_or_else(|| cx.chain.slot_clock.now_duration().unwrap_or_default()); - return Ok(Some((columns, peer_group, max_seen_timestamp))); + return Ok(Some(DownloadResult::new( + columns, + peer_group, + max_seen_timestamp, + ))); } let active_request_count_by_peer = cx.active_request_count_by_peer(); @@ -306,11 +310,10 @@ impl ActiveCustodyRequest { // and downscore if data_columns_by_root does not return the expected custody // columns. For the rest of peers, don't downscore if columns are missing. // - // Post-Gloas, blocks and payload envelopes are decoupled. A peer may - // have the block but not yet imported the envelope and data columns. - // Don't enforce max_responses in this case. - lookup_peers.contains(&peer_id) - && !cx.fork_context.current_fork_name().gloas_enabled(), + // Post-Gloas the lookup peer set is the `gloas_child_peers`: peers that imported + // a FULL child, which requires the parent's columns. They provably custody the + // columns, so withholding is penalizable just like pre-Gloas. + lookup_peers.contains(&peer_id), ) .map_err(Error::SendFailed)?; @@ -343,7 +346,7 @@ impl ActiveCustodyRequest { }, ); } - LookupRequestResult::NoRequestNeeded(_) => unreachable!(), + LookupRequestResult::NoRequestNeeded(..) => unreachable!(), LookupRequestResult::Pending(_) => unreachable!(), } } diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 8c091eca80..cc74785098 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -9,13 +9,13 @@ use tracing::{Span, debug}; use types::{Hash256, Slot}; pub use blobs_by_range::BlobsByRangeRequestItems; -pub use blobs_by_root::{BlobsByRootRequestItems, BlobsByRootSingleBlockRequest}; pub use blocks_by_range::BlocksByRangeRequestItems; pub use blocks_by_root::{BlocksByRootRequestItems, BlocksByRootSingleRequest}; pub use data_columns_by_range::DataColumnsByRangeRequestItems; pub use data_columns_by_root::{ DataColumnsByRootRequestItems, DataColumnsByRootSingleBlockRequest, }; +pub use payload_envelopes_by_range::PayloadEnvelopesByRangeRequestItems; pub use payload_envelopes_by_root::{ PayloadEnvelopesByRootRequestItems, PayloadEnvelopesByRootSingleRequest, }; @@ -25,11 +25,11 @@ use crate::metrics; use super::{RpcEvent, RpcResponseError, RpcResponseResult}; mod blobs_by_range; -mod blobs_by_root; mod blocks_by_range; mod blocks_by_root; mod data_columns_by_range; mod data_columns_by_root; +mod payload_envelopes_by_range; mod payload_envelopes_by_root; #[derive(Debug, PartialEq, Eq, IntoStaticStr)] diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs deleted file mode 100644 index f0ff99867b..0000000000 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs +++ /dev/null @@ -1,73 +0,0 @@ -use lighthouse_network::rpc::methods::BlobsByRootRequest; -use std::sync::Arc; -use types::{BlobSidecar, EthSpec, ForkContext, Hash256, data::BlobIdentifier}; - -use super::{ActiveRequestItems, LookupVerifyError}; - -#[derive(Debug, Clone)] -pub struct BlobsByRootSingleBlockRequest { - pub block_root: Hash256, - pub indices: Vec, -} - -impl BlobsByRootSingleBlockRequest { - pub fn into_request(self, spec: &ForkContext) -> Result { - BlobsByRootRequest::new( - self.indices - .into_iter() - .map(|index| BlobIdentifier { - block_root: self.block_root, - index, - }) - .collect(), - spec, - ) - } -} - -pub struct BlobsByRootRequestItems { - request: BlobsByRootSingleBlockRequest, - items: Vec>>, -} - -impl BlobsByRootRequestItems { - pub fn new(request: BlobsByRootSingleBlockRequest) -> Self { - Self { - request, - items: vec![], - } - } -} - -impl ActiveRequestItems for BlobsByRootRequestItems { - type Item = Arc>; - - /// Appends a chunk to this multi-item request. If all expected chunks are received, this - /// method returns `Some`, resolving the request before the stream terminator. - /// The active request SHOULD be dropped after `add_response` returns an error - fn add(&mut self, blob: Self::Item) -> Result { - let block_root = blob.block_root(); - if self.request.block_root != block_root { - return Err(LookupVerifyError::UnrequestedBlockRoot(block_root)); - } - - if !blob.verify_blob_sidecar_inclusion_proof() { - return Err(LookupVerifyError::InvalidInclusionProof); - } - - if !self.request.indices.contains(&blob.index) { - return Err(LookupVerifyError::UnrequestedIndex(blob.index)); - } - if self.items.iter().any(|b| b.index == blob.index) { - return Err(LookupVerifyError::DuplicatedData(blob.slot(), blob.index)); - } - - self.items.push(blob); - - Ok(self.items.len() >= self.request.indices.len()) - } - - fn consume(&mut self) -> Vec { - std::mem::take(&mut self.items) - } -} diff --git a/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs new file mode 100644 index 0000000000..13e6454a23 --- /dev/null +++ b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs @@ -0,0 +1,48 @@ +use super::{ActiveRequestItems, LookupVerifyError}; +use lighthouse_network::rpc::methods::PayloadEnvelopesByRangeRequest; +use std::sync::Arc; +use types::{EthSpec, SignedExecutionPayloadEnvelope}; + +/// Accumulates results of a payload_envelopes_by_range request. Only returns items after +/// receiving the stream termination. +pub struct PayloadEnvelopesByRangeRequestItems { + request: PayloadEnvelopesByRangeRequest, + items: Vec>>, +} + +impl PayloadEnvelopesByRangeRequestItems { + pub fn new(request: PayloadEnvelopesByRangeRequest) -> Self { + Self { + request, + items: vec![], + } + } +} + +impl ActiveRequestItems for PayloadEnvelopesByRangeRequestItems { + type Item = Arc>; + + fn add(&mut self, envelope: Self::Item) -> Result { + if envelope.slot().as_u64() < self.request.start_slot + || envelope.slot().as_u64() >= self.request.start_slot + self.request.count + { + return Err(LookupVerifyError::UnrequestedSlot(envelope.slot())); + } + + if self + .items + .iter() + .any(|existing| existing.slot() == envelope.slot()) + { + return Err(LookupVerifyError::DuplicatedData(envelope.slot(), 0)); + } + + self.items.push(envelope); + + Ok(self.items.len() >= self.request.count as usize) + } + + fn consume(&mut self) -> Vec { + std::mem::take(&mut self.items) + } +} diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index d533d8ed0d..6292388339 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -952,6 +952,9 @@ impl SyncingChain { CouplingError::BlobPeerFailure(msg) => { debug!(?batch_id, msg, "Blob peer failure"); } + CouplingError::EnvelopePeerFailure(msg) => { + debug!(?batch_id, msg, "Envelope peer failure"); + } CouplingError::InternalError(msg) => { error!(?batch_id, msg, "Block components coupling internal error"); } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 5c9e18362c..13eeaee9aa 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1,20 +1,21 @@ use super::*; use crate::NetworkMessage; +use crate::network_beacon_processor::BlockProcessingResult; +use crate::network_beacon_processor::sync_methods::WhichPeerToPenalize; use crate::network_beacon_processor::{ ChainSegmentProcessId, InvalidBlockStorage, NetworkBeaconProcessor, }; use crate::sync::block_lookups::{BlockLookupSummary, PARENT_DEPTH_TOLERANCE}; use crate::sync::{ SyncMessage, - manager::{BatchProcessResult, BlockProcessType, BlockProcessingResult, SyncManager}, + manager::{BatchProcessResult, BlockProcessType, SyncManager}, }; -use beacon_chain::blob_verification::KzgVerifiedBlob; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; +use beacon_chain::payload_envelope_verification::AvailableEnvelope; use beacon_chain::{ - AvailabilityProcessingStatus, BlockError, EngineState, NotifyExecutionLayer, + AvailabilityProcessingStatus, EngineState, NotifyExecutionLayer, block_verification_types::{AsBlock, AvailableBlockData}, - data_availability_checker::Availability, test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, NumBlobs, generate_rand_block_and_blobs, test_spec, @@ -31,17 +32,22 @@ use lighthouse_network::{ types::SyncState, }; use slot_clock::{SlotClock, TestingSlotClock}; +use std::collections::HashSet; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use tracing::info; use types::{ - BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, EthSpec, ForkContext, ForkName, - Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, + BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSubnetId, + ForkContext, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, }; const D: Duration = Duration::new(0, 0); +/// Gloas genesis needs enough validators to populate `proposer_lookahead`. +const TEST_RIG_VALIDATOR_COUNT: usize = 8; + /// Configuration for how the test rig should respond to sync requests. /// /// Controls simulated peer behavior during lookup tests, including RPC errors, @@ -58,6 +64,10 @@ pub struct SimulateConfig { return_too_few_data_n_times: usize, return_no_columns_on_indices_n_times: usize, return_no_columns_on_indices: Vec, + /// Only omit columns for this block root, if set. + return_no_columns_for_block: Option, + /// Leave matching envelope requests unanswered. + hold_envelope_for_block: Option, skip_by_range_routes: bool, // Use a callable fn because BlockProcessingResult does not implement Clone #[educe(Debug(ignore))] @@ -131,6 +141,16 @@ impl SimulateConfig { self } + fn return_no_columns_for_block(mut self, block_root: Hash256) -> Self { + self.return_no_columns_for_block = Some(block_root); + self + } + + fn hold_envelope_for_block(mut self, block_root: Hash256) -> Self { + self.hold_envelope_for_block = Some(block_root); + self + } + pub(super) fn return_rpc_error(mut self, error: RPCError) -> Self { self.return_rpc_error = Some(error); self @@ -210,6 +230,14 @@ pub(crate) struct TestRigConfig { node_custody_type_override: Option, } +struct FullEmptyFork { + a: Hash256, + b: Hash256, + c: Hash256, + b_block: Arc>, + c_block: Arc>, +} + impl TestRig { pub(crate) fn new(test_rig_config: TestRigConfig) -> Self { // Use `fork_from_env` logic to set correct fork epochs @@ -220,10 +248,10 @@ impl TestRig { Duration::from_secs(12), ); - // Initialise a new beacon chain + // Gloas genesis needs enough validators for proposer lookahead. let harness = BeaconChainHarness::>::builder(E) .spec(spec.clone()) - .deterministic_keypairs(1) + .deterministic_keypairs(TEST_RIG_VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() .testing_slot_clock(clock.clone()) @@ -303,6 +331,7 @@ impl TestRig { fork_name, network_blocks_by_root: <_>::default(), network_blocks_by_slot: <_>::default(), + network_envelopes_by_root: <_>::default(), penalties: <_>::default(), seen_lookups: <_>::default(), requests: <_>::default(), @@ -427,9 +456,9 @@ impl TestRig { process_fn.await } } - Work::RpcBlobs { process_fn } | Work::RpcCustodyColumn(process_fn) => { - process_fn.await - } + Work::RpcBlobs { process_fn } + | Work::RpcCustodyColumn(process_fn) + | Work::RpcEnvelope(process_fn) => process_fn.await, Work::ChainSegment { process_fn, process_id: (chain_id, batch_epoch), @@ -549,52 +578,6 @@ impl TestRig { self.send_rpc_blocks_response(req_id, peer_id, &blocks); } - (RequestType::BlobsByRoot(req), AppRequestId::Sync(req_id)) => { - if self.complete_strategy.return_no_data_n_times > 0 { - self.complete_strategy.return_no_data_n_times -= 1; - return self.send_rpc_blobs_response(req_id, peer_id, &[]); - } - - let mut blobs = req - .blob_ids - .iter() - .map(|id| { - self.network_blocks_by_root - .get(&id.block_root) - .unwrap_or_else(|| { - panic!("Test consumer requested unknown block: {id:?}") - }) - .block_data() - .blobs() - .unwrap_or_else(|| panic!("Block {id:?} has no blobs")) - .iter() - .find(|blob| blob.index == id.index) - .unwrap_or_else(|| panic!("Blob id {id:?} not avail")) - .clone() - }) - .collect::>(); - - if self.complete_strategy.return_too_few_data_n_times > 0 { - self.complete_strategy.return_too_few_data_n_times -= 1; - blobs.pop(); - } - - if self - .complete_strategy - .return_wrong_sidecar_for_block_n_times - > 0 - { - self.complete_strategy - .return_wrong_sidecar_for_block_n_times -= 1; - let first = blobs.first_mut().expect("empty blobs"); - let mut blob = Arc::make_mut(first).clone(); - blob.signed_block_header.message.body_root = Hash256::ZERO; - *first = Arc::new(blob); - } - - self.send_rpc_blobs_response(req_id, peer_id, &blobs); - } - (RequestType::DataColumnsByRoot(req), AppRequestId::Sync(req_id)) => { if self.complete_strategy.return_no_data_n_times > 0 { self.complete_strategy.return_no_data_n_times -= 1; @@ -602,11 +585,14 @@ impl TestRig { } let will_omit_columns = req.data_column_ids.iter().any(|id| { - id.columns.iter().any(|c| { - self.complete_strategy - .return_no_columns_on_indices - .contains(c) - }) + self.complete_strategy + .return_no_columns_for_block + .is_none_or(|root| id.block_root == root) + && id.columns.iter().any(|c| { + self.complete_strategy + .return_no_columns_on_indices + .contains(c) + }) }); let columns_to_omit = if will_omit_columns && self.complete_strategy.return_no_columns_on_indices_n_times > 0 @@ -628,7 +614,6 @@ impl TestRig { .unwrap_or_else(|| { panic!("Test consumer requested unknown block: {id:?}") }) - .block_data() .data_columns() .unwrap_or_else(|| panic!("Block id {id:?} has no columns")); id.columns @@ -660,15 +645,34 @@ impl TestRig { .return_wrong_sidecar_for_block_n_times -= 1; let first = columns.first_mut().expect("empty columns"); let column = Arc::make_mut(first); - column - .signed_block_header_mut() - .expect("not fulu") - .message - .body_root = Hash256::ZERO; + // Corrupt the claimed block root. + match column { + DataColumnSidecar::Fulu(col) => { + col.signed_block_header.message.body_root = Hash256::ZERO; + } + DataColumnSidecar::Gloas(col) => { + col.beacon_block_root = Hash256::ZERO; + } + } } self.send_rpc_columns_response(req_id, peer_id, &columns); } + (RequestType::PayloadEnvelopesByRoot(req), AppRequestId::Sync(req_id)) => { + // Lookup sync requests one envelope root at a time. + let block_root = req + .beacon_block_roots + .as_slice() + .first() + .copied() + .unwrap_or_else(|| panic!("empty envelope request: {req:?}")); + if self.complete_strategy.hold_envelope_for_block == Some(block_root) { + return; + } + let envelope = self.network_envelopes_by_root.get(&block_root).cloned(); + self.send_rpc_envelope_response(req_id, peer_id, envelope); + } + (RequestType::BlocksByRange(req), AppRequestId::Sync(req_id)) => { if self.complete_strategy.skip_by_range_routes { return; @@ -759,7 +763,7 @@ impl TestRig { .return_wrong_range_column_indices_n_times -= 1; let wrong_columns = (req.start_slot..req.start_slot + req.count) .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) - .filter_map(|block| block.block_data().data_columns()) + .filter_map(|block| block.data_columns()) .flat_map(|columns| { columns .into_iter() @@ -783,7 +787,7 @@ impl TestRig { let wrong_columns = self .network_blocks_by_slot .get(&Slot::new(wrong_slot)) - .and_then(|block| block.block_data().data_columns()) + .and_then(|block| block.data_columns()) .into_iter() .flat_map(|columns| { columns @@ -800,7 +804,7 @@ impl TestRig { self.complete_strategy.return_partial_range_columns_n_times -= 1; let columns = (req.start_slot..req.start_slot + req.count) .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) - .filter_map(|block| block.block_data().data_columns()) + .filter_map(|block| block.data_columns()) .flat_map(|columns| { columns .into_iter() @@ -816,7 +820,7 @@ impl TestRig { let columns = (req.start_slot..req.start_slot + req.count) .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) - .filter_map(|block| block.block_data().data_columns()) + .filter_map(|block| block.data_columns()) .flat_map(|columns| { columns .into_iter() @@ -826,6 +830,25 @@ impl TestRig { self.send_rpc_columns_response(req_id, peer_id, &columns); } + (RequestType::PayloadEnvelopesByRange(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.skip_by_range_routes { + return; + } + + let envelopes = (req.start_slot..req.start_slot + req.count) + .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) + .filter_map(|block| { + let block_root = block.canonical_root(); + // Respect a withheld payload envelope. + if self.complete_strategy.hold_envelope_for_block == Some(block_root) { + return None; + } + self.network_envelopes_by_root.get(&block_root).cloned() + }) + .collect::>(); + self.send_rpc_envelopes_response(req_id, peer_id, &envelopes); + } + (RequestType::Status(_req), AppRequestId::Router) => { // Ignore Status requests for now } @@ -928,16 +951,72 @@ impl TestRig { }); } + fn send_rpc_envelope_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelope: Option>>, + ) { + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with envelope {:?}", + envelope.as_ref().map(|e| e.slot()) + )); + + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: envelope.clone(), + seen_timestamp: D, + }); + // Stream termination + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: None, + seen_timestamp: D, + }); + } + + fn send_rpc_envelopes_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelopes: &[Arc>], + ) { + let slots = envelopes.iter().map(|e| e.slot()).collect::>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with envelopes {slots:?}" + )); + + for envelope in envelopes { + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: Some(envelope.clone()), + seen_timestamp: D, + }); + } + // Stream termination + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: None, + seen_timestamp: D, + }); + } + + #[allow(dead_code)] + fn is_after_gloas(&self) -> bool { + self.fork_name.gloas_enabled() + } + // Preparation steps - /// Returns the block root of the tip of the built chain - pub(super) async fn build_chain(&mut self, block_count: usize) -> Hash256 { - let mut blocks = vec![]; - + fn get_external_harness_with_genesis(&mut self) -> BeaconChainHarness> { // Initialise a new beacon chain let external_harness = BeaconChainHarness::>::builder(E) .spec(self.harness.spec.clone()) - .deterministic_keypairs(1) + .deterministic_keypairs(TEST_RIG_VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() .testing_slot_clock(self.harness.chain.slot_clock.clone()) @@ -957,7 +1036,17 @@ impl TestRig { self.network_blocks_by_slot .insert(genesis_block.slot(), genesis_block); - for i in 0..block_count { + external_harness + } + + /// Returns the block root of the tip of the built chain + pub(super) async fn build_chain(&mut self, block_count: usize) -> Hash256 { + let mut blocks = vec![]; + + // Initialise a new beacon chain + let external_harness = self.get_external_harness_with_genesis(); + + for _ in 0..block_count { external_harness.advance_slot(); let block_root = external_harness .extend_chain( @@ -967,23 +1056,17 @@ impl TestRig { ) .await; let block = external_harness.get_full_block(&block_root); - let block_root = block.canonical_root(); let block_slot = block.slot(); - self.network_blocks_by_root - .insert(block_root, block.clone()); - self.network_blocks_by_slot.insert(block_slot, block); - self.log(&format!( - "Produced block {} index {i} in external harness", - block_slot, - )); + self.insert_external_block( + block, + external_harness + .chain + .get_payload_envelope(&block_root) + .unwrap(), + ); blocks.push((block_slot, block_root)); } - // Re-log to have a nice list of block roots at the end - for block in &blocks { - self.log(&format!("Build chain {block:?}")); - } - // Auto-update the clock on the main harness to accept the blocks self.harness .set_current_slot(external_harness.get_current_slot()); @@ -991,11 +1074,157 @@ impl TestRig { blocks.last().expect("empty blocks").1 } + /// Builds: + /// + /// ```text + /// G (full) -> A (full) -> B (FULL: bid.parent_block_hash == A.block_hash) + /// A -> C (EMPTY: bid.parent_block_hash == G.block_hash) + /// ``` + pub(super) async fn build_full_empty_fork(&mut self) -> (Hash256, Hash256, Hash256) { + // Initialise a new beacon chain (mirrors `build_chain`). + let external_harness = self.get_external_harness_with_genesis(); + + // G: full canonical block on genesis. + external_harness.advance_slot(); + let g_root = external_harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + let g_block_hash = external_harness + .get_full_block(&g_root) + .as_block() + .payload_bid_block_hash() + .unwrap(); + + // A: full block on G, imported with its envelope so the FULL child below sees A as full. + external_harness.advance_slot(); + let a_slot = external_harness.get_current_slot(); + let (a_contents, a_envelope, a_state) = external_harness + .make_block_with_envelope(external_harness.get_current_state(), a_slot) + .await; + let a_block = a_contents.0.clone(); + let a_root = a_block.canonical_root(); + let a_block_hash = a_block.as_block().payload_bid_block_hash().unwrap(); + external_harness + .process_block(a_slot, a_root, a_contents) + .await + .unwrap(); + + external_harness.advance_slot(); + let child_slot = external_harness.get_current_slot(); + + // C: EMPTY child of A. Built before A's envelope is imported, so its bid points at G. + let (c_contents, c_envelope, c_state) = external_harness + .make_block_with_envelope(a_state.clone(), child_slot) + .await; + let c_block = c_contents.0.clone(); + let c_root = c_block.canonical_root(); + + // Import A's envelope so the next child sees A as full. + let a_envelope = a_envelope.expect("A should have envelope"); + external_harness + .process_envelope(a_root, a_envelope, &a_state, a_block.state_root()) + .await; + + // B: FULL child of A. Built after A's envelope is imported, so its bid points at A. + let (b_contents, b_envelope, b_state) = external_harness + .make_block_with_envelope(a_state.clone(), child_slot) + .await; + let b_block = b_contents.0.clone(); + let b_root = b_block.canonical_root(); + + assert_eq!( + ( + b_block.parent_root(), + c_block.parent_root(), + b_block.is_parent_block_full(a_block_hash), + c_block.is_parent_block_full(a_block_hash), + c_block.is_parent_block_full(g_block_hash), + ), + (a_root, a_root, true, false, true) + ); + + // Import both children (and their envelopes) so every block is served through the same + // `get_full_block` path as the rest of the chain. + external_harness + .process_block(child_slot, c_root, c_contents) + .await + .unwrap(); + if let Some(c_envelope) = c_envelope { + external_harness + .process_envelope(c_root, c_envelope, &c_state, c_block.state_root()) + .await; + } + external_harness + .process_block(child_slot, b_root, b_contents) + .await + .unwrap(); + if let Some(b_envelope) = b_envelope { + external_harness + .process_envelope(b_root, b_envelope, &b_state, b_block.state_root()) + .await; + } + + // Cache every block through the single `get_full_block` + `insert_external_block2` path. + for root in [g_root, a_root, c_root, b_root] { + let block = external_harness.get_full_block(&root); + let envelope = external_harness.chain.get_payload_envelope(&root).unwrap(); + self.insert_external_block(block, envelope); + } + + self.harness.set_current_slot(child_slot); + + (a_root, b_root, c_root) + } + + async fn new_gloas_full_empty_fork() -> Option<(Self, FullEmptyFork)> { + let Some(mut r) = Self::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { + return None; + }; + if !r.is_after_gloas() { + return None; + } + + let (a, b, c) = r.build_full_empty_fork().await; + let fork = FullEmptyFork { + a, + b, + c, + b_block: r.network_blocks_by_root.get(&b).unwrap().block_cloned(), + c_block: r.network_blocks_by_root.get(&c).unwrap().block_cloned(), + }; + + Some((r, fork)) + } + + fn insert_external_block( + &mut self, + block: RangeSyncBlock, + envelope: Option>, + ) { + let block_root = block.canonical_root(); + let block_slot = block.slot(); + self.network_blocks_by_root + .insert(block_root, block.clone()); + self.network_blocks_by_slot.insert(block_slot, block); + // Cache Gloas envelopes for lookup RPCs. + if let Some(envelope) = envelope { + self.network_envelopes_by_root + .insert(block_root, envelope.into()); + } + self.log(&format!( + "Produced block {block_root:?} slot {block_slot} in external harness", + )); + } + fn corrupt_last_block_signature(&mut self) { let range_sync_block = self.get_last_block().clone(); let mut block = (*range_sync_block.block_cloned()).clone(); let blobs = range_sync_block.block_data().blobs(); - let columns = range_sync_block.block_data().data_columns(); + let columns = range_sync_block.data_columns(); *block.signature_mut() = self.valid_signature(); self.re_insert_block(Arc::new(block), blobs, columns); } @@ -1006,56 +1235,11 @@ impl TestRig { keypair.sk.sign(msg) } - fn corrupt_last_blob_proposer_signature(&mut self) { - let range_sync_block = self.get_last_block().clone(); - let block = range_sync_block.block_cloned(); - let mut blobs = range_sync_block - .block_data() - .blobs() - .expect("no blobs") - .into_iter() - .collect::>(); - let columns = range_sync_block.block_data().data_columns(); - let first = blobs.first_mut().expect("empty blobs"); - Arc::make_mut(first).signed_block_header.signature = self.valid_signature(); - let max_blobs = - self.harness - .spec - .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; - let blobs = - types::BlobSidecarList::new(blobs, max_blobs).expect("invalid blob sidecar list"); - self.re_insert_block(block, Some(blobs), columns); - } - - fn corrupt_last_blob_kzg_proof(&mut self) { - let range_sync_block = self.get_last_block().clone(); - let block = range_sync_block.block_cloned(); - let mut blobs = range_sync_block - .block_data() - .blobs() - .expect("no blobs") - .into_iter() - .collect::>(); - let columns = range_sync_block.block_data().data_columns(); - let first = blobs.first_mut().expect("empty blobs"); - Arc::make_mut(first).kzg_proof = kzg::KzgProof::empty(); - let max_blobs = - self.harness - .spec - .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; - let blobs = - types::BlobSidecarList::new(blobs, max_blobs).expect("invalid blob sidecar list"); - self.re_insert_block(block, Some(blobs), columns); - } - fn corrupt_last_column_proposer_signature(&mut self) { let range_sync_block = self.get_last_block().clone(); let block = range_sync_block.block_cloned(); let blobs = range_sync_block.block_data().blobs(); - let mut columns = range_sync_block - .block_data() - .data_columns() - .expect("no columns"); + let mut columns = range_sync_block.data_columns().expect("no columns"); let first = columns.first_mut().expect("empty columns"); Arc::make_mut(first) .signed_block_header_mut() @@ -1065,18 +1249,24 @@ impl TestRig { } fn corrupt_last_column_kzg_proof(&mut self) { - let range_sync_block = self.get_last_block().clone(); + let block_root = self.get_last_block().canonical_root(); + self.corrupt_column_kzg_proof(block_root); + } + + fn corrupt_column_kzg_proof(&mut self, block_root: Hash256) { + let range_sync_block = self + .network_blocks_by_root + .get(&block_root) + .unwrap_or_else(|| panic!("No block for root {block_root}")) + .clone(); let block = range_sync_block.block_cloned(); let blobs = range_sync_block.block_data().blobs(); - let mut columns = range_sync_block - .block_data() - .data_columns() - .expect("no columns"); + let mut columns = range_sync_block.data_columns().expect("no columns"); let first = columns.first_mut().expect("empty columns"); let column = Arc::make_mut(first); let proof = column.kzg_proofs_mut().first_mut().expect("no kzg proofs"); *proof = kzg::KzgProof::empty(); - self.re_insert_block(block, blobs, Some(columns)); + self.upsert_block(block, blobs, Some(columns)); } fn get_last_block(&self) -> &RangeSyncBlock { @@ -1096,22 +1286,41 @@ impl TestRig { ) { self.network_blocks_by_slot.clear(); self.network_blocks_by_root.clear(); + self.upsert_block(block, blobs, columns); + } + + fn upsert_block( + &mut self, + block: Arc>, + blobs: Option>, + columns: Option>, + ) { let block_root = block.canonical_root(); let block_slot = block.slot(); - let block_data = if let Some(columns) = columns { - AvailableBlockData::new_with_data_columns(columns) - } else if let Some(blobs) = blobs { - AvailableBlockData::new_with_blobs(blobs) + let range_sync_block = if block.fork_name_unchecked().gloas_enabled() { + // Gloas carries data columns in the payload envelope, not in `block_data`. + let envelope = self + .network_envelopes_by_root + .get(&block_root) + .cloned() + .map(|envelope| AvailableEnvelope::new(envelope, columns.unwrap_or_default())); + RangeSyncBlock::new_gloas(block, envelope).unwrap() } else { - AvailableBlockData::NoData + let block_data = if let Some(columns) = columns { + AvailableBlockData::new_with_data_columns(columns) + } else if let Some(blobs) = blobs { + AvailableBlockData::new_with_blobs(blobs) + } else { + AvailableBlockData::NoData + }; + RangeSyncBlock::new( + block, + block_data, + &self.harness.chain.data_availability_checker, + self.harness.chain.spec.clone(), + ) + .unwrap() }; - let range_sync_block = RangeSyncBlock::new( - block, - block_data, - &self.harness.chain.data_availability_checker, - self.harness.chain.spec.clone(), - ) - .unwrap(); self.network_blocks_by_slot .insert(block_slot, range_sync_block.clone()); self.network_blocks_by_root @@ -1209,7 +1418,6 @@ impl TestRig { let peer_id = self.new_connected_supernode_peer(); let columns = self .get_last_block() - .block_data() .data_columns() .expect("No data columns"); let column = columns.first().expect("empty columns"); @@ -1222,6 +1430,10 @@ impl TestRig { self.harness.chain.head().head_slot() } + pub(super) fn head_root(&self) -> Hash256 { + self.harness.chain.head().head_block_root() + } + pub(super) fn assert_head_slot(&self, slot: u64) { assert_eq!(self.head_slot(), Slot::new(slot), "Unexpected head slot"); } @@ -1322,12 +1534,6 @@ impl TestRig { self.assert_empty_network(); } - fn assert_pending_lookup_sync(&self) { - assert!(self.created_lookups() > 0, "no created lookups"); - assert_eq!(self.dropped_lookups(), 0, "some dropped lookups"); - assert_eq!(self.completed_lookups(), 0, "some completed lookups"); - } - /// Assert there is at least one range sync chain created and that all sync chains completed pub(super) fn assert_successful_range_sync(&self) { assert!( @@ -1413,23 +1619,10 @@ impl TestRig { // Test setup - fn new_after_deneb() -> Option { - genesis_fork().deneb_enabled().then(Self::default) - } - fn new_after_fulu() -> Option { genesis_fork().fulu_enabled().then(Self::default) } - fn new_after_deneb_before_fulu() -> Option { - let fork = genesis_fork(); - if fork.deneb_enabled() && !fork.fulu_enabled() { - Some(Self::default()) - } else { - None - } - } - pub fn new_fulu_peer_test(fulu_test_type: FuluTestType) -> Option { genesis_fork().fulu_enabled().then(|| { Self::new(TestRigConfig { @@ -1443,14 +1636,44 @@ impl TestRig { info!(msg, "TEST_RIG"); } - pub fn is_after_deneb(&self) -> bool { - self.fork_name.deneb_enabled() - } - pub fn is_after_fulu(&self) -> bool { self.fork_name.fulu_enabled() } + fn trigger_unknown_parent_blocks_from_all_peers( + &mut self, + blocks: &[Arc>], + ) { + for peer in self.new_connected_peers_for_peerdas() { + for block in blocks { + self.trigger_unknown_parent_block(peer, block.clone()); + } + } + } + + fn trigger_full_empty_fork(&mut self, fork: &FullEmptyFork) { + self.trigger_unknown_parent_blocks_from_all_peers(&[ + fork.b_block.clone(), + fork.c_block.clone(), + ]); + } + + async fn trigger_custody_lookup_from_all_peers(&mut self) -> Option { + if self.is_after_gloas() { + self.build_chain(2).await; + let child = self.get_last_block().block_cloned(); + let parent_root = child.parent_root(); + self.trigger_unknown_parent_blocks_from_all_peers(&[child]); + Some(parent_root) + } else { + let block_root = self.build_chain(1).await; + for peer in self.new_connected_peers_for_peerdas() { + self.trigger_unknown_block_from_attestation(block_root, peer); + } + None + } + } + fn trigger_unknown_parent_block(&mut self, peer_id: PeerId, block: Arc>) { let block_root = block.canonical_root(); self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)) @@ -1461,7 +1684,18 @@ impl TestRig { peer_id: PeerId, data_column: Arc>, ) { - self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, data_column)); + let DataColumnSidecar::Fulu(col) = data_column.as_ref() else { + self.log(&format!( + "trigger_unknown_parent_data_column noop for Gloas peer {peer_id:?}" + )); + return; + }; + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { + peer_id, + block_root: col.block_root(), + parent_root: col.block_parent_root(), + slot: col.slot(), + }); } fn trigger_unknown_block_from_attestation(&mut self, block_root: Hash256, peer_id: PeerId) { @@ -1494,6 +1728,13 @@ impl TestRig { self.sync_manager.block_lookups().active_single_lookups() } + fn active_lookup_roots(&self) -> Vec { + self.active_single_lookups() + .iter() + .map(|l| l.block_root) + .collect() + } + fn active_single_lookups_count(&self) -> usize { self.active_single_lookups().len() } @@ -1539,7 +1780,7 @@ impl TestRig { .network_globals .peers .write() - .__add_connected_peer_testing_only(false, &self.harness.spec, key); + .__add_connected_peer_with_custody_subnets(false, &self.harness.spec, key); // Assumes custody subnet count == column count let custody_subnets = self @@ -1570,13 +1811,38 @@ impl TestRig { .network_globals .peers .write() - .__add_connected_peer_testing_only(true, &self.harness.spec, key); + .__add_connected_peer_with_custody_subnets(true, &self.harness.spec, key); self.log(&format!( "Added new peer for testing {peer_id:?}, custody: supernode" )); peer_id } + /// Add a connected supernode peer, but without setting the peers' custody subnet. + /// This is to simulate the real behaviour where metadata is only received some time after + /// a connection is established. + pub fn new_connected_supernode_peer_no_metadata_custody_subnet(&mut self) -> PeerId { + let key = self.determinstic_key(); + self.network_globals + .peers + .write() + .__add_connected_peer(true, key, &self.harness.spec) + } + + /// Update the peer's custody subnet in PeerDB and send a `UpdatedPeerCgc` message to sync. + pub fn send_peer_cgc_update_to_sync( + &mut self, + peer_id: &PeerId, + subnets: HashSet, + ) { + self.network_globals + .peers + .write() + .__set_custody_subnets(peer_id, subnets) + .unwrap(); + self.send_sync_message(SyncMessage::UpdatedPeerCgc(*peer_id)) + } + fn determinstic_key(&mut self) -> CombinedKey { k256::ecdsa::SigningKey::random(&mut self.rng_08).into() } @@ -1732,77 +1998,6 @@ impl TestRig { } } - fn insert_blob_to_da_checker(&mut self, blob: Arc>) { - match self - .harness - .chain - .data_availability_checker - .put_kzg_verified_blobs( - blob.block_root(), - std::iter::once( - KzgVerifiedBlob::new(blob, &self.harness.chain.kzg, Duration::new(0, 0)) - .expect("Invalid blob"), - ), - ) - .unwrap() - { - Availability::Available(_) => panic!("column removed from da_checker, available"), - Availability::MissingComponents(block_root) => { - self.log(&format!("inserted column to da_checker {block_root:?}")) - } - }; - } - - fn insert_block_to_da_checker_as_pre_execution(&mut self, block: Arc>) { - self.log(&format!( - "Inserting block to availability_cache as pre_execution_block {:?}", - block.canonical_root() - )); - self.harness - .chain - .data_availability_checker - .put_pre_execution_block(block.canonical_root(), block, BlockImportSource::Gossip) - .unwrap(); - } - - fn simulate_block_gossip_processing_becomes_invalid(&mut self, block_root: Hash256) { - self.log(&format!( - "Marking block {block_root:?} in da_checker as execution error" - )); - self.harness - .chain - .data_availability_checker - .remove_block_on_execution_error(&block_root); - - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: false, - }); - } - - async fn simulate_block_gossip_processing_becomes_valid( - &mut self, - block: Arc>, - ) { - let block_root = block.canonical_root(); - - match self.import_block_to_da_checker(block).await { - AvailabilityProcessingStatus::Imported(block_root) => { - self.log(&format!( - "insert block to da_checker and it imported {block_root:?}" - )); - } - AvailabilityProcessingStatus::MissingComponents(_, _) => { - panic!("block not imported after adding to da_checker"); - } - } - - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: false, - }); - } - fn requests_count(&self) -> HashMap<&'static str, usize> { let mut requests_count = HashMap::new(); for (request, _) in &self.requests { @@ -1919,18 +2114,14 @@ async fn happy_path_unknown_block_parent(depth: usize) { r.build_chain(depth).await; r.trigger_with_last_unknown_block_parent(); r.simulate(SimulateConfig::happy_path()).await; - // All lookups should NOT complete on this test, however note the following for the tip lookup, - // it's the lookup for the tip block which has 0 peers and a block cached: + // Note the following for the tip lookup, it's the lookup for the tip block which has 0 peers + // and a block cached: // - before deneb the block is cached, so it's sent for processing, and success - // - before fulu the block is cached, but we can't fetch blobs so it's stuck + // - deneb/electra the block is cached, so it's sent for processing, and success // - after fulu the block is cached, we start a custody request and since we use the global pool // of peers we DO have 1 connected synced supernode peer, which gives us the columns and the // lookup succeeds - if r.is_after_deneb() && !r.is_after_fulu() { - r.assert_successful_lookup_sync_parent_trigger() - } else { - r.assert_successful_lookup_sync(); - } + r.assert_successful_lookup_sync(); } /// Assert that sync completes from an UnknownDataColumnParent @@ -1938,6 +2129,10 @@ async fn happy_path_unknown_data_parent(depth: usize) { let Some(mut r) = TestRig::new_after_fulu() else { return; }; + // No unknown-parent data-column trigger post-Gloas. + if r.is_after_gloas() { + return; + } r.build_chain(depth).await; r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; @@ -1955,7 +2150,9 @@ async fn happy_path_multiple_triggers(depth: usize) { r.trigger_with_last_block(); r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - r.trigger_with_last_unknown_data_column_parent(); + if !r.is_after_gloas() { + r.trigger_with_last_unknown_data_column_parent(); + } r.simulate(SimulateConfig::happy_path()).await; assert_eq!(r.created_lookups(), depth + 1, "Don't create extra lookups"); r.assert_successful_lookup_sync(); @@ -1978,31 +2175,37 @@ async fn bad_peer_empty_block_response(depth: usize) { // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with no blobs / columns, we downscore, and retry the same lookup +/// Assert that if peer responds with no columns, we downscore, and retry the same lookup. async fn bad_peer_empty_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; r.simulate(SimulateConfig::new().return_no_data_once()) .await; // We register a penalty, retry and complete sync successfully - r.assert_penalties(&["NotEnoughResponsesReturned"]); + if !r.is_after_gloas() { + // TODO(gloas): tip columns have no attributable FULL-child peer here. + r.assert_penalties(&["NotEnoughResponsesReturned"]); + } r.assert_successful_lookup_sync(); // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with not enough blobs / columns, we downscore, and retry the same -/// lookup +/// Assert that if peer responds with not enough columns, we downscore, and retry the same +/// lookup. async fn bad_peer_too_few_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; r.simulate(SimulateConfig::new().return_too_few_data_once()) .await; // We register a penalty, retry and complete sync successfully - r.assert_penalties(&["NotEnoughResponsesReturned"]); + if !r.is_after_gloas() { + // TODO(gloas): tip columns have no attributable FULL-child peer here. + r.assert_penalties(&["NotEnoughResponsesReturned"]); + } r.assert_successful_lookup_sync(); // TODO(tree-sync) Assert that a single lookup is created (no drops) } @@ -2019,16 +2222,21 @@ async fn bad_peer_wrong_block_response(depth: usize) { // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with bad blobs / columns, we downscore, and retry the same lookup +/// Assert that if peer responds with bad columns, we downscore, and retry the same lookup. async fn bad_peer_wrong_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; r.simulate(SimulateConfig::new().return_wrong_sidecar_for_block_once()) .await; - // We register a penalty, retry and complete sync successfully - r.assert_penalties(&["UnrequestedBlockRoot"]); + // We register a penalty, retry and complete sync successfully. Under Gloas the tip block + // (depth 1) has no attributable FULL-child peer so no custody request is made and no penalty + // is possible; at depth >= 2 the parent's columns are served by the tip (its FULL child), so + // the wrong-sidecar penalty is attributable. + if !r.is_after_gloas() || depth >= 2 { + r.assert_penalties(&["UnrequestedBlockRoot"]); + } r.assert_successful_lookup_sync(); // TODO(tree-sync) Assert that a single lookup is created (no drops) } @@ -2070,7 +2278,14 @@ async fn too_many_processing_failures(depth: usize) { r.build_chain_and_trigger_last_block(depth).await; // Simulate that a peer always returns empty r.simulate( - SimulateConfig::new().with_process_result(|| BlockError::BlockSlotLimitReached.into()), + SimulateConfig::new().with_process_result(|| BlockProcessingResult::Error { + penalty: Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::BlockPeer, + "lookup_block_processing_failure", + )), + reason: "lookup_block_processing_failure".to_string(), + }), ) .await; // We register multiple penalties, the lookup fails and sync does not progress @@ -2095,10 +2310,16 @@ async fn unknown_parent_does_not_add_peers_to_itself() { r.build_chain(2).await; r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - r.trigger_with_last_unknown_data_column_parent(); + // No data-column parent trigger post-Gloas. + let parent_lookup_peers = if r.is_after_gloas() { + 2 + } else { + r.trigger_with_last_unknown_data_column_parent(); + 3 + }; r.simulate(SimulateConfig::happy_path()).await; r.assert_peers_at_lookup_of_slot(2, 0); - r.assert_peers_at_lookup_of_slot(1, 3); + r.assert_peers_at_lookup_of_slot(1, parent_lookup_peers); assert_eq!(r.created_lookups(), 2, "Don't create extra lookups"); // All lookups should NOT complete on this test, however note the following for the tip lookup, // it's the lookup for the tip block which has 0 peers and a block cached: @@ -2114,15 +2335,21 @@ async fn unknown_parent_does_not_add_peers_to_itself() { } #[tokio::test] -/// Assert that if the beacon processor returns Ignored, the lookup is dropped +/// Assert that a non-attributable processing error (e.g. processor overloaded) is retried up to +/// `SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS`, no peer is penalized, and the lookup is then dropped. async fn test_single_block_lookup_ignored_response() { let mut r = TestRig::default(); r.build_chain_and_trigger_last_block(1).await; - // Send an Ignored response, the request should be dropped - r.simulate(SimulateConfig::new().with_process_result(|| BlockProcessingResult::Ignored)) - .await; + r.simulate( + SimulateConfig::new().with_process_result(|| BlockProcessingResult::Error { + penalty: None, + reason: "processor_overloaded".to_string(), + }), + ) + .await; // The block was not actually imported r.assert_head_slot(0); + r.assert_no_penalties(); assert_eq!(r.created_lookups(), 1, "no created lookups"); assert_eq!(r.dropped_lookups(), 1, "no dropped lookups"); assert_eq!(r.completed_lookups(), 0, "some completed lookups"); @@ -2132,11 +2359,15 @@ async fn test_single_block_lookup_ignored_response() { /// Assert that if the beacon processor returns DuplicateFullyImported, the lookup completes successfully async fn test_single_block_lookup_duplicate_response() { let mut r = TestRig::default(); + // The mock only covers block processing; Gloas also needs real envelope/column results. + if r.is_after_gloas() { + return; + } r.build_chain_and_trigger_last_block(1).await; // Send a DuplicateFullyImported response, the lookup should complete successfully r.simulate( SimulateConfig::new() - .with_process_result(|| BlockError::DuplicateFullyImported(Hash256::ZERO).into()), + .with_process_result(|| BlockProcessingResult::Imported(true, "duplicate")), ) .await; // The block was not actually imported @@ -2196,6 +2427,10 @@ async fn lookups_form_chain() { /// Assert that if a lookup chain (by appending ancestors) is too long we drop it async fn test_parent_lookup_too_deep_grow_ancestor_one() { let mut r = TestRig::default(); + // TODO(gloas): range sync does not fetch payload envelopes yet. + if r.is_after_gloas() { + return; + } r.build_chain(PARENT_DEPTH_TOLERANCE + 1).await; r.trigger_with_last_block(); r.simulate(SimulateConfig::happy_path()).await; @@ -2342,10 +2577,14 @@ async fn test_same_chain_race_condition() { #[tokio::test] /// Assert that if the lookup's block is in the da_checker we don't download it again async fn block_in_da_checker_skips_download() { - // Only in Deneb, as the block needs blobs to remain in the da_checker - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { + // Only post-Fulu, as the block needs custody columns to remain in the da_checker + let Some(mut r) = TestRig::new_after_fulu() else { return; }; + // TODO(gloas): the helper does not populate the envelope missing-component path yet. + if r.is_after_gloas() { + return; + } // Add block to da_checker // Complete test with happy path // Assert that there were no requests for blocks @@ -2365,74 +2604,6 @@ async fn block_in_da_checker_skips_download() { ); } -#[tokio::test] -async fn block_in_processing_cache_becomes_invalid() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - let block = r.block_at_slot(1); - r.insert_block_to_da_checker_as_pre_execution(block.clone()); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - r.assert_pending_lookup_sync(); - // Here the only active lookup is waiting for the block to finish processing - - // Simulate invalid block, removing it from processing cache - r.simulate_block_gossip_processing_becomes_invalid(block.canonical_root()); - // Should download block, then issue blobs request - r.simulate(SimulateConfig::happy_path()).await; - r.assert_successful_lookup_sync(); -} - -#[tokio::test] -async fn block_in_processing_cache_becomes_valid_imported() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - let block = r.block_at_slot(1); - r.insert_block_to_da_checker_as_pre_execution(block.clone()); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - r.assert_pending_lookup_sync(); - // Here the only active lookup is waiting for the block to finish processing - - // Resolve the block from processing step - r.simulate_block_gossip_processing_becomes_valid(block) - .await; - // Should not trigger block or blob request - r.assert_empty_network(); - // Resolve blob and expect lookup completed - r.assert_no_active_lookups(); -} - -// IGNORE: wait for change that delays blob fetching to knowing the block -#[tokio::test] -async fn blobs_in_da_checker_skip_download() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - let block = r.get_last_block().clone(); - let blobs = block.block_data().blobs().expect("block with no blobs"); - for blob in &blobs { - r.insert_blob_to_da_checker(blob.clone()); - } - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - - r.assert_successful_lookup_sync(); - assert_eq!( - r.requests - .iter() - .filter(|(request, _)| matches!(request, RequestType::BlobsByRoot(_))) - .collect::>(), - Vec::<&(RequestType, AppRequestId)>::new(), - "There should be no blob requests" - ); -} - macro_rules! fulu_peer_matrix_tests { ( [$($name:ident => $variant:expr),+ $(,)?] @@ -2483,14 +2654,13 @@ async fn custody_lookup_some_custody_failures(test_type: FuluTestType) { let Some(mut r) = TestRig::new_fulu_peer_test(test_type) else { return; }; - let block_root = r.build_chain(1).await; - // Send the same trigger from all peers, so that the lookup has all peers - for peer in r.new_connected_peers_for_peerdas() { - r.trigger_unknown_block_from_attestation(block_root, peer); - } + let block_under_test = r.trigger_custody_lookup_from_all_peers().await; let custody_columns = r.custody_columns(); - r.simulate(SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..4], 3)) - .await; + let mut config = SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..4], 3); + if let Some(block_root) = block_under_test { + config = config.return_no_columns_for_block(block_root); + } + r.simulate(config).await; r.assert_penalties_of_type("NotEnoughResponsesReturned"); r.assert_successful_lookup_sync(); } @@ -2499,20 +2669,15 @@ async fn custody_lookup_permanent_custody_failures(test_type: FuluTestType) { let Some(mut r) = TestRig::new_fulu_peer_test(test_type) else { return; }; - let block_root = r.build_chain(1).await; - - // Send the same trigger from all peers, so that the lookup has all peers - for peer in r.new_connected_peers_for_peerdas() { - r.trigger_unknown_block_from_attestation(block_root, peer); - } + let block_under_test = r.trigger_custody_lookup_from_all_peers().await; let custody_columns = r.custody_columns(); - r.simulate( - SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..2], usize::MAX), - ) - .await; - // Every peer that does not return a column is part of the lookup because it claimed to have - // imported the lookup, so we will penalize. + let mut config = + SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..2], usize::MAX); + if let Some(block_root) = block_under_test { + config = config.return_no_columns_for_block(block_root); + } + r.simulate(config).await; r.assert_penalties_of_type("NotEnoughResponsesReturned"); r.assert_failed_lookup_sync(); } @@ -2541,43 +2706,7 @@ async fn crypto_on_fail_with_invalid_block_signature() { r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_block_processing_failure"); - } -} - -#[tokio::test] -async fn crypto_on_fail_with_bad_blob_proposer_signature() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - r.corrupt_last_blob_proposer_signature(); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - if cfg!(feature = "fake_crypto") { - r.assert_successful_lookup_sync(); - r.assert_no_penalties(); - } else { - r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_blobs_processing_failure"); - } -} - -#[tokio::test] -async fn crypto_on_fail_with_bad_blob_kzg_proof() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - r.corrupt_last_blob_kzg_proof(); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - if cfg!(feature = "fake_crypto") { - r.assert_successful_lookup_sync(); - r.assert_no_penalties(); - } else { - r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_blobs_processing_failure"); + r.assert_penalties_of_type("InvalidSignature"); } } @@ -2586,6 +2715,10 @@ async fn crypto_on_fail_with_bad_column_proposer_signature() { let Some(mut r) = TestRig::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { return; }; + // Gloas columns have no per-column proposer signature. + if r.is_after_gloas() { + return; + } r.build_chain(1).await; r.corrupt_last_column_proposer_signature(); r.trigger_with_last_block(); @@ -2595,7 +2728,7 @@ async fn crypto_on_fail_with_bad_column_proposer_signature() { r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_custody_column_processing_failure"); + r.assert_penalties_of_type("InvalidSignature"); } } @@ -2604,15 +2737,55 @@ async fn crypto_on_fail_with_bad_column_kzg_proof() { let Some(mut r) = TestRig::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { return; }; - r.build_chain(1).await; - r.corrupt_last_column_kzg_proof(); - r.trigger_with_last_block(); + if r.is_after_gloas() { + r.build_chain(2).await; + let child = r.get_last_block().block_cloned(); + r.corrupt_column_kzg_proof(child.parent_root()); + r.trigger_unknown_parent_blocks_from_all_peers(&[child]); + } else { + r.build_chain(1).await; + r.corrupt_last_column_kzg_proof(); + r.trigger_with_last_block(); + } r.simulate(SimulateConfig::happy_path()).await; if cfg!(feature = "fake_crypto") { r.assert_successful_lookup_sync(); r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_custody_column_processing_failure"); + r.assert_penalties_of_type("AvailabilityCheck"); } } + +#[tokio::test] +async fn gloas_full_empty_children_retain_parent_for_payload() { + let Some((mut r, fork)) = TestRig::new_gloas_full_empty_fork().await else { + return; + }; + + r.trigger_full_empty_fork(&fork); + + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); +} + +#[tokio::test] +async fn gloas_empty_child_continues_while_parent_payload_withheld() { + let Some((mut r, fork)) = TestRig::new_gloas_full_empty_fork().await else { + return; + }; + + r.trigger_full_empty_fork(&fork); + + r.simulate(SimulateConfig::happy_path().hold_envelope_for_block(fork.a)) + .await; + + assert_eq!(r.head_root(), fork.c); + assert_eq!(r.created_lookups(), 4); + assert_eq!(r.completed_lookups(), 2); + assert_eq!(r.dropped_lookups(), 0); + assert_eq!(r.active_lookup_roots(), vec![fork.a, fork.b]); + r.assert_no_penalties(); + r.assert_empty_network(); + r.assert_empty_processor(); +} diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index 4e185cc081..2f318bfb9a 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -21,7 +21,7 @@ use tokio::sync::mpsc; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use types::{ForkName, Hash256, MinimalEthSpec as E, Slot}; +use types::{ForkName, Hash256, MinimalEthSpec as E, SignedExecutionPayloadEnvelope, Slot}; mod lookups; mod range; @@ -77,6 +77,10 @@ struct TestRig { /// Blocks that will be used in the test but may not be known to `harness` yet. network_blocks_by_root: HashMap>, network_blocks_by_slot: HashMap>, + /// Gloas execution payload envelopes keyed by block root, populated during `build_chain` + /// from the external harness store. The rig serves these when a lookup issues a + /// `PayloadEnvelopesByRoot` request. + network_envelopes_by_root: HashMap>>, penalties: Vec, /// All seen lookups through the test run seen_lookups: HashMap, diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 891d9d1e97..e6890cf242 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -27,12 +27,20 @@ use crate::sync::range_sync::RangeSyncType; use lighthouse_network::rpc::RPCError; use lighthouse_network::rpc::methods::StatusMessageV2; use lighthouse_network::{PeerId, SyncInfo}; +use std::collections::HashSet; use types::{Epoch, EthSpec, Hash256, MinimalEthSpec as E, Slot}; /// MinimalEthSpec has 8 slots per epoch const SLOTS_PER_EPOCH: usize = 8; impl TestRig { + /// Range sync doesn't yet ingest Gloas blocks in these tests: the range harness doesn't serve + /// payload envelopes, so a Gloas block never becomes fully available and sync can't complete. + /// Skip the affected completion tests under a Gloas genesis. TODO(gloas): support range sync. + fn skip_range_sync_under_gloas(&self) -> bool { + self.fork_name.gloas_enabled() + } + fn add_head_peer(&mut self) -> PeerId { let local_info = self.local_info(); self.add_supernode_peer(SyncInfo { @@ -50,7 +58,7 @@ impl TestRig { finalized_root: Hash256::random(), head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), head_root: Hash256::random(), - earliest_available_slot: None, + earliest_available_slot: Some(Slot::new(0)), } } @@ -259,6 +267,9 @@ impl TestRig { #[tokio::test] async fn head_sync_completes() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_head_sync().await; r.simulate(SimulateConfig::happy_path()).await; r.assert_head_sync_completed(); @@ -270,6 +281,9 @@ async fn head_sync_completes() { #[tokio::test] async fn finalized_to_head_transition() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_and_head_sync().await; r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); @@ -281,6 +295,9 @@ async fn finalized_to_head_transition() { #[tokio::test] async fn finalized_sync_completes() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); @@ -292,6 +309,9 @@ async fn finalized_sync_completes() { #[tokio::test] async fn batch_rpc_error_retries() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path().return_rpc_error(RPCError::UnsupportedProtocol)) .await; @@ -360,6 +380,9 @@ async fn batch_peer_returns_partial_columns_then_succeeds() { #[tokio::test] async fn batch_non_faulty_failure_retries() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path().with_range_non_faulty_failures(1)) .await; @@ -371,6 +394,9 @@ async fn batch_non_faulty_failure_retries() { #[tokio::test] async fn batch_faulty_failure_redownloads() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path().with_range_faulty_failures(1)) .await; @@ -427,6 +453,9 @@ async fn late_response_for_removed_chain() { #[tokio::test] async fn ee_offline_then_online_resumes_sync() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path().with_ee_offline_for_n_range_responses(2)) .await; @@ -439,6 +468,9 @@ async fn ee_offline_then_online_resumes_sync() { #[tokio::test] async fn finalized_sync_with_local_head_partial() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync_with_local_head(3).await; r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); @@ -449,6 +481,9 @@ async fn finalized_sync_with_local_head_partial() { #[tokio::test] async fn finalized_sync_with_local_head_near_target() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } let target_epochs = 5; let local_slots = (target_epochs * SLOTS_PER_EPOCH) - 1; // all blocks except last r.build_chain(target_epochs * SLOTS_PER_EPOCH).await; @@ -467,7 +502,7 @@ async fn finalized_sync_with_local_head_near_target() { #[tokio::test] async fn not_enough_custody_peers_then_peers_arrive() { let mut r = TestRig::default(); - if !r.fork_name.fulu_enabled() { + if !r.fork_name.fulu_enabled() || r.skip_range_sync_under_gloas() { return; } let remote_info = r.setup_finalized_sync_insufficient_peers().await; @@ -476,3 +511,76 @@ async fn not_enough_custody_peers_then_peers_arrive() { r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); } + +/// This is a regression test for the following race condition scenario: +/// 1. A node is connected to 3 supernode peers: peer 1 is synced, & peer 2 and 3 are advanced. +/// 2. No metadata has been received yet (i.e. no custody info), so the node cannot start data +/// column range sync yet. +/// 3. Now peer 1 sends the CGC via metadata response, we now have one peer on all custody subnets, +/// BUT not on the finalized syncing chain. +/// 4. The node tries to `send_batch` but fails repeatedly with `NoPeers`, as there's no peer +/// that is able to serve columns for the advanced epochs. The chain is removed after 5 failed attempts. +/// 5. Now peer 2 & 3 send CGC updates, BUT because there's no syncing chain, nothing happens - +/// sync is stuck until finding new peers. +/// +/// The expected behaviour in this scenario should be: +/// 4. not finding suitable peers, chain is kept and batch remains in AwaitingDownload +/// 5. finalized sync should resume as soon as CGC updates are received from peer 2 or 3. +#[tokio::test] +async fn finalized_sync_not_enough_custody_peers_resume_after_peer_cgc_update() { + let mut r = TestRig::default(); + if !r.fork_name.fulu_enabled() || r.skip_range_sync_under_gloas() { + return; + } + + // GIVEN: the node is connected to 3 supernode peers: + let advanced_epochs: usize = 2; + let sync_epochs = advanced_epochs + 3; + let sync_slots = sync_epochs * SLOTS_PER_EPOCH - 1; + r.build_chain(sync_slots).await; + r.harness.set_current_slot(Slot::new(sync_slots as u64 + 1)); + + // Peer 1 is synced (same finalized epoch), but its earliest available slot means it + // cannot serve the batches needed for this sync. + let peer_1 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + let mut remote_info = r.local_info().clone(); + remote_info.earliest_available_slot = Some(Slot::new(sync_slots as u64)); + r.send_sync_message(SyncMessage::AddPeer(peer_1, remote_info)); + + // Peer 2 is advanced (local finalized epoch + 2) + let peer_2 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + let remote_info = r.finalized_remote_info_advanced_by((advanced_epochs as u64).into()); + r.send_sync_message(SyncMessage::AddPeer(peer_2, remote_info.clone())); + // We expect a finalized chain to be created with peer 2, but no requests sent out yet due to missing custody info. + r.assert_state(RangeSyncType::Finalized); + r.assert_empty_network(); + + // Peer 3 is connected and advanced + let peer_3 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + r.send_sync_message(SyncMessage::AddPeer(peer_3, remote_info)); + // We are still in finalized sync state (now with peer 3 added) + r.assert_state(RangeSyncType::Finalized); + + for (i, p) in [peer_1, peer_2, peer_3].iter().enumerate() { + let peer_idx = i + 1; + r.log(&format!("Peer {peer_idx}: {p:?}")); + } + + // WHEN: peer 1 sends its CGC via metadata response + let all_custody_subnets = (0..r.harness.spec.data_column_sidecar_subnet_count) + .map(|i| i.into()) + .collect::>(); + r.send_peer_cgc_update_to_sync(&peer_1, all_custody_subnets.clone()); + + // We still don't have any peers on the syncing chain with custody columns (only peer 1) + // The node won't send the batch and will remain in the finalized sync state (this was failing before!) + r.assert_state(RangeSyncType::Finalized); + r.assert_empty_network(); + + // Now we receive peer 2 & 3's CGC updates, the node will resume syncing from these two peers + r.send_peer_cgc_update_to_sync(&peer_2, all_custody_subnets.clone()); + r.send_peer_cgc_update_to_sync(&peer_3, all_custody_subnets); + + r.simulate(SimulateConfig::happy_path()).await; + r.assert_range_sync_completed(); +} diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 647b5858cb..988e2d1fc5 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -387,6 +387,14 @@ pub fn cli_app() -> Command { .help("Disables the quic transport. The node will rely solely on the TCP transport for libp2p connections.") .display_order(0) ) + .arg( + Arg::new("enable-mplex") + .long("enable-mplex") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .help("Enables mplex multiplexer alongside yamux. Yamux is preferred when both are available.") + .display_order(0) + ) .arg( Arg::new("disable-peer-scoring") .long("disable-peer-scoring") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 045b432dc9..ddf8d07c4e 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1443,6 +1443,10 @@ pub fn set_network_config( config.disable_quic_support = true; } + if parse_flag(cli_args, "enable-mplex") { + config.enable_mplex = true; + } + if parse_flag(cli_args, "disable-upnp") { config.upnp_enabled = false; } diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 30163f1f0c..1f57db1b59 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -494,6 +494,9 @@ Flags: Sets the local ENR IP address and port to match those set for lighthouse. Specifically, the IP address will be the value of --listen-address and the UDP port will be --discovery-port. + --enable-mplex + Enables mplex multiplexer alongside yamux. Yamux is preferred when + both are available. --enable-partial-columns Enable partial messages for data columns. This can reduce the amount of data sent over the network. Enabled by default on Hoodi and diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e9fb44209b..572f9522ee 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -46,7 +46,10 @@ use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; -use types::{PayloadAttestationData, PayloadAttestationMessage, SignedProposerPreferences}; +use types::{ + PayloadAttestationData, PayloadAttestationMessage, SignedExecutionPayloadBid, + SignedProposerPreferences, +}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); @@ -2752,8 +2755,8 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } - /// `GET v1/validator/execution_payload_envelope/{slot}` - pub async fn get_validator_execution_payload_envelope( + /// `GET v1/validator/execution_payload_envelopes/{slot}` + pub async fn get_validator_execution_payload_envelopes( &self, slot: Slot, ) -> Result>, Error> { @@ -2762,14 +2765,14 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") - .push("execution_payload_envelope") + .push("execution_payload_envelopes") .push(&slot.to_string()); self.get(path).await } - /// `GET v1/validator/execution_payload_envelope/{slot}` in SSZ format - pub async fn get_validator_execution_payload_envelope_ssz( + /// `GET v1/validator/execution_payload_envelopes/{slot}` in SSZ format + pub async fn get_validator_execution_payload_envelopes_ssz( &self, slot: Slot, ) -> Result, Error> { @@ -2778,7 +2781,7 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") - .push("execution_payload_envelope") + .push("execution_payload_envelopes") .push(&slot.to_string()); let opt_response = self @@ -2790,8 +2793,8 @@ impl BeaconNodeHttpClient { ExecutionPayloadEnvelope::from_ssz_bytes(&response_bytes).map_err(Error::InvalidSsz) } - /// `POST v1/beacon/execution_payload_envelope` - pub async fn post_beacon_execution_payload_envelope( + /// `POST v1/beacon/execution_payload_envelopes` + pub async fn post_beacon_execution_payload_envelopes( &self, envelope: &SignedExecutionPayloadEnvelope, fork_name: ForkName, @@ -2801,7 +2804,7 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") - .push("execution_payload_envelope"); + .push("execution_payload_envelopes"); self.post_generic_with_consensus_version( path, @@ -2814,8 +2817,8 @@ impl BeaconNodeHttpClient { Ok(()) } - /// `POST v1/beacon/execution_payload_envelope` in SSZ format - pub async fn post_beacon_execution_payload_envelope_ssz( + /// `POST v1/beacon/execution_payload_envelopes` in SSZ format + pub async fn post_beacon_execution_payload_envelopes_ssz( &self, envelope: &SignedExecutionPayloadEnvelope, fork_name: ForkName, @@ -2825,7 +2828,7 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") - .push("execution_payload_envelope"); + .push("execution_payload_envelopes"); self.post_generic_with_consensus_version_and_ssz_body( path, @@ -2838,8 +2841,56 @@ impl BeaconNodeHttpClient { Ok(()) } - /// Path for `v1/beacon/execution_payload_envelope/{block_id}` - pub fn get_beacon_execution_payload_envelope_path( + /// `POST v1/beacon/execution_payload_bids` + pub async fn post_beacon_execution_payload_bids( + &self, + bid: &SignedExecutionPayloadBid, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bids"); + + self.post_generic_with_consensus_version( + path, + bid, + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// `POST v1/beacon/execution_payload_bids` in SSZ format + pub async fn post_beacon_execution_payload_bids_ssz( + &self, + bid: &SignedExecutionPayloadBid, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bids"); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + bid.as_ssz_bytes(), + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// Path for `v1/beacon/execution_payload_envelopes/{block_id}` + pub fn get_beacon_execution_payload_envelopes_path( &self, block_id: BlockId, ) -> Result { @@ -2847,35 +2898,35 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") - .push("execution_payload_envelope") + .push("execution_payload_envelopes") .push(&block_id.to_string()); Ok(path) } - /// `GET v1/beacon/execution_payload_envelope/{block_id}` + /// `GET v1/beacon/execution_payload_envelopes/{block_id}` /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_execution_payload_envelope( + pub async fn get_beacon_execution_payload_envelopes( &self, block_id: BlockId, ) -> Result< Option>>, Error, > { - let path = self.get_beacon_execution_payload_envelope_path(block_id)?; + let path = self.get_beacon_execution_payload_envelopes_path(block_id)?; self.get_opt(path) .await .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } - /// `GET v1/beacon/execution_payload_envelope/{block_id}` in SSZ format + /// `GET v1/beacon/execution_payload_envelopes/{block_id}` in SSZ format /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_execution_payload_envelope_ssz( + pub async fn get_beacon_execution_payload_envelopes_ssz( &self, block_id: BlockId, ) -> Result>, Error> { - let path = self.get_beacon_execution_payload_envelope_path(block_id)?; + let path = self.get_beacon_execution_payload_envelopes_path(block_id)?; let opt_response = self .get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_beacon_blocks_ssz) .await?; diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index f05b2741cb..f15a6b2932 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -207,6 +207,18 @@ pub enum InvalidPayloadAttestation { }, } +/// The import status of a block's parent, as seen by fork choice. +#[allow(clippy::large_enum_variant)] +pub enum ParentImportStatus { + /// The parent block is imported and the child's bid commits to a parent payload known to fork + /// choice. + Imported(ProtoBlock), + /// The parent block is not known to fork choice. + UnknownBlock, + /// The parent block is known, but the child's bid commits to a payload not known to fork choice. + UnknownPayload, +} + impl From for Error { fn from(e: String) -> Self { Error::ProtoArrayStringError(e) @@ -1537,6 +1549,37 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } + /// Returns `true` if the block's parent is imported (and, for a post-Gloas FULL child, its + /// parent's payload is imported too). See [`Self::get_parent_import_status`]. + pub fn is_parent_imported(&self, block: &SignedBeaconBlock) -> bool { + matches!( + self.get_parent_import_status(block), + ParentImportStatus::Imported(_) + ) + } + + /// Returns the import status of the parent of `block`. + /// + /// A post-Gloas FULL child also requires the parent's payload (committed to by the child's bid) + /// to have been received by fork choice. + pub fn get_parent_import_status(&self, block: &SignedBeaconBlock) -> ParentImportStatus { + if let Some(parent_block) = self.get_block(&block.parent_root()) { + let Some(parent_block_hash) = parent_block.execution_payload_block_hash else { + // Pre-Gloas parent: payload is embedded in the block, so treat as imported. + return ParentImportStatus::Imported(parent_block); + }; + if block.is_parent_block_full(parent_block_hash) + && !self.is_payload_received(&block.parent_root()) + { + ParentImportStatus::UnknownPayload + } else { + ParentImportStatus::Imported(parent_block) + } + } else { + ParentImportStatus::UnknownBlock + } + } + /// Called by the proposer to decide whether to build on the full or empty parent. pub fn should_build_on_full( &self, diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index 159eab0ec0..dcc499547b 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -4,9 +4,9 @@ mod metrics; pub use crate::fork_choice::{ AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, - InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, PayloadVerificationStatus, - PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation, - ResetPayloadStatuses, + InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, ParentImportStatus, + PayloadVerificationStatus, PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, + QueuedAttestation, ResetPayloadStatuses, }; pub use fork_choice_store::ForkChoiceStore; pub use proto_array::{ diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index ee86277f9c..c424c01f6c 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -19,3 +19,11 @@ superstruct = { workspace = true } typenum = { workspace = true } types = { workspace = true } yaml_serde = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +fixed_bytes = { workspace = true } + +[[bench]] +name = "find_head" +harness = false diff --git a/consensus/proto_array/benches/find_head.rs b/consensus/proto_array/benches/find_head.rs new file mode 100644 index 0000000000..98077a7f97 --- /dev/null +++ b/consensus/proto_array/benches/find_head.rs @@ -0,0 +1,118 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use fixed_bytes::FixedBytesExtended; +use proto_array::{Block, ExecutionStatus, JustifiedBalances, ProtoArrayForkChoice}; +use std::collections::BTreeSet; +use std::time::Duration; +use types::{ + AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, + MainnetEthSpec, Slot, +}; + +fn get_root(i: u64) -> Hash256 { + Hash256::from_low_u64_be(i) +} + +fn get_hash(i: u64) -> ExecutionBlockHash { + ExecutionBlockHash::from_root(get_root(i)) +} + +/// Build a linear chain of `num_blocks` blocks. +fn build_chain(num_blocks: u64, gloas: bool) -> (ProtoArrayForkChoice, types::ChainSpec) { + let mut spec = MainnetEthSpec::default_spec(); + let gloas_fork_slot = 32; + if gloas { + spec.gloas_fork_epoch = Some(Epoch::new(1)); + } + + let finalized_checkpoint = Checkpoint { + epoch: Epoch::new(0), + root: get_root(0), + }; + let junk_shuffling_id = AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero()); + + let mut fork_choice = ProtoArrayForkChoice::new::( + Slot::new(0), + Slot::new(0), + Hash256::zero(), + finalized_checkpoint, + finalized_checkpoint, + junk_shuffling_id.clone(), + junk_shuffling_id.clone(), + ExecutionStatus::Optimistic(ExecutionBlockHash::zero()), + None, + None, + 0, + &spec, + ) + .expect("should create fork choice"); + + for i in 1..=num_blocks { + let is_gloas = gloas && i >= gloas_fork_slot; + let block = Block { + slot: Slot::new(i), + root: get_root(i), + parent_root: Some(get_root(i - 1)), + state_root: Hash256::zero(), + target_root: get_root(0), + current_epoch_shuffling_id: junk_shuffling_id.clone(), + next_epoch_shuffling_id: junk_shuffling_id.clone(), + justified_checkpoint: finalized_checkpoint, + finalized_checkpoint, + execution_status: ExecutionStatus::Optimistic(ExecutionBlockHash::zero()), + unrealized_justified_checkpoint: Some(finalized_checkpoint), + unrealized_finalized_checkpoint: Some(finalized_checkpoint), + execution_payload_parent_hash: if is_gloas { + Some(get_hash(i - 1)) + } else { + None + }, + execution_payload_block_hash: if is_gloas { Some(get_hash(i)) } else { None }, + proposer_index: Some(0), + }; + + fork_choice + .process_block::(block, Slot::new(i), &spec, Duration::ZERO) + .expect("should process block"); + } + + (fork_choice, spec) +} + +fn bench_find_head(c: &mut Criterion) { + let mut group = c.benchmark_group("find_head"); + let equivocating_indices = BTreeSet::new(); + let finalized_checkpoint = Checkpoint { + epoch: Epoch::new(0), + root: get_root(0), + }; + let balances = JustifiedBalances::from_effective_balances(vec![1; 64]).unwrap(); + + // 216k = ~1 month non-finality mainnet, 518k = ~1 month non-finality Gnosis. + // Must survive extended non-finality (500k+ blocks). + for (label, gloas) in [("pre_gloas", false), ("gloas", true)] { + for &num_blocks in &[100, 1_000, 10_000, 50_000, 216_000, 518_000] { + let (mut fork_choice, spec) = build_chain(num_blocks, gloas); + + group.bench_function(BenchmarkId::new(label, num_blocks), |b| { + b.iter(|| { + fork_choice + .find_head::( + finalized_checkpoint, + finalized_checkpoint, + &balances, + Hash256::zero(), + &equivocating_indices, + Slot::new(num_blocks), + &spec, + ) + .expect("should find head") + }); + }); + } + } + + group.finish(); +} + +criterion_group!(benches, bench_find_head); +criterion_main!(benches); diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index d185ed371c..eb0f30cc87 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -1,6 +1,6 @@ use crate::PayloadStatus; use safe_arith::ArithError; -use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256, Slot}; +use types::{Epoch, ExecutionBlockHash, Hash256}; #[derive(Clone, PartialEq, Debug)] pub enum Error { @@ -9,8 +9,6 @@ pub enum Error { NodeUnknown(Hash256), InvalidFinalizedRootChange, InvalidNodeIndex(usize), - InvalidParentIndex(usize), - InvalidBestChildIndex(usize), InvalidJustifiedIndex(usize), InvalidBestDescendant(usize), InvalidParentDelta(usize), @@ -30,7 +28,6 @@ pub enum Error { current_finalized_epoch: Epoch, new_finalized_epoch: Epoch, }, - InvalidBestNode(Box), InvalidAncestorOfValidPayload { ancestor_block_root: Hash256, ancestor_payload_block_hash: ExecutionBlockHash, @@ -74,14 +71,3 @@ impl From for Error { Error::Arith(e) } } - -#[derive(Clone, PartialEq, Debug)] -pub struct InvalidBestNodeInfo { - pub current_slot: Slot, - pub start_root: Hash256, - pub justified_checkpoint: Checkpoint, - pub finalized_checkpoint: Checkpoint, - pub head_root: Hash256, - pub head_justified_checkpoint: Checkpoint, - pub head_finalized_checkpoint: Checkpoint, -} diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index cc82e2ab2a..7cde1023cf 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -152,7 +152,7 @@ impl ForkChoiceTestDefinition { pub fn run(self) { let spec = self.spec.unwrap_or_else(|| { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; // Legacy test definitions target pre-Gloas behaviour unless explicitly overridden. spec.gloas_fork_epoch = None; spec diff --git a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs index 76348289c8..8417e2964b 100644 --- a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs +++ b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs @@ -2,7 +2,7 @@ use super::*; fn gloas_spec() -> ChainSpec { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; spec.gloas_fork_epoch = Some(Epoch::new(0)); spec } @@ -977,7 +977,7 @@ mod tests { fn gloas_fork_boundary_spec() -> ChainSpec { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; spec.gloas_fork_epoch = Some(Epoch::new(1)); spec } diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 8c2289e8c3..929792528e 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1,4 +1,3 @@ -use crate::error::InvalidBestNodeInfo; use crate::proto_array_fork_choice::IndexedForkChoiceNode; use crate::{ Block, ExecutionStatus, JustifiedBalances, LatestMessage, PayloadStatus, error::Error, @@ -392,6 +391,10 @@ pub struct ProtoArray { pub prune_threshold: usize, pub nodes: Vec, pub indices: HashMap, + /// Cached parent→children index. `children[i]` holds the node indices of all children of + /// node `i`. Maintained incrementally by `on_block` and `maybe_prune`. + #[serde(skip)] + pub children: Vec>, } impl ProtoArray { @@ -674,6 +677,16 @@ impl ProtoArray { self.indices.insert(node.root(), node_index); self.nodes.push(node.clone()); + // Maintain cached children index. `parent_index` is already bounds-checked above + // against `self.nodes`, and `self.children` is kept in lockstep with `self.nodes`. + self.children.push(Vec::new()); + if let Some(parent_index) = node.parent() { + self.children + .get_mut(parent_index) + .ok_or(Error::InvalidNodeIndex(parent_index))? + .push(node_index); + } + if let Some(parent_index) = node.parent() && matches!(block.execution_status, ExecutionStatus::Valid(_)) { @@ -1093,31 +1106,25 @@ impl ProtoArray { spec, )?; - // Perform a sanity check that the node is indeed valid to be the head. - let best_node = self - .nodes - .get(best_fc_node.proto_node_index) - .ok_or(Error::InvalidNodeIndex(best_fc_node.proto_node_index))?; - if !self.node_is_viable_for_head::( - best_node, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - ) { - return Err(Error::InvalidBestNode(Box::new(InvalidBestNodeInfo { - current_slot, - start_root: *justified_root, - justified_checkpoint: best_justified_checkpoint, - finalized_checkpoint: best_finalized_checkpoint, - head_root: best_node.root(), - head_justified_checkpoint: *best_node.justified_checkpoint(), - head_finalized_checkpoint: *best_node.finalized_checkpoint(), - }))); - } - Ok((best_fc_node.root, best_fc_node.payload_status)) } + /// Rebuild the cached `self.children` index from `self.nodes`. Called once after + /// deserialization to populate the transient field. + pub fn rebuild_children_index(&mut self) -> Result<(), Error> { + let mut children = vec![Vec::new(); self.nodes.len()]; + for (i, node) in self.nodes.iter().enumerate() { + if let Some(parent_idx) = node.parent() { + children + .get_mut(parent_idx) + .ok_or(Error::InvalidNodeIndex(parent_idx))? + .push(i); + } + } + self.children = children; + Ok(()) + } + /// Spec: `get_filtered_block_tree`. /// /// Returns the set of node indices on viable branches — those with at least @@ -1128,7 +1135,7 @@ impl ProtoArray { current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, - ) -> HashSet { + ) -> Result, Error> { let mut viable = HashSet::new(); self.filter_block_tree::( start_index, @@ -1136,71 +1143,88 @@ impl ProtoArray { best_justified_checkpoint, best_finalized_checkpoint, &mut viable, - ); - viable + )?; + Ok(viable) } /// Spec: `filter_block_tree`. + /// + /// Proto_array stores nodes in insertion order — children always have higher + /// indices than their parents. A single reverse pass therefore processes every + /// child before its parent, matching the spec's recursive post-order semantics + /// without recursion (required to survive 500k+ blocks of non-finality). + /// + /// The spec removes execution-invalid blocks (and their entire subtrees) from + /// `store.blocks` before running. We replicate that here with a forward pass + /// propagating `excluded` from parent to child — V29 children of an invalidated + /// V17 ancestor are excluded transitively, since V29 nodes carry no + /// `execution_status` of their own. fn filter_block_tree( &self, - node_index: usize, + start_index: usize, current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, viable: &mut HashSet, - ) -> bool { - let Some(node) = self.nodes.get(node_index) else { - return false; - }; + ) -> Result<(), Error> { + // Forward pass: a node is "excluded" if it (or any ancestor down to + // `start_index`) has an invalid execution status. + let mut excluded = vec![false; self.nodes.len()]; + for i in (start_index + 1)..self.nodes.len() { + let node = self.nodes.get(i).ok_or(Error::InvalidNodeIndex(i))?; + let parent_excluded = match node.parent() { + Some(p) => *excluded.get(p).ok_or(Error::InvalidNodeIndex(p))?, + None => false, + }; + let self_invalid = node.execution_status().is_ok_and(|s| s.is_invalid()); + excluded[i] = parent_excluded || self_invalid; + } - // Skip invalid children — they aren't in store.blocks in the spec. - let children: Vec = self - .nodes - .iter() - .enumerate() - .filter(|(_, child)| { - child.parent() == Some(node_index) - && !child - .execution_status() - .is_ok_and(|status| status.is_invalid()) - }) - .map(|(i, _)| i) - .collect(); - - if !children.is_empty() { - // Evaluate ALL children (no short-circuit) to mark all viable branches. - let any_viable = children - .iter() - .map(|&child_index| { - self.filter_block_tree::( - child_index, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - viable, - ) - }) - .collect::>() - .into_iter() - .any(|v| v); - if any_viable { - viable.insert(node_index); - return true; + for node_index in (start_index..self.nodes.len()).rev() { + // Spec: invalid subtree removed from `store.blocks` — skip entirely. + if *excluded + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))? + { + continue; } - return false; - } + let node = self + .nodes + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))?; - // Leaf node: check viability. - if self.node_is_viable_for_head::( - node, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - ) { - viable.insert(node_index); - return true; + // Spec: children = [root for root in blocks if blocks[root].parent_root == block_root] + let valid_children: Vec = self + .children + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))? + .iter() + .copied() + .filter_map(|i| match excluded.get(i) { + Some(false) => Some(Ok(i)), + Some(true) => None, + None => Some(Err(Error::InvalidNodeIndex(i))), + }) + .collect::>()?; + + if !valid_children.is_empty() { + // Spec: if any(children): if any(filter_block_tree_result): blocks[block_root] = block + if valid_children.iter().any(|c| viable.contains(c)) { + viable.insert(node_index); + } + } else { + // Spec: leaf — check correct_justified and correct_finalized + if self.node_is_viable_for_head::( + node, + current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, + ) { + viable.insert(node_index); + } + } } - false + Ok(()) } /// Spec: `get_head`. @@ -1227,7 +1251,7 @@ impl ProtoArray { current_slot, best_justified_checkpoint, best_finalized_checkpoint, - ); + )?; // Compute once rather than per-child per-level. let apply_proposer_boost = @@ -1518,25 +1542,35 @@ impl ProtoArray { } Ok(children) } else { - Ok(self - .nodes + // Spec: [root for root in blocks.keys() if blocks[root].parent_root == node.root ...] + // (cached `self.children[i]` is the same set as the spec's filtered scan). + let indices = self + .children + .get(node.proto_node_index) + .ok_or(Error::InvalidNodeIndex(node.proto_node_index))?; + indices .iter() - .enumerate() - .filter(|(_, child_node)| { - child_node.parent() == Some(node.proto_node_index) - && child_node.get_parent_payload_status() == node.payload_status + .copied() + .filter_map(|i| { + self.nodes + .get(i) + .ok_or(Error::InvalidNodeIndex(i)) + .map(|child| { + // Spec: node.payload_status == get_parent_payload_status(store, blocks[root]) + (child.get_parent_payload_status() == node.payload_status).then(|| { + ( + IndexedForkChoiceNode { + root: child.root(), + proto_node_index: i, + payload_status: PayloadStatus::Pending, + }, + child.clone(), + ) + }) + }) + .transpose() }) - .map(|(child_index, child_node)| { - ( - IndexedForkChoiceNode { - root: child_node.root(), - proto_node_index: child_index, - payload_status: PayloadStatus::Pending, - }, - child_node.clone(), - ) - }) - .collect()) + .collect() } } @@ -1667,6 +1701,19 @@ impl ProtoArray { // Drop all the nodes prior to finalization. self.nodes = self.nodes.split_off(finalized_index); + // Drop pruned entries from children index and shift all remaining indices down. + // Invariant: child_index > parent_index, and all parents we kept have + // index >= finalized_index, so every remaining child_index is also + // >= finalized_index. + self.children = self.children.split_off(finalized_index); + for children in self.children.iter_mut() { + for child_index in children.iter_mut() { + *child_index = child_index + .checked_sub(finalized_index) + .ok_or(Error::IndexOverflow("children"))?; + } + } + // Adjust the indices map. for (_root, index) in self.indices.iter_mut() { *index = index @@ -1911,10 +1958,7 @@ fn get_proposer_score( justified_balances: &JustifiedBalances, spec: &ChainSpec, ) -> Result { - let Some(proposer_score_boost) = spec.proposer_score_boost else { - return Ok(0); - }; - calculate_committee_fraction::(justified_balances, proposer_score_boost) + calculate_committee_fraction::(justified_balances, spec.proposer_score_boost) .ok_or(Error::ProposerBoostOverflow(0)) } diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 6ab2398f48..62a5c6e8e9 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -514,6 +514,7 @@ impl ProtoArrayForkChoice { prune_threshold: DEFAULT_PRUNE_THRESHOLD, nodes: Vec::with_capacity(1), indices: HashMap::with_capacity(1), + children: Vec::with_capacity(1), }; let block = Block { diff --git a/consensus/proto_array/src/ssz_container.rs b/consensus/proto_array/src/ssz_container.rs index 69efb35027..ec70e88a73 100644 --- a/consensus/proto_array/src/ssz_container.rs +++ b/consensus/proto_array/src/ssz_container.rs @@ -59,11 +59,13 @@ impl TryFrom<(SszContainerV29, JustifiedBalances)> for ProtoArrayForkChoice { type Error = Error; fn try_from((from, balances): (SszContainerV29, JustifiedBalances)) -> Result { - let proto_array = ProtoArray { + let mut proto_array = ProtoArray { prune_threshold: from.prune_threshold, nodes: from.nodes, indices: from.indices.into_iter().collect::>(), + children: Vec::new(), }; + proto_array.rebuild_children_index()?; Ok(Self { proto_array, diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 25dcb4ba06..9ccaa86579 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -151,7 +151,7 @@ pub struct ChainSpec { /* * Fork choice */ - pub proposer_score_boost: Option, + pub proposer_score_boost: u64, pub reorg_head_weight_threshold: u64, pub reorg_parent_weight_threshold: u64, pub reorg_max_epochs_since_finalization: u64, @@ -1162,7 +1162,7 @@ impl ChainSpec { /* * Fork choice */ - proposer_score_boost: Some(40), + proposer_score_boost: 40, reorg_head_weight_threshold: 20, reorg_parent_weight_threshold: 160, reorg_max_epochs_since_finalization: 2, @@ -1587,7 +1587,7 @@ impl ChainSpec { /* * Fork choice */ - proposer_score_boost: Some(40), + proposer_score_boost: 40, reorg_head_weight_threshold: 20, reorg_parent_weight_threshold: 160, reorg_max_epochs_since_finalization: 2, @@ -2640,7 +2640,9 @@ impl Config { min_per_epoch_churn_limit: spec.min_per_epoch_churn_limit, max_per_epoch_activation_churn_limit: spec.max_per_epoch_activation_churn_limit, - proposer_score_boost: spec.proposer_score_boost.map(|value| MaybeQuoted { value }), + proposer_score_boost: Some(MaybeQuoted { + value: spec.proposer_score_boost, + }), reorg_head_weight_threshold: spec.reorg_head_weight_threshold, reorg_parent_weight_threshold: spec.reorg_parent_weight_threshold, reorg_max_epochs_since_finalization: spec.reorg_max_epochs_since_finalization, @@ -2854,7 +2856,9 @@ impl Config { min_per_epoch_churn_limit, max_per_epoch_activation_churn_limit, churn_limit_quotient, - proposer_score_boost: proposer_score_boost.map(|q| q.value), + proposer_score_boost: proposer_score_boost + .map(|q| q.value) + .unwrap_or(chain_spec.proposer_score_boost), reorg_head_weight_threshold, reorg_parent_weight_threshold, reorg_max_epochs_since_finalization, diff --git a/consensus/types/src/fork/fork_context.rs b/consensus/types/src/fork/fork_context.rs index 3407689e79..f563578237 100644 --- a/consensus/types/src/fork/fork_context.rs +++ b/consensus/types/src/fork/fork_context.rs @@ -93,14 +93,16 @@ impl ForkContext { pub fn current_fork_digest(&self) -> [u8; 4] { self.current_fork.read().fork_digest } - - /// Returns the next fork digest. If there's no future fork, returns the current fork digest. - pub fn next_fork_digest(&self) -> Option<[u8; 4]> { + /// Per [spec](https://github.com/ethereum/consensus-specs/blob/1baa05e71148b0975e28918ac6022d2256b56f4a/specs/fulu/p2p-interface.md?plain=1#L636-L637) + /// `nfd` must be zero-valued when no next fork is scheduled. + /// Returns the next fork digest. If there's no future fork, returns zero-valued bytes. + pub fn next_fork_digest(&self) -> [u8; 4] { let current_fork_epoch = self.current_fork_epoch(); self.epoch_to_forks .range(current_fork_epoch..) .nth(1) .map(|(_, fork)| fork.fork_digest) + .unwrap_or_default() } /// Updates the `digest_epoch` field to a new digest epoch. @@ -222,11 +224,46 @@ mod tests { let context = ForkContext::new::(electra_slot, genesis_root, &spec); - let next_digest = context.next_fork_digest().unwrap(); + let next_digest = context.next_fork_digest(); let expected_digest = spec.compute_fork_digest(genesis_root, spec.fulu_fork_epoch.unwrap()); assert_eq!(next_digest, expected_digest); } + #[test] + fn test_next_fork_digest_returns_zero_when_no_next_fork() { + let spec = make_chain_spec(); + let genesis_root = Hash256::ZERO; + // Epoch 100 is the last BPO fork in make_chain_spec + let last_bpo_slot = Epoch::new(100).end_slot(E::slots_per_epoch()); + + let context = ForkContext::new::(last_bpo_slot, genesis_root, &spec); + + // No next fork after the last BPO epoch — must return zero bytes per spec + assert_eq!(context.next_fork_digest(), [0u8; 4]); + } + + #[test] + fn test_next_fork_digest_zero_after_runtime_transition_to_last_fork() { + let spec = make_chain_spec(); + let genesis_root = Hash256::ZERO; + // Start at Gloas (epoch 7) + let gloas_epoch = spec.gloas_fork_epoch.unwrap(); + let gloas_slot = gloas_epoch.end_slot(E::slots_per_epoch()); + + let context = ForkContext::new::(gloas_slot, genesis_root, &spec); + + // Before: next fork exists (BPO at epoch 50) + let bpo_50_digest = spec.compute_fork_digest(genesis_root, Epoch::new(50)); + assert_eq!(context.next_fork_digest(), bpo_50_digest); + + // Simulate runtime transition to the last BPO fork (epoch 100) + let last_digest = spec.compute_fork_digest(genesis_root, Epoch::new(100)); + context.update_current_fork(ForkName::Gloas, last_digest, Epoch::new(100)); + + // After: no next fork — must return zero bytes per spec + assert_eq!(context.next_fork_digest(), [0u8; 4]); + } + #[test] fn test_get_fork_from_context_bytes() { let spec = make_chain_spec(); diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 1736cd951f..f640583189 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -767,7 +767,8 @@ impl Tester { ))? .map(|avail: AvailabilityProcessingStatus| avail.try_into()); let success = blob_success && result.as_ref().is_ok_and(|inner| inner.is_ok()); - if success != valid { + // Only assert valid blocks import; blob-DA failure cases are expected to import now. + if valid && !success { return Err(Error::DidntFail(format!( "block with root {} was valid={} whilst test expects valid={}. result: {:?}", block_root, diff --git a/testing/validator_test_rig/Cargo.toml b/testing/validator_test_rig/Cargo.toml index 2057a9fdc8..dc8b2babd7 100644 --- a/testing/validator_test_rig/Cargo.toml +++ b/testing/validator_test_rig/Cargo.toml @@ -5,6 +5,7 @@ edition = { workspace = true } [dependencies] eth2 = { workspace = true } +ethereum_ssz = { workspace = true } mockito = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index 1ecdd85f3b..4501379d25 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -4,18 +4,23 @@ use mockito::{Matcher, Mock, Server, ServerGuard}; use regex::Regex; use reqwest::StatusCode; use sensitive_url::SensitiveUrl; +use ssz::Decode; use std::marker::PhantomData; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; use tracing::info; -use types::{ChainSpec, ConfigAndPreset, EthSpec, SignedBlindedBeaconBlock}; +use types::{ + ChainSpec, ConfigAndPreset, EthSpec, ForkName, PayloadAttestationData, + PayloadAttestationMessage, SignedBlindedBeaconBlock, Slot, +}; pub struct MockBeaconNode { server: ServerGuard, pub beacon_api_client: BeaconNodeHttpClient, _phantom: PhantomData, pub received_blocks: Arc>>>, + pub payload_attestation_message: Arc>>, } impl MockBeaconNode { @@ -31,6 +36,7 @@ impl MockBeaconNode { beacon_api_client, _phantom: PhantomData, received_blocks: Arc::new(Mutex::new(Vec::new())), + payload_attestation_message: Arc::new(Mutex::new(Vec::new())), } } @@ -124,4 +130,112 @@ impl MockBeaconNode { ) .create() } + + /// Mocks `GET /eth/v1/validator/payload_attestations_data/{slot}` + pub fn mock_get_validator_payload_attestation_data( + &mut self, + data: &PayloadAttestationData, + fork_name: ForkName, + slot: Slot, + ) -> Mock { + let path_pattern = Regex::new(&format!( + r"^/eth/v1/validator/payload_attestation_data/{}$", + slot.as_u64() + )) + .unwrap(); + + let body = serde_json::json!({ + "version": fork_name.to_string(), + "data": data, + }); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(serde_json::to_string(&body).unwrap()) + .create() + } + + /// Mocks `GET /eth/v1/validator/payload_attestation_data/{slot}` returning error + pub fn mock_get_validator_payload_attestation_data_error(&mut self, slot: Slot) -> Mock { + let path_pattern = Regex::new(&format!( + r"^/eth/v1/validator/payload_attestation_data/{}$", + slot.as_u64() + )) + .unwrap(); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .with_status(500) + .with_header("content-type", "application/json") + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } + + /// Mocks `POST /eth/v1/beacon/pool/payload_attestations` + pub fn mock_post_beacon_pool_payload_attestations(&mut self) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/beacon/pool/payload_attestations$").unwrap(); + let payload_attestation_message = Arc::clone(&self.payload_attestation_message); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/json") + .with_status(200) + .with_body_from_request(move |request| { + let body = request.body().expect("Failed to get request body"); + let message: Vec = serde_json::from_slice(body) + .expect("Failed to deserialize payload attestations"); + payload_attestation_message.lock().unwrap().extend(message); + vec![] + }) + .create() + } + + /// Mocks `POST /eth/v1/beacon/pool/payload_attestations` (SSZ) with an optional `delay`. + pub fn mock_post_beacon_pool_payload_attestations_ssz(&mut self, delay: Duration) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/beacon/pool/payload_attestations$").unwrap(); + let url = self.server.url(); + + let payload_attestation_message = Arc::clone(&self.payload_attestation_message); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .with_status(200) + .with_body_from_request(move |request| { + info!( + "Received payload attestation SSZ on server {} with delay {} ms", + url, + delay.as_secs(), + ); + let body = request.body().expect("Failed to get request body"); + + let chunk_size = ::ssz_fixed_len(); + let messages: Vec = body + .chunks(chunk_size) + .map(|chunk| { + PayloadAttestationMessage::from_ssz_bytes(chunk) + .expect("Failed to deserialize PayloadAttestationMessage from SSZ") + }) + .collect(); + + payload_attestation_message.lock().unwrap().extend(messages); + std::thread::sleep(delay); + vec![] + }) + .create() + } + + /// Mocks `POST /eth/v1/beacon/pool/payload_attestations` (SSZ) returning error + pub fn mock_post_beacon_pool_payload_attestations_ssz_error(&mut self) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/beacon/pool/payload_attestations$").unwrap(); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .with_status(500) + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } } diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index 2582968265..f2fbf193e1 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -23,3 +23,20 @@ tree_hash = { workspace = true } types = { workspace = true } validator_metrics = { workspace = true } validator_store = { workspace = true } + +[dev-dependencies] +account_utils = { workspace = true } +eth2_keystore = { workspace = true } +initialized_validators = { workspace = true } +lighthouse_validator_store = { workspace = true } +mockito = { workspace = true } +slashing_protection = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = [ + "rt-multi-thread", + "sync", + "signal", + "macros", + "test-util", +] } +validator_test_rig = { workspace = true } diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 1dd1878f4c..06fd14360a 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -659,7 +659,7 @@ impl BlockService { .beacon_nodes .first_success(|beacon_node| async move { beacon_node - .get_validator_execution_payload_envelope_ssz::(slot) + .get_validator_execution_payload_envelopes_ssz::(slot) .await .map_err(|e| { BlockError::Recoverable(format!( @@ -702,7 +702,7 @@ impl BlockService { let signed_envelope = signed_envelope.clone(); async move { beacon_node - .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .post_beacon_execution_payload_envelopes_ssz(&signed_envelope, fork_name) .await .map_err(|e| { BlockError::Recoverable(format!( diff --git a/validator_client/validator_services/src/payload_attestation_service.rs b/validator_client/validator_services/src/payload_attestation_service.rs index f41893941f..f4cd26552a 100644 --- a/validator_client/validator_services/src/payload_attestation_service.rs +++ b/validator_client/validator_services/src/payload_attestation_service.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::time::sleep; use tracing::{debug, error, info}; -use types::{ChainSpec, EthSpec}; +use types::{ChainSpec, EthSpec, Slot}; use validator_store::ValidatorStore; pub struct Inner { @@ -39,7 +39,11 @@ impl Deref for PayloadAttestationService { } } -impl PayloadAttestationService { +impl PayloadAttestationService +where + S: ValidatorStore + 'static, + T: SlotClock + 'static, +{ pub fn new( duties_service: Arc>, validator_store: Arc, @@ -61,11 +65,8 @@ impl PayloadAttestationServ } pub fn start_update_service(self) -> Result<(), String> { - let slot_duration = self.chain_spec.get_slot_duration(); - let payload_attestation_due = self.chain_spec.get_payload_attestation_due(); - info!( - payload_attestation_due_ms = payload_attestation_due.as_millis(), + payload_attestation_due_ms = self.chain_spec.get_payload_attestation_due().as_millis(), "Payload attestation service started" ); @@ -73,46 +74,7 @@ impl PayloadAttestationServ let interval_fut = async move { loop { - let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { - error!("Failed to read slot clock"); - sleep(slot_duration).await; - continue; - }; - - let Some(current_slot) = self.slot_clock.now() else { - error!("Failed to read slot clock after trigger"); - continue; - }; - - if !self - .chain_spec - .fork_name_at_slot::(current_slot) - .gloas_enabled() - { - let duration_to_next_epoch = self - .slot_clock - .duration_to_next_epoch(S::E::slots_per_epoch()) - .unwrap_or_else(|| { - self.chain_spec.get_slot_duration() * S::E::slots_per_epoch() as u32 - }); - sleep(duration_to_next_epoch).await; - continue; - } - - sleep(duration_to_next_slot + payload_attestation_due).await; - - let Some(attestation_slot) = self.slot_clock.now() else { - error!("Failed to read slot clock after sleep"); - continue; - }; - - let service = self.clone(); - self.executor.spawn( - async move { - service.produce_and_publish(attestation_slot).await; - }, - "payload_attestation_producer", - ); + self.run_update().await; } }; @@ -120,6 +82,60 @@ impl PayloadAttestationServ Ok(()) } + async fn run_update(&self) { + let Some(attestation_slot) = self.wait_for_attestation_slot().await else { + return; + }; + + let service = self.clone(); + self.executor.spawn( + async move { + service.produce_and_publish(attestation_slot).await; + }, + "payload_attestation_producer", + ); + } + + async fn wait_for_attestation_slot(&self) -> Option { + let slot_duration = self.chain_spec.get_slot_duration(); + let payload_attestation_due = self.chain_spec.get_payload_attestation_due(); + + let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { + error!("Failed to read slot clock"); + sleep(slot_duration).await; + return None; + }; + + let Some(current_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after trigger"); + return None; + }; + + if !self + .chain_spec + .fork_name_at_slot::(current_slot) + .gloas_enabled() + { + let duration_to_next_epoch = self + .slot_clock + .duration_to_next_epoch(S::E::slots_per_epoch()) + .unwrap_or_else(|| { + self.chain_spec.get_slot_duration() * S::E::slots_per_epoch() as u32 + }); + sleep(duration_to_next_epoch).await; + return None; + } + + sleep(duration_to_next_slot + payload_attestation_due).await; + + let Some(attestation_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after sleep"); + return None; + }; + + Some(attestation_slot) + } + async fn produce_and_publish(&self, slot: types::Slot) { let duties = self.duties_service.get_ptc_duties_for_slot(slot); @@ -249,3 +265,469 @@ impl PayloadAttestationServ } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::duties_service::DutiesServiceBuilder; + use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; + use beacon_node_fallback::{ + BeaconNodeFallback, CandidateBeaconNode, Config as BeaconNodeConfig, + }; + use bls::{Keypair, PublicKeyBytes}; + use eth2::types::PtcDuty; + use eth2_keystore::KeystoreBuilder; + use futures::FutureExt; + use initialized_validators::InitializedValidators; + use lighthouse_validator_store::LighthouseValidatorStore; + use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; + use slot_clock::ManualSlotClock; + use std::sync::Arc; + use std::time::Duration; + use task_executor::test_utils::TestRuntime; + use tempfile::{TempDir, tempdir}; + use types::{Epoch, ForkName, Hash256, MainnetEthSpec, PayloadAttestationData, Slot}; + use validator_test_rig::mock_beacon_node::MockBeaconNode; + + type E = MainnetEthSpec; + type S = LighthouseValidatorStore; + + async fn create_validator_store( + slot_clock: ManualSlotClock, + spec: Arc, + executor: TaskExecutor, + num_validators: usize, + ) -> (Arc, Vec, TempDir) { + let validator_dir = tempdir().unwrap(); + let password = b"test"; + + let mut validator_definitions = Vec::with_capacity(num_validators); + let mut pubkeys = Vec::with_capacity(num_validators); + + for i in 0..num_validators { + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password, String::new()) + .unwrap() + .build() + .unwrap(); + let keystore_path = validator_dir + .path() + .join(format!("voting-keystore-{i}.json")); + keystore + .to_json_writer(std::fs::File::create(&keystore_path).unwrap()) + .unwrap(); + + let validator_definition = ValidatorDefinition::new_keystore_with_password( + keystore_path, + PasswordStorage::ValidatorDefinitions( + String::from_utf8(password.to_vec()).unwrap().into(), + ), + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + + pubkeys.push(keypair.pk.into()); + validator_definitions.push(validator_definition); + } + + let initialized_validators = InitializedValidators::from_definitions( + validator_definitions.into(), + validator_dir.path().into(), + Default::default(), + ) + .await + .unwrap(); + + let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); + + let validator_store = Arc::new(LighthouseValidatorStore::<_, E>::new( + initialized_validators, + slashing_protection, + Hash256::ZERO, + spec, + None, + slot_clock, + &Default::default(), + executor, + )); + + for (i, pubkey) in pubkeys.iter().enumerate() { + validator_store.set_validator_index(pubkey, i as u64); + } + + (validator_store, pubkeys, validator_dir) + } + + struct TestHarness { + mock_beacon_node_1: MockBeaconNode, + mock_beacon_node_2: MockBeaconNode, + service: PayloadAttestationService, + pubkeys: Vec, + _test_runtime: TestRuntime, + _validator_dir: TempDir, + } + + impl TestHarness { + async fn create_validators(num_validators: usize) -> Self { + let mut default_spec = MainnetEthSpec::default_spec(); + default_spec.gloas_fork_epoch = Some(Epoch::new(0)); + let spec = Arc::new(default_spec); + + let test_runtime = TestRuntime::default(); + let executor = test_runtime.task_executor.clone(); + let slot_duration = spec.get_slot_duration(); + let slot_clock = + ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration); + + let (validator_store, pubkeys, validator_dir) = create_validator_store( + slot_clock.clone(), + spec.clone(), + executor.clone(), + num_validators, + ) + .await; + + let mock_beacon_node_1 = MockBeaconNode::::new().await; + let mock_beacon_node_2 = MockBeaconNode::::new().await; + + let beacon_node_1 = + CandidateBeaconNode::new(mock_beacon_node_1.beacon_api_client.clone(), 0); + let beacon_node_2 = + CandidateBeaconNode::new(mock_beacon_node_2.beacon_api_client.clone(), 1); + + let beacon_node_fallback = Arc::new(BeaconNodeFallback::new( + vec![beacon_node_1, beacon_node_2], + BeaconNodeConfig::default(), + vec![], + spec.clone(), + )); + + let duties_service = Arc::new( + DutiesServiceBuilder::new() + .validator_store(validator_store.clone()) + .slot_clock(slot_clock.clone()) + .beacon_nodes(beacon_node_fallback.clone()) + .executor(executor.clone()) + .spec(spec.clone()) + .build() + .unwrap(), + ); + + let service = PayloadAttestationService::new( + duties_service, + validator_store, + slot_clock, + beacon_node_fallback, + executor, + spec, + ); + + Self { + mock_beacon_node_1, + mock_beacon_node_2, + service, + pubkeys, + _test_runtime: test_runtime, + _validator_dir: validator_dir, + } + } + + fn insert_ptc_duties(&self, slot: Slot) { + let duties = self + .pubkeys + .iter() + .enumerate() + .map(|(i, pubkey)| PtcDuty { + pubkey: *pubkey, + validator_index: i as u64, + slot, + }) + .collect(); + self.service + .duties_service + .ptc_duties + .write() + .insert(Epoch::new(0), (Hash256::ZERO, duties)); + } + } + + // advance_time so that we don't have to wait for real-time to elapse in the test + async fn advance_time(slot_clock: &ManualSlotClock, duration: Duration) { + slot_clock.advance_time(duration); + tokio::time::advance(duration).await; + } + + #[tokio::test] + async fn test_wait_for_attestation_slot() { + tokio::time::pause(); + + let harness = TestHarness::create_validators(1).await; + let service = &harness.service; + let service_wait = service.wait_for_attestation_slot(); + tokio::pin!(service_wait); + + // This first call of .now_or_never() starts the timer and registers the sleep timer with tokio + // It calls sleep(duration_to_next_slot + payload_attestation_due).await which registers a timer with a deadline of 21s + assert!(service_wait.as_mut().now_or_never().is_none()); + + let duration_to_next_slot = harness.service.slot_clock.duration_to_next_slot().unwrap(); + let payload_attestation_due = harness.service.chain_spec.get_payload_attestation_due(); + let duration_to_wait = duration_to_next_slot + payload_attestation_due; + // Advance both slot_clock and tokio::time to 21s (the sleep deadline) + // The timer hasn't fired yet because tokio requires time to be strictly past the deadline. + // so the following assert! should return None + // This verifies that the function wait_for_attestation_slot waits for the correct duration before returning a slot. + advance_time(&harness.service.slot_clock, duration_to_wait).await; + assert!( + service_wait.as_mut().now_or_never().is_none(), + "Function should return None before the sleep duration has elapsed" + ); + + // Advance time for 1 more second, the sleep should have completed and the function should return Some(attestation_slot) + // slot_clock is now at 22s, which is slot 1 + // Removing this advance_time should cause the following assert_eq! to fail + advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(Slot::new(1)) + ); + } + + #[tokio::test] + async fn publish_payload_attestation_ssz() { + let mut harness = TestHarness::create_validators(1).await; + + let attestation_slot = Slot::new(1); + harness.insert_ptc_duties(attestation_slot); + + let expected_payload_attestation = PayloadAttestationData { + beacon_block_root: Hash256::ZERO, + slot: attestation_slot, + payload_present: true, + blob_data_available: true, + }; + + harness + .mock_beacon_node_1 + .mock_get_validator_payload_attestation_data( + &expected_payload_attestation, + ForkName::Gloas, + attestation_slot, + ); + + let mock_ssz = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz(Duration::from_secs(0)); + let mock_json = harness + .mock_beacon_node_2 + .mock_post_beacon_pool_payload_attestations(); + + let service = harness.service; + service.produce_and_publish(attestation_slot).await; + + let messages = harness + .mock_beacon_node_1 + .payload_attestation_message + .lock() + .unwrap(); + + // We create one validator with one PTC duty, so the PayloadAttestationMessage length should be 1 + assert_eq!( + messages.len(), + 1, + "Expected one payload attestation message" + ); + + // First try on beacon_node_1 (mock_ssz) is successful + // therefore mock_json is not hit at all + mock_ssz.expect(1).assert(); + mock_json.expect(0).assert(); + + let result = &messages[0]; + assert_eq!(result.validator_index, 0); + assert_eq!( + result.data.beacon_block_root, + expected_payload_attestation.beacon_block_root + ); + assert_eq!(result.data.slot, attestation_slot); + assert!(result.data.payload_present); + assert!(result.data.blob_data_available); + } + + #[tokio::test] + async fn publish_payload_attestation_ssz_fails_fallback_to_json() { + let mut harness = TestHarness::create_validators(1).await; + + let attestation_slot = Slot::new(1); + harness.insert_ptc_duties(attestation_slot); + + let expected_payload_attestation = PayloadAttestationData { + beacon_block_root: Hash256::ZERO, + slot: attestation_slot, + payload_present: true, + blob_data_available: true, + }; + + harness + .mock_beacon_node_1 + .mock_get_validator_payload_attestation_data( + &expected_payload_attestation, + ForkName::Gloas, + Slot::new(1), + ); + + // mock_ssz returns 500 to simulate BN does not support SSZ, so that it fallbacks to mock_json + let mock_ssz = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz_error(); + let mock_json = harness + .mock_beacon_node_2 + .mock_post_beacon_pool_payload_attestations(); + + let service = harness.service; + service.produce_and_publish(attestation_slot).await; + + // first_success function tries both beacon nodes for SSZ post payload attestation: + // first pass: both fail (mock_ssz returns 500, mock_json does not support SSZ) + // second pass: repeats the first pass + // Therefore mock_ssz is hit twice. + // When SSZ fails, it fallbacks to JSON and should succeed on first call on mock_json. + mock_ssz.expect(2).assert(); + mock_json.expect(1).assert(); + + let messages = harness + .mock_beacon_node_2 + .payload_attestation_message + .lock() + .unwrap(); + + assert_eq!( + messages.len(), + 1, + "Expected one payload attestation via JSON fallback" + ); + } + + #[tokio::test] + async fn no_duties_no_publish() { + let mut harness = TestHarness::create_validators(1).await; + + // we do not insert any duties in this test + let mock = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz(Duration::from_secs(0)); + + let service = harness.service; + + // when there is no duty, produce_and_publish should return early + // therefore, the beacon node is not called, expected to hit 0 + service.produce_and_publish(Slot::new(1)).await; + mock.expect(0).assert(); + + assert!( + harness + .mock_beacon_node_1 + .payload_attestation_message + .lock() + .unwrap() + .is_empty(), + "No payload attestation should be published when there are no duties" + ); + } + + #[tokio::test] + async fn test_get_payload_attestation_data_error() { + let mut harness = TestHarness::create_validators(1).await; + + let attestation_slot = Slot::new(1); + // We have PTC duties + harness.insert_ptc_duties(attestation_slot); + + // However, we simulate that both BNs have error in get_validator_payload_attestation_data + harness + .mock_beacon_node_1 + .mock_get_validator_payload_attestation_data_error(attestation_slot); + harness + .mock_beacon_node_2 + .mock_get_validator_payload_attestation_data_error(attestation_slot); + + let mock_ssz = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz(Duration::from_secs(0)); + let mock_json = harness + .mock_beacon_node_2 + .mock_post_beacon_pool_payload_attestations(); + + let service = harness.service; + // The produce_and_publish() should return early before reaching the POST endpoint + service.produce_and_publish(attestation_slot).await; + + // Both beacon nodes should not be called at all + mock_ssz.expect(0).assert(); + mock_json.expect(0).assert(); + + // No payload attestation message published + assert!( + harness + .mock_beacon_node_1 + .payload_attestation_message + .lock() + .unwrap() + .is_empty(), + "No payload attestation should be published when get data fails" + ); + } + + #[tokio::test] + async fn publish_multiple_payload_attestation_messages() { + // Create 3 validators with 1 PTC duty for each validator + let mut harness = TestHarness::create_validators(3).await; + + let attestation_slot = Slot::new(1); + harness.insert_ptc_duties(attestation_slot); + + let expected_payload_attestation = PayloadAttestationData { + beacon_block_root: Hash256::ZERO, + slot: attestation_slot, + payload_present: true, + blob_data_available: true, + }; + + harness + .mock_beacon_node_1 + .mock_get_validator_payload_attestation_data( + &expected_payload_attestation, + ForkName::Gloas, + attestation_slot, + ); + + let mock_ssz = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz(Duration::from_secs(0)); + + let service = harness.service; + service.produce_and_publish(attestation_slot).await; + + let messages = harness + .mock_beacon_node_1 + .payload_attestation_message + .lock() + .unwrap(); + + // With 3 PTC duties in total, we should have 3 PayloadAttestationMessage + assert_eq!( + messages.len(), + 3, + "Expected three payload attestation messages" + ); + // mock_ssz is only hit once + // this is to verify that a single call to the POST endpoint can publish multiple messages in one go + mock_ssz.expect(1).assert(); + } +}