diff --git a/Cargo.lock b/Cargo.lock index cb5e126ce..a8ce752aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -660,6 +660,7 @@ dependencies = [ "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types-debug", + "alloy-rpc-types-engine", "alloy-rpc-types-eth 2.0.5", "alloy-rpc-types-trace", "alloy-signer 2.0.5", @@ -1854,6 +1855,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-scoped" version = "0.9.0" @@ -1909,6 +1928,19 @@ dependencies = [ "rustc_version 0.4.1", ] +[[package]] +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + [[package]] name = "atomic" version = "0.6.1" @@ -1933,6 +1965,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http 1.4.0", + "log", + "url", +] + [[package]] name = "aurora-engine-modexp" version = "1.2.0" @@ -3162,6 +3206,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.0" @@ -3173,6 +3228,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.45" @@ -3222,6 +3290,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", "inout", + "zeroize", ] [[package]] @@ -4277,7 +4346,16 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", ] [[package]] @@ -4298,10 +4376,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -4309,10 +4399,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] +[[package]] +name = "discv5" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7999df38d0bd8f688212e1a4fae31fd2fea6d218649b9cd7c40bf3ec1318fc" +dependencies = [ + "aes", + "aes-gcm", + "alloy-rlp", + "arrayvec", + "ctr", + "delay_map", + "enr", + "fnv", + "futures", + "hashlink 0.11.0", + "hex", + "hkdf", + "lazy_static", + "libp2p-identity", + "more-asserts", + "multiaddr", + "parking_lot", + "rand 0.8.6", + "smallvec", + "socket2 0.6.3", + "tokio", + "tracing", + "uint 0.10.0", + "zeroize", +] + [[package]] name = "discv5" version = "0.10.4" @@ -4327,7 +4449,7 @@ dependencies = [ "enr", "fnv", "futures", - "hashlink", + "hashlink 0.11.0", "hex", "hkdf", "lazy_static", @@ -4393,6 +4515,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + [[package]] name = "dunce" version = "1.0.5" @@ -4981,6 +5109,16 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-bounded" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e" +dependencies = [ + "futures-timer", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -5014,6 +5152,16 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -5025,6 +5173,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls 0.23.40", + "rustls-pki-types", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -5385,6 +5544,24 @@ dependencies = [ "serde_core", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "hashlink" version = "0.11.0" @@ -5451,6 +5628,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + [[package]] name = "hickory-net" version = "0.26.1" @@ -5463,7 +5646,7 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "hickory-proto", + "hickory-proto 0.26.1", "idna", "ipnet", "jni 0.22.4", @@ -5475,6 +5658,32 @@ dependencies = [ "url", ] +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.4", + "ring", + "socket2 0.5.10", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + [[package]] name = "hickory-proto" version = "0.26.1" @@ -5496,6 +5705,27 @@ dependencies = [ "url", ] +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto 0.25.2", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.4", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "hickory-resolver" version = "0.26.1" @@ -5505,7 +5735,7 @@ dependencies = [ "cfg-if", "futures-util", "hickory-net", - "hickory-proto", + "hickory-proto 0.26.1", "ipconfig", "ipnet", "jni 0.22.4", @@ -5976,6 +6206,60 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "if-addrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "if-watch" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" +dependencies = [ + "async-io", + "core-foundation 0.9.4", + "fnv", + "futures", + "if-addrs 0.15.0", + "ipnet", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "rtnetlink", + "system-configuration", + "tokio", + "windows 0.62.2", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http 1.4.0", + "http-body-util", + "hyper 1.9.0", + "hyper-util", + "log", + "rand 0.9.4", + "tokio", + "url", + "xmltree", +] + [[package]] name = "imbl" version = "7.0.0" @@ -6590,6 +6874,7 @@ dependencies = [ "kona-genesis", "kona-registry", "libc", + "libp2p", "metrics-exporter-prometheus", "metrics-process", "serde", @@ -6660,6 +6945,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "kona-disc" +version = "0.1.2" +source = "git+https://github.com/ethereum-optimism/optimism?rev=423d93e6374a65f3a83c1ed5e29c5998c972a5da#423d93e6374a65f3a83c1ed5e29c5998c972a5da" +dependencies = [ + "alloy-rlp", + "backon", + "derive_more 2.1.1", + "discv5 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", + "kona-genesis", + "kona-macros", + "kona-peers", + "libp2p", + "rand 0.9.4", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "kona-driver" version = "0.4.0" @@ -6682,21 +6986,57 @@ dependencies = [ ] [[package]] -name = "kona-executor" -version = "0.4.0" +name = "kona-engine" +version = "0.1.2" source = "git+https://github.com/ethereum-optimism/optimism?rev=423d93e6374a65f3a83c1ed5e29c5998c972a5da#423d93e6374a65f3a83c1ed5e29c5998c972a5da" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", - "alloy-evm", - "alloy-op-evm", - "alloy-op-hardforks", + "alloy-hardforks", + "alloy-network 2.0.5", "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "kona-genesis", - "kona-mpt", - "kona-protocol", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth 2.0.5", + "alloy-transport", + "alloy-transport-http", + "async-trait", + "derive_more 2.1.1", + "http-body-util", + "kona-genesis", + "kona-macros", + "kona-protocol", + "op-alloy-consensus", + "op-alloy-network", + "op-alloy-provider", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower 0.5.3", + "tracing", + "url", +] + +[[package]] +name = "kona-executor" +version = "0.4.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=423d93e6374a65f3a83c1ed5e29c5998c972a5da#423d93e6374a65f3a83c1ed5e29c5998c972a5da" +dependencies = [ + "alloy-consensus 2.0.5", + "alloy-eips 2.0.5", + "alloy-evm", + "alloy-op-evm", + "alloy-op-hardforks", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "kona-genesis", + "kona-mpt", + "kona-protocol", "op-alloy-consensus", "op-alloy-rpc-types-engine", "op-revm", @@ -6726,6 +7066,39 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "kona-gossip" +version = "0.1.2" +source = "git+https://github.com/ethereum-optimism/optimism?rev=423d93e6374a65f3a83c1ed5e29c5998c972a5da#423d93e6374a65f3a83c1ed5e29c5998c972a5da" +dependencies = [ + "alloy-consensus 2.0.5", + "alloy-eips 2.0.5", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "derive_more 2.1.1", + "discv5 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures", + "ipnet", + "kona-disc", + "kona-genesis", + "kona-macros", + "kona-peers", + "lazy_static", + "libp2p", + "libp2p-identity", + "libp2p-stream", + "op-alloy-consensus", + "op-alloy-rpc-types-engine", + "openssl", + "serde", + "serde_repr", + "snap", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "kona-hardforks" version = "0.4.5" @@ -6830,6 +7203,80 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "kona-node-service" +version = "0.1.3" +source = "git+https://github.com/ethereum-optimism/optimism?rev=423d93e6374a65f3a83c1ed5e29c5998c972a5da#423d93e6374a65f3a83c1ed5e29c5998c972a5da" +dependencies = [ + "alloy-chains", + "alloy-eips 2.0.5", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth 2.0.5", + "alloy-signer 2.0.5", + "alloy-signer-local 2.0.5", + "alloy-transport", + "alloy-transport-http", + "async-stream", + "async-trait", + "backon", + "derive_more 2.1.1", + "discv5 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures", + "http-body-util", + "jsonrpsee", + "kona-derive", + "kona-disc", + "kona-engine", + "kona-genesis", + "kona-gossip", + "kona-interop", + "kona-macros", + "kona-peers", + "kona-protocol", + "kona-providers-alloy", + "kona-rpc", + "kona-sources", + "libp2p", + "libp2p-stream", + "op-alloy-network", + "op-alloy-provider", + "op-alloy-rpc-types-engine", + "strum 0.27.2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tower 0.5.3", + "tracing", + "url", +] + +[[package]] +name = "kona-peers" +version = "0.1.2" +source = "git+https://github.com/ethereum-optimism/optimism?rev=423d93e6374a65f3a83c1ed5e29c5998c972a5da#423d93e6374a65f3a83c1ed5e29c5998c972a5da" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more 2.1.1", + "dirs 6.0.0", + "discv5 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", + "kona-registry", + "lazy_static", + "libp2p", + "libp2p-identity", + "secp256k1 0.31.1", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "unsigned-varint 0.8.0", + "url", +] + [[package]] name = "kona-preimage" version = "0.3.0" @@ -6942,7 +7389,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "tracing-subscriber 0.3.23", - "unsigned-varint", + "unsigned-varint 0.8.0", ] [[package]] @@ -6997,6 +7444,59 @@ dependencies = [ "toml", ] +[[package]] +name = "kona-rpc" +version = "0.3.2" +source = "git+https://github.com/ethereum-optimism/optimism?rev=423d93e6374a65f3a83c1ed5e29c5998c972a5da#423d93e6374a65f3a83c1ed5e29c5998c972a5da" +dependencies = [ + "alloy-eips 2.0.5", + "alloy-primitives", + "alloy-rpc-types-engine", + "async-trait", + "backon", + "derive_more 2.1.1", + "getrandom 0.3.4", + "ipnet", + "jsonrpsee", + "kona-engine", + "kona-genesis", + "kona-gossip", + "kona-macros", + "kona-protocol", + "libp2p", + "op-alloy-consensus", + "op-alloy-rpc-jsonrpsee", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", + "serde", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "kona-sources" +version = "0.1.2" +source = "git+https://github.com/ethereum-optimism/optimism?rev=423d93e6374a65f3a83c1ed5e29c5998c972a5da#423d93e6374a65f3a83c1ed5e29c5998c972a5da" +dependencies = [ + "alloy-primitives", + "alloy-rpc-client", + "alloy-signer 2.0.5", + "alloy-signer-local 2.0.5", + "alloy-transport", + "alloy-transport-http", + "derive_more 2.1.1", + "notify", + "op-alloy-rpc-types-engine", + "rustls 0.23.40", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", +] + [[package]] name = "kona-std-fpvm" version = "0.2.0" @@ -7132,6 +7632,153 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libp2p" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce71348bf5838e46449ae240631117b487073d5f347c06d434caddcb91dceb5a" +dependencies = [ + "bytes", + "either", + "futures", + "futures-timer", + "getrandom 0.2.17", + "libp2p-allow-block-list", + "libp2p-connection-limits", + "libp2p-core", + "libp2p-dns", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-identity", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-noise", + "libp2p-ping", + "libp2p-quic", + "libp2p-swarm", + "libp2p-tcp", + "libp2p-upnp", + "libp2p-yamux", + "multiaddr", + "pin-project", + "rw-stream-sink", + "thiserror 2.0.18", +] + +[[package]] +name = "libp2p-allow-block-list" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-connection-limits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-core" +version = "0.43.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249128cd37a2199aff30a7675dffa51caf073b51aa612d2f544b19932b9aebca" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-identity", + "multiaddr", + "multihash", + "multistream-select", + "parking_lot", + "pin-project", + "quick-protobuf", + "rand 0.8.6", + "rw-stream-sink", + "thiserror 2.0.18", + "tracing", + "unsigned-varint 0.8.0", + "web-time", +] + +[[package]] +name = "libp2p-dns" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" +dependencies = [ + "async-trait", + "futures", + "hickory-resolver 0.25.2", + "libp2p-core", + "libp2p-identity", + "parking_lot", + "smallvec", + "tracing", +] + +[[package]] +name = "libp2p-gossipsub" +version = "0.49.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a538e571cd38f504f761c61b8f79127489ea7a7d6f05c41ca15d31ffb5726326" +dependencies = [ + "async-channel", + "asynchronous-codec", + "base64 0.22.1", + "byteorder", + "bytes", + "either", + "fnv", + "futures", + "futures-timer", + "getrandom 0.2.17", + "hashlink 0.9.1", + "hex_fmt", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.6", + "regex", + "sha2 0.10.9", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-identify" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "libp2p-identity" version = "0.2.13" @@ -7145,12 +7792,223 @@ dependencies = [ "k256", "multihash", "quick-protobuf", + "rand 0.8.6", "sha2 0.10.9", "thiserror 2.0.18", "tracing", "zeroize", ] +[[package]] +name = "libp2p-mdns" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" +dependencies = [ + "futures", + "hickory-proto 0.25.2", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "smallvec", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-metrics" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-identity", + "libp2p-ping", + "libp2p-swarm", + "pin-project", + "prometheus-client", + "web-time", +] + +[[package]] +name = "libp2p-noise" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core", + "libp2p-identity", + "multiaddr", + "multihash", + "quick-protobuf", + "rand 0.8.6", + "snow", + "static_assertions", + "thiserror 2.0.18", + "tracing", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "libp2p-ping" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74bb7fcdfd9fead4144a3859da0b49576f171a8c8c7c0bfc7c541921d25e60d3" +dependencies = [ + "futures", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-quic" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-tls", + "quinn", + "rand 0.8.6", + "ring", + "rustls 0.23.40", + "socket2 0.5.10", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-stream" +version = "0.4.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6bd8025c80205ec2810cfb28b02f362ab48a01bee32c50ab5f12761e033464" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "tracing", +] + +[[package]] +name = "libp2p-swarm" +version = "0.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce88c6c4bf746c8482480345ea3edfd08301f49e026889d1cbccfa1808a9ed9e" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "hashlink 0.10.0", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm-derive", + "multistream-select", + "rand 0.8.6", + "smallvec", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" +dependencies = [ + "heck", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "libp2p-tcp" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6585b9309699f58704ec9ab0bb102eca7a3777170fa91a8678d73ca9cafa93" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libc", + "libp2p-core", + "socket2 0.6.3", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-tls" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" +dependencies = [ + "futures", + "futures-rustls", + "libp2p-core", + "libp2p-identity", + "rcgen", + "ring", + "rustls 0.23.40", + "rustls-webpki 0.103.13", + "thiserror 2.0.18", + "x509-parser 0.17.0", + "yasna", +] + +[[package]] +name = "libp2p-upnp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" +dependencies = [ + "futures", + "futures-timer", + "igd-next", + "libp2p-core", + "libp2p-swarm", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-yamux" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" +dependencies = [ + "either", + "futures", + "libp2p-core", + "thiserror 2.0.18", + "tracing", + "yamux 0.12.1", + "yamux 0.13.10", +] + [[package]] name = "libproc" version = "0.14.11" @@ -7677,7 +8535,7 @@ dependencies = [ "percent-encoding", "serde", "static_assertions", - "unsigned-varint", + "unsigned-varint 0.8.0", "url", ] @@ -7699,7 +8557,7 @@ version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" dependencies = [ - "unsigned-varint", + "unsigned-varint 0.8.0", ] [[package]] @@ -7708,6 +8566,20 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "multistream-select" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project", + "smallvec", + "unsigned-varint 0.7.2", +] + [[package]] name = "munge" version = "0.4.7" @@ -7746,10 +8618,58 @@ dependencies = [ ] [[package]] -name = "ndk-context" -version = "0.1.1" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags 2.11.1", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] [[package]] name = "nix" @@ -7776,6 +8696,18 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nix" version = "0.31.3" @@ -7788,6 +8720,12 @@ dependencies = [ "libc", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -8197,6 +9135,7 @@ dependencies = [ "alloy-serde 2.0.5", "alloy-signer 2.0.5", "derive_more 2.1.1", + "jsonrpsee", "op-alloy-consensus", "reth-rpc-traits", "serde", @@ -8271,6 +9210,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-src" +version = "300.6.1+3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.116" @@ -8279,6 +9227,7 @@ checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -9011,6 +9960,31 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -9191,6 +10165,29 @@ dependencies = [ "hex", ] +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "proptest" version = "1.11.0" @@ -9376,6 +10373,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "quick-protobuf-codec" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +dependencies = [ + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror 1.0.69", + "unsigned-varint 0.8.0", +] + [[package]] name = "quinn" version = "0.11.9" @@ -9384,6 +10394,7 @@ checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", + "futures-io", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -9497,7 +10508,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "chacha20", + "chacha20 0.10.0", "getrandom 0.4.2", "rand_core 0.10.1", ] @@ -9688,6 +10699,19 @@ dependencies = [ "rayon", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -9723,6 +10747,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -10308,7 +11343,7 @@ source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59c dependencies = [ "alloy-primitives", "alloy-rlp", - "discv5", + "discv5 0.10.4 (git+https://github.com/sigp/discv5?rev=7663c00)", "enr", "itertools 0.14.0", "parking_lot", @@ -10334,7 +11369,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "derive_more 2.1.1", - "discv5", + "discv5 0.10.4 (git+https://github.com/sigp/discv5?rev=7663c00)", "enr", "futures", "itertools 0.14.0", @@ -10359,7 +11394,7 @@ dependencies = [ "dashmap", "data-encoding", "enr", - "hickory-resolver", + "hickory-resolver 0.26.1", "linked_hash_set", "reth-ethereum-forks", "reth-network-peers", @@ -11122,7 +12157,7 @@ version = "2.3.0" source = "git+https://github.com/paradigmxyz/reth?tag=v2.3.0#9384bc53d8c0c77e59cac83fdaaf3b372c6d2216" dependencies = [ "futures-util", - "if-addrs", + "if-addrs 0.14.0", "reqwest 0.13.3", "serde_with", "thiserror 2.0.18", @@ -11142,7 +12177,7 @@ dependencies = [ "aquamarine", "auto_impl", "derive_more 2.1.1", - "discv5", + "discv5 0.10.4 (git+https://github.com/sigp/discv5?rev=7663c00)", "enr", "futures", "itertools 0.14.0", @@ -13414,6 +14449,24 @@ dependencies = [ "paste", ] +[[package]] +name = "rtnetlink" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "nix 0.30.1", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "ruint" version = "1.18.0" @@ -13667,6 +14720,17 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "rw-stream-sink" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + [[package]] name = "ryu" version = "1.0.23" @@ -14958,6 +16022,23 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "rand_core 0.6.4", + "ring", + "rustc_version 0.4.1", + "sha2 0.10.9", + "subtle", +] + [[package]] name = "snowbridge-amcl" version = "1.0.2" @@ -15014,7 +16095,7 @@ dependencies = [ "cargo_metadata 0.18.1", "chrono", "clap", - "dirs", + "dirs 5.0.1", "sp1-primitives", ] @@ -15287,7 +16368,7 @@ dependencies = [ "anyhow", "bincode 1.3.3", "clap", - "dirs", + "dirs 5.0.1", "either", "enum-map", "eyre", @@ -15514,7 +16595,7 @@ dependencies = [ "backoff", "bincode 1.3.3", "cfg-if", - "dirs", + "dirs 5.0.1", "eventsource-stream", "futures", "hex", @@ -15557,7 +16638,7 @@ dependencies = [ "bincode 1.3.3", "blake3", "cfg-if", - "dirs", + "dirs 5.0.1", "hex", "lazy_static", "serde", @@ -16984,6 +18065,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + [[package]] name = "unsigned-varint" version = "0.8.0" @@ -18140,7 +19227,6 @@ dependencies = [ "reth-cli-util", "reth-node-builder", "reth-optimism-consensus", - "reth-provider", "reth-tracing", "tikv-jemallocator", "world-chain-chainspec", @@ -18259,9 +19345,20 @@ dependencies = [ "alloy-signer-local 2.0.5", "clap", "color-eyre", + "discv5 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", "ed25519-dalek", "futures-util", "hex", + "k256", + "kona-cli", + "kona-disc", + "kona-genesis", + "kona-gossip", + "kona-node-service", + "kona-peers", + "kona-sources", + "libp2p", + "reqwest 0.12.28", "reth-chainspec", "reth-cli", "reth-cli-commands", @@ -18280,6 +19377,7 @@ dependencies = [ "reth-rpc-server-types", "reth-tracing", "tracing", + "url", "world-chain-chainspec", ] @@ -18392,6 +19490,48 @@ dependencies = [ "world-chain-state", ] +[[package]] +name = "world-chain-kona" +version = "2.3.0" +dependencies = [ + "alloy-eips 2.0.5", + "alloy-network 2.0.5", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth 2.0.5", + "alloy-transport", + "alloy-transport-http", + "async-trait", + "color-eyre", + "ed25519-dalek", + "futures", + "jsonrpsee", + "kona-derive", + "kona-engine", + "kona-genesis", + "kona-node-service", + "kona-protocol", + "kona-providers-alloy", + "kona-registry", + "kona-rpc", + "op-alloy-network", + "op-alloy-provider", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", + "reth-engine-primitives", + "reth-optimism-node", + "reth-payload-builder", + "reth-payload-primitives", + "tokio", + "tokio-util", + "tracing", + "url", + "world-chain-cli", + "world-chain-primitives", +] + [[package]] name = "world-chain-nitro-worker" version = "2.3.0" @@ -18425,12 +19565,14 @@ dependencies = [ "color-eyre", "ed25519-dalek", "hex", + "kona-genesis", "op-alloy-consensus", "op-alloy-rpc-types-engine", "reth-basic-payload-builder", "reth-chainspec", "reth-codecs", "reth-db", + "reth-engine-primitives", "reth-eth-wire", "reth-eth-wire-types", "reth-evm", @@ -18453,7 +19595,9 @@ dependencies = [ "reth-rpc-engine-api", "reth-rpc-eth-api", "reth-rpc-server-types", + "reth-tasks", "reth-transaction-pool", + "serde_json", "tokio", "tracing", "vergen", @@ -18462,6 +19606,7 @@ dependencies = [ "world-chain-chainspec", "world-chain-cli", "world-chain-evm", + "world-chain-kona", "world-chain-p2p", "world-chain-payload", "world-chain-pool", @@ -18751,7 +19896,7 @@ dependencies = [ "tracing-subscriber 0.3.23", "world-chain-proof-core", "world-chain-proof-kona-client-utils", - "x509-parser", + "x509-parser 0.18.1", ] [[package]] @@ -19284,6 +20429,35 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "x509-parser" version = "0.18.1" @@ -19312,12 +20486,27 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + [[package]] name = "xmlparser" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "xtask" version = "2.3.0" @@ -19365,12 +20554,52 @@ dependencies = [ "world-chain-test-utils", ] +[[package]] +name = "yamux" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed0164ae619f2dc144909a9f082187ebb5893693d8c0196e8085283ccd4b776" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.8.6", + "static_assertions", +] + +[[package]] +name = "yamux" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1991f6690292030e31b0144d73f5e8368936c58e45e7068254f7138b23b00672" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.9.4", + "static_assertions", + "web-time", +] + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index d5031c47e..ffd28c374 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "crates/evm", "crates/state", "crates/pbh", + "crates/kona", "proofs/core", "proofs/protocol", "proofs/nitro", @@ -89,9 +90,7 @@ world-chain-evm = { path = "crates/evm", default-features = false } world-chain-state = { path = "crates/state", default-features = false } world-chain-devnet = { path = "crates/devnet", default-features = false } world-chain-test-utils = { path = "crates/test-utils", default-features = false } -world-chain-prover = { path = "proofs/prover", default-features = false } -world-chain-prover-sp1 = { path = "proofs/prover-sp1", default-features = false } -world-chain-prover-nitro = { path = "proofs/prover-nitro", default-features = false } +world-chain-kona = { path = "crates/kona", default-features = false } world-chain-proof-core = { path = "proofs/core", default-features = false } world-chain-proof-protocol = { path = "proofs/protocol", default-features = false } world-chain-proof-nitro = { path = "proofs/nitro", default-features = false } @@ -104,6 +103,7 @@ world-chain-proof-succinct-host-utils = { path = "proofs/succinct/utils/host", d world-chain-proof-succinct-utils = { path = "proofs/succinct/utils/proof", default-features = false } world-chain-proposer = { path = "proofs/proposer", default-features = false } world-chain-challenger = { path = "proofs/challenger", default-features = false } +world-chain-prover = { path = "proofs/prover", default-features = false } world-chain-prover-service = { path = "proofs/prover-service", default-features = false } world-chain-proof-worker = { path = "proofs/worker", default-features = false } world-chain-sp1-worker = { path = "proofs/sp1-worker", default-features = false } @@ -181,6 +181,23 @@ reth-optimism-primitives = { git = "https://github.com/ethereum-optimism/optimis reth-optimism-storage = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da", default-features = false } reth-optimism-flashblocks = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da", default-features = false } +# kona (ethereum-optimism/optimism monorepo, same rev as op-alloy/reth-optimism) +kona-engine = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-node-service = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-rpc = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-registry = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-providers-alloy = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-interop = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-disc = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-gossip = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-peers = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-sources = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da" } +kona-cli = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da", default-features = false, features = ["secrets"] } + +discv5 = "0.10.2" +libp2p = "0.56.0" +k256 = { version = "0.13", default-features = false, features = ["ecdsa"] } + # alloy op op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da", default-features = false } op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", rev = "423d93e6374a65f3a83c1ed5e29c5998c972a5da", default-features = false } diff --git a/bin/world-chain/Cargo.toml b/bin/world-chain/Cargo.toml index 9be2595f5..a50304b7d 100644 --- a/bin/world-chain/Cargo.toml +++ b/bin/world-chain/Cargo.toml @@ -26,8 +26,6 @@ reth-tracing.workspace = true # op-reth reth-optimism-consensus.workspace = true -reth-provider.workspace = true - # misc dotenvy.workspace = true clap.workspace = true diff --git a/bin/world-chain/src/main.rs b/bin/world-chain/src/main.rs index 2cdf4b02d..b2625b9d8 100644 --- a/bin/world-chain/src/main.rs +++ b/bin/world-chain/src/main.rs @@ -46,6 +46,7 @@ fn main() { node_exit_future, node: _node, } = builder.node(node).launch().await?; + node_exit_future.await?; Ok(()) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index cdfb567fe..5c4c9ef66 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -34,6 +34,20 @@ hex.workspace = true reth-network-peers.workspace = true tracing.workspace = true +# kona consensus node (in-process) — CLI args / P2P config +kona-node-service.workspace = true +kona-disc.workspace = true +kona-gossip.workspace = true +kona-peers.workspace = true +kona-sources.workspace = true +kona-cli.workspace = true +kona-genesis.workspace = true +discv5.workspace = true +libp2p.workspace = true +k256.workspace = true +url.workspace = true +reqwest.workspace = true + [dev-dependencies] alloy-genesis.workspace = true reth-node-core.workspace = true diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 42f88f202..d2af04b02 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -18,10 +18,12 @@ use world_chain_chainspec::{ }; pub mod builder; +pub mod kona; pub mod p2p; pub mod pbh; pub use builder::*; +pub use kona::*; pub use p2p::*; pub use pbh::*; @@ -151,6 +153,10 @@ pub struct WorldChainArgs { #[command(flatten)] pub flashblocks: Option, + /// Kona consensus node args + #[command(flatten)] + pub kona: Option, + /// Comma-separated list of peer IDs to which transactions should be propagated #[arg(long = "tx-peers", value_delimiter = ',', value_name = "PEER_ID")] pub tx_peers: Option>, @@ -545,6 +551,7 @@ mod tests { block_uncompressed_size_limit: None, }, flashblocks: None, + kona: None, tx_peers: Some(vec![peer_id.parse().unwrap()]), disable_bootnodes: true, simulate_enabled: false, diff --git a/crates/cli/src/cli/kona.rs b/crates/cli/src/cli/kona.rs new file mode 100644 index 000000000..e319ca3d1 --- /dev/null +++ b/crates/cli/src/cli/kona.rs @@ -0,0 +1,926 @@ +//! CLI arguments for the in-process Kona consensus node. +//! +//! When `--kona.enabled` is set, the Kona OP Stack consensus node runs in-process alongside reth, +//! eliminating the need for a separate op-node binary. Engine API calls are dispatched directly via +//! Rust function calls instead of HTTP/IPC. + +use alloy_primitives::{Address, B256}; +use alloy_signer_local::PrivateKeySigner; +use kona_disc::LocalNode; +use kona_genesis::RollupConfig; +use kona_gossip::GaterConfig; +use kona_node_service::NetworkConfig; +use kona_peers::{BootNode, BootStoreFile, PeerMonitoring, PeerScoreLevel}; +use kona_sources::{BlockSigner, ClientCert, RemoteSigner}; +use libp2p::identity::Keypair; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; +use std::{ + net::{IpAddr, SocketAddr, ToSocketAddrs}, + num::ParseIntError, + path::PathBuf, + str::FromStr, + time::Duration, +}; +use tracing::{info, warn}; +use url::Url; + +/// Resolves a hostname or IP address string to an [`IpAddr`]. +fn resolve_host(host: &str) -> Result { + if let Ok(ip) = host.parse::() { + return Ok(ip); + } + let socket_addr = format!("{host}:0"); + match socket_addr.to_socket_addrs() { + Ok(mut addrs) => addrs + .next() + .map(|addr| addr.ip()) + .ok_or_else(|| format!("DNS resolution for '{host}' returned no addresses")), + Err(e) => Err(format!("Failed to resolve '{host}': {e}")), + } +} + +/// Rewrites an `enode://`/`enr:` bootnode so its host is an IP literal. +/// +/// kona's [`BootNode::parse_bootnode`] delegates to reth's `NodeRecord::from_str`, which only +/// accepts IP-literal hosts (it `unwrap()`s, panicking the node on a DNS hostname). Kubernetes +/// service DNS names are the norm for our devnets, so we resolve the host to an IP first. `enr:` +/// records and bootnodes already using an IP literal are returned unchanged. +fn resolve_bootnode_host(bootnode: &str) -> Result { + // ENR records embed their own socket addrs; nothing to resolve. + if bootnode.starts_with("enr:") { + return Ok(bootnode.to_string()); + } + let url = url::Url::parse(bootnode).map_err(|e| format!("invalid bootnode url: {e}"))?; + match url.host() { + // Already an IP literal — pass through unchanged. + Some(url::Host::Ipv4(_)) | Some(url::Host::Ipv6(_)) => Ok(bootnode.to_string()), + Some(url::Host::Domain(domain)) => { + // Domain may already be an IP string (url treats it as a domain); resolve_host + // handles that, then DNS. + let ip = resolve_host(domain)?; + let rewritten = match ip { + IpAddr::V4(v4) => bootnode.replacen(domain, &v4.to_string(), 1), + // Bracket IPv6 hosts so the URL stays valid. + IpAddr::V6(v6) => bootnode.replacen(domain, &format!("[{v6}]"), 1), + }; + Ok(rewritten) + } + None => Err("bootnode url has no host".to_string()), + } +} + +/// P2P network configuration for the in-process Kona consensus node. +/// +/// These flags mirror kona's `P2PArgs` (see `bin/node/src/flags/p2p.rs`) and allow operators to +/// configure persistent P2P identity, bootnodes, peer scoring, gossip mesh parameters, discovery +/// settings, and sequencer signing — all via the world-chain binary's CLI. +#[derive(Debug, Clone, PartialEq, Eq, clap::Args)] +#[command(next_help_heading = "Kona P2P")] +pub struct KonaP2PArgs { + /// Disable Discv5 (node discovery). + #[arg( + long = "p2p.no-discovery", + id = "kona.no_discovery", + default_value = "false", + env = "KONA_NODE_P2P_NO_DISCOVERY" + )] + pub no_discovery: bool, + + /// Read the hex-encoded 32-byte private key for the peer ID from this txt file. + /// Created if not already exists. Important to persist to keep the same network identity + /// after restarting. + #[arg( + long = "p2p.priv.path", + id = "kona.priv_path", + env = "KONA_NODE_P2P_PRIV_PATH" + )] + pub priv_path: Option, + + /// The hex-encoded 32-byte private key for the peer ID. + #[arg( + long = "p2p.priv.raw", + id = "kona.private_key", + env = "KONA_NODE_P2P_PRIV_RAW" + )] + pub private_key: Option, + + /// IP address or DNS hostname to advertise to external peers from Discv5. + /// Uses `p2p.listen.ip` if not set. Setting this disables dynamic ENR updates. + #[arg(long = "p2p.advertise.ip", + id = "kona.advertise_ip", env = "KONA_NODE_P2P_ADVERTISE_IP", value_parser = resolve_host)] + pub advertise_ip: Option, + + /// TCP port to advertise. Same as `p2p.listen.tcp` if not set. + #[arg( + long = "p2p.advertise.tcp", + id = "kona.advertise_tcp_port", + env = "KONA_NODE_P2P_ADVERTISE_TCP_PORT" + )] + pub advertise_tcp_port: Option, + + /// UDP port to advertise. Same as `p2p.listen.udp` if not set. + #[arg( + long = "p2p.advertise.udp", + id = "kona.advertise_udp_port", + env = "KONA_NODE_P2P_ADVERTISE_UDP_PORT" + )] + pub advertise_udp_port: Option, + + /// IP address or DNS hostname to bind LibP2P/Discv5 to. + #[arg(long = "p2p.listen.ip", + id = "kona.listen_ip", default_value = "0.0.0.0", env = "KONA_NODE_P2P_LISTEN_IP", value_parser = resolve_host)] + pub listen_ip: IpAddr, + + /// TCP port to bind LibP2P to. Any available system port if set to 0. + #[arg( + long = "p2p.listen.tcp", + id = "kona.listen_tcp_port", + default_value = "9222", + env = "KONA_NODE_P2P_LISTEN_TCP_PORT" + )] + pub listen_tcp_port: u16, + + /// UDP port to bind Discv5 to. Same as TCP port if left 0. + #[arg( + long = "p2p.listen.udp", + id = "kona.listen_udp_port", + default_value = "9223", + env = "KONA_NODE_P2P_LISTEN_UDP_PORT" + )] + pub listen_udp_port: u16, + + /// Low-tide peer count. The node actively searches for new peer connections if below this. + #[arg( + long = "p2p.peers.lo", + id = "kona.peers_lo", + default_value = "20", + env = "KONA_NODE_P2P_PEERS_LO" + )] + pub peers_lo: u32, + + /// High-tide peer count. The node starts pruning peer connections after reaching this. + #[arg( + long = "p2p.peers.hi", + id = "kona.peers_hi", + default_value = "30", + env = "KONA_NODE_P2P_PEERS_HI" + )] + pub peers_hi: u32, + + /// Grace period (seconds) to keep a newly connected peer around. + #[arg( + long = "p2p.peers.grace", + id = "kona.peers_grace", + default_value = "30", + env = "KONA_NODE_P2P_PEERS_GRACE", + value_parser = |arg: &str| -> Result {Ok(Duration::from_secs(arg.parse()?))} + )] + pub peers_grace: Duration, + + /// GossipSub topic stable mesh target count (desired outbound degree). + #[arg( + long = "p2p.gossip.mesh.d", + id = "kona.gossip_mesh_d", + default_value = "8", + env = "KONA_NODE_P2P_GOSSIP_MESH_D" + )] + pub gossip_mesh_d: usize, + + /// GossipSub topic stable mesh low watermark. + #[arg( + long = "p2p.gossip.mesh.lo", + id = "kona.gossip_mesh_dlo", + default_value = "6", + env = "KONA_NODE_P2P_GOSSIP_MESH_DLO" + )] + pub gossip_mesh_dlo: usize, + + /// GossipSub topic stable mesh high watermark. + #[arg( + long = "p2p.gossip.mesh.dhi", + id = "kona.gossip_mesh_dhi", + default_value = "12", + env = "KONA_NODE_P2P_GOSSIP_MESH_DHI" + )] + pub gossip_mesh_dhi: usize, + + /// GossipSub gossip target (announcements of IHAVE). + #[arg( + long = "p2p.gossip.mesh.dlazy", + id = "kona.gossip_mesh_dlazy", + default_value = "6", + env = "KONA_NODE_P2P_GOSSIP_MESH_DLAZY" + )] + pub gossip_mesh_dlazy: usize, + + /// Publish messages to all known peers on the topic, outside of the mesh. + #[arg( + long = "p2p.gossip.mesh.floodpublish", + id = "kona.gossip_flood_publish", + default_value = "false", + env = "KONA_NODE_P2P_GOSSIP_FLOOD_PUBLISH" + )] + pub gossip_flood_publish: bool, + + /// Peer scoring strategy: none or light. + #[arg( + long = "p2p.scoring", + id = "kona.scoring", + default_value = "light", + env = "KONA_NODE_P2P_SCORING" + )] + pub scoring: PeerScoreLevel, + + /// Ban peers based on their score. + #[arg( + long = "p2p.ban.peers", + id = "kona.ban_enabled", + default_value = "false", + env = "KONA_NODE_P2P_BAN_PEERS" + )] + pub ban_enabled: bool, + + /// Score threshold below which peers are banned. + #[arg( + long = "p2p.ban.threshold", + id = "kona.ban_threshold", + default_value = "-100", + env = "KONA_NODE_P2P_BAN_THRESHOLD" + )] + pub ban_threshold: i64, + + /// Duration in minutes to ban a peer for. + #[arg( + long = "p2p.ban.duration", + id = "kona.ban_duration", + default_value = "60", + env = "KONA_NODE_P2P_BAN_DURATION" + )] + pub ban_duration: u64, + + /// Interval in seconds to find peers using the discovery service. + #[arg( + long = "p2p.discovery.interval", + id = "kona.discovery_interval", + default_value = "5", + env = "KONA_NODE_P2P_DISCOVERY_INTERVAL" + )] + pub discovery_interval: u64, + + /// Seconds to wait before removing a random peer from discovery to rotate the peer set. + #[arg( + long = "p2p.discovery.randomize", + id = "kona.discovery_randomize", + env = "KONA_NODE_P2P_DISCOVERY_RANDOMIZE" + )] + pub discovery_randomize: Option, + + /// Directory to store the bootstore. + #[arg( + long = "p2p.bootstore", + id = "kona.bootstore", + env = "KONA_NODE_P2P_BOOTSTORE" + )] + pub bootstore: Option, + + /// Disable the bootstore. + #[arg( + long = "p2p.no-bootstore", + id = "kona.disable_bootstore", + env = "KONA_NODE_P2P_NO_BOOTSTORE" + )] + pub disable_bootstore: bool, + + /// Max redial attempts for a disconnected peer. 0 = unlimited. + #[arg( + long = "p2p.redial", + id = "kona.peer_redial", + env = "KONA_NODE_P2P_REDIAL", + default_value = "500" + )] + pub peer_redial: Option, + + /// Duration in minutes of the peer dial period. + #[arg( + long = "p2p.redial.period", + id = "kona.redial_period", + env = "KONA_NODE_P2P_REDIAL_PERIOD", + default_value = "60" + )] + pub redial_period: u64, + + /// Comma-separated list of bootnode ENRs or enode URLs. + #[arg( + long = "p2p.bootnodes", + id = "kona.bootnodes", + value_delimiter = ',', + env = "KONA_NODE_P2P_BOOTNODES" + )] + pub bootnodes: Vec, + + /// Enable topic scoring (being phased out, for backwards-compat/debugging only). + #[arg( + long = "p2p.topic-scoring", + id = "kona.topic_scoring", + default_value = "false", + env = "KONA_NODE_P2P_TOPIC_SCORING" + )] + pub topic_scoring: bool, + + /// Override the unsafe block signer address. + /// By default fetched from rollup config's system config on L1. + #[arg( + long = "p2p.unsafe.block.signer", + id = "kona.unsafe_block_signer", + env = "KONA_NODE_P2P_UNSAFE_BLOCK_SIGNER" + )] + pub unsafe_block_signer: Option
, + + /// Signer configuration for gossip payloads. + #[command(flatten)] + pub signer: KonaSignerArgs, +} + +impl Default for KonaP2PArgs { + fn default() -> Self { + Self { + no_discovery: false, + priv_path: None, + private_key: None, + advertise_ip: None, + advertise_tcp_port: None, + advertise_udp_port: None, + listen_ip: IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), + listen_tcp_port: 9222, + listen_udp_port: 9223, + peers_lo: 20, + peers_hi: 30, + peers_grace: Duration::from_secs(30), + gossip_mesh_d: 8, + gossip_mesh_dlo: 6, + gossip_mesh_dhi: 12, + gossip_mesh_dlazy: 6, + gossip_flood_publish: false, + scoring: PeerScoreLevel::default(), + ban_enabled: false, + ban_threshold: -100, + ban_duration: 60, + discovery_interval: 5, + discovery_randomize: None, + bootstore: None, + disable_bootstore: false, + peer_redial: Some(500), + redial_period: 60, + bootnodes: Vec::new(), + topic_scoring: false, + unsafe_block_signer: None, + signer: KonaSignerArgs::default(), + } + } +} + +impl KonaP2PArgs { + /// Load or generate the libp2p keypair from CLI inputs. + /// + /// If a raw private key is provided, it is used directly. If a file path is provided, the key + /// is loaded from the file (or generated and written to it if it doesn't exist). If neither is + /// provided, returns an error. + fn keypair(&self) -> eyre::Result { + if let Some(mut private_key) = self.private_key { + let keypair = kona_cli::SecretKeyLoader::parse(&mut private_key.0) + .map_err(|e| eyre::Report::msg(format!("{e}")))?; + info!( + target: "world_chain::p2p", + peer_id = %keypair.public().to_peer_id(), + "Loaded P2P keypair from raw private key" + ); + return Ok(keypair); + } + + let Some(ref key_path) = self.priv_path else { + eyre::eyre::bail!( + "Neither a raw private key nor a private key file path was provided." + ); + }; + + kona_cli::SecretKeyLoader::load(key_path).map_err(|e| eyre::Report::msg(format!("{e}"))) + } + + /// Construct a [`NetworkConfig`] from these CLI arguments. + /// + /// Adapted from kona's `P2PArgs::config()`. The `rollup_config` is the parsed OP Stack rollup + /// config. `l2_chain_id` is used for bootstore default path. + pub fn build_network_config( + self, + rollup_config: &RollupConfig, + l2_chain_id: u64, + ) -> eyre::Result { + let advertise_ip = self.advertise_ip.unwrap_or(self.listen_ip); + let static_ip = self.advertise_ip.is_some(); + let advertise_tcp_port = self.advertise_tcp_port.unwrap_or(self.listen_tcp_port); + let advertise_udp_port = self.advertise_udp_port.unwrap_or(self.listen_udp_port); + + let keypair = self.keypair().unwrap_or_else(|e| { + let generated = Keypair::generate_secp256k1(); + warn!( + target: "world_chain::p2p", + error = %e, + peer_id = %generated.public().to_peer_id(), + "Failed to load P2P keypair, generated ephemeral keypair. \ + Set --p2p.priv.path or --p2p.priv.raw for a persistent peer ID." + ); + generated + }); + + let secp256k1_key = keypair + .clone() + .try_into_secp256k1() + .map_err(|e| eyre::Report::msg(format!("Failed to convert keypair to secp256k1: {e}")))? + .secret() + .to_bytes(); + let local_node_key = discv5::enr::k256::ecdsa::SigningKey::from_bytes( + &secp256k1_key.into(), + ) + .map_err(|e| eyre::Report::msg(format!("Failed to convert to k256 signing key: {e}")))?; + + let discovery_address = LocalNode::new( + local_node_key, + advertise_ip, + advertise_tcp_port, + advertise_udp_port, + ); + + let gossip_config = kona_gossip::default_config_builder() + .mesh_n(self.gossip_mesh_d) + .mesh_n_low(self.gossip_mesh_dlo) + .mesh_n_high(self.gossip_mesh_dhi) + .gossip_lazy(self.gossip_mesh_dlazy) + .flood_publish(self.gossip_flood_publish) + .build() + .map_err(|e| eyre::Report::msg(format!("Failed to build gossip config: {e}")))?; + + let monitor_peers = self.ban_enabled.then_some(PeerMonitoring { + ban_duration: Duration::from_secs(60 * self.ban_duration), + ban_threshold: self.ban_threshold as f64, + }); + + let discovery_listening_address = SocketAddr::new(self.listen_ip, self.listen_udp_port); + let discovery_config = + NetworkConfig::discv5_config(discovery_listening_address.into(), static_ip); + + let mut gossip_address = libp2p::Multiaddr::from(self.listen_ip); + gossip_address.push(libp2p::multiaddr::Protocol::Tcp(self.listen_tcp_port)); + + let unsafe_block_signer = self.unsafe_block_signer.unwrap_or(Address::ZERO); + + let bootstore = if self.disable_bootstore { + None + } else { + Some(self.bootstore.map_or_else( + || BootStoreFile::Default { + chain_id: l2_chain_id, + }, + BootStoreFile::Custom, + )) + }; + + // kona's `BootNode::parse_bootnode` -> reth `NodeRecord::from_str` only accepts + // IP-literal enode hosts and `.unwrap()`s on failure, so a DNS-hostname enode + // (e.g. Kubernetes `reth-0..svc.cluster.local:9222`) panics node startup and + // leaves the CL with zero gossip peers. Resolve any DNS host to an IP literal + // first; skip (with a warning) bootnodes that can't be parsed or resolved instead + // of taking the whole node down. + let bootnodes = self + .bootnodes + .iter() + .filter_map(|bootnode| match resolve_bootnode_host(bootnode) { + Ok(resolved) => Some(BootNode::parse_bootnode(&resolved)), + Err(e) => { + warn!( + target: "world_chain::p2p", + bootnode = %bootnode, + error = %e, + "Skipping unresolvable bootnode", + ); + None + } + }) + .collect::>() + .into(); + + let gossip_signer = self.signer.into_block_signer()?; + + Ok(NetworkConfig { + discovery_config, + discovery_interval: Duration::from_secs(self.discovery_interval), + discovery_address, + discovery_randomize: self.discovery_randomize.map(Duration::from_secs), + enr_update: !static_ip, + gossip_address, + keypair, + unsafe_block_signer, + gossip_config, + scoring: self.scoring, + monitor_peers, + bootstore, + topic_scoring: self.topic_scoring, + gater_config: GaterConfig { + peer_redialing: self.peer_redial, + dial_period: Duration::from_secs(60 * self.redial_period), + }, + bootnodes, + rollup_config: rollup_config.clone(), + gossip_signer, + }) + } +} + +/// Signer configuration for Kona's gossip payloads. +/// +/// Mirrors kona's `SignerArgs`. Supports local key signing or remote signer. +#[derive(Debug, Clone, Default, PartialEq, Eq, clap::Args)] +pub struct KonaSignerArgs { + /// Local private key for the sequencer to sign unsafe blocks. + #[arg( + long = "p2p.sequencer.key", + id = "kona.sequencer_key", + env = "KONA_NODE_P2P_SEQUENCER_KEY", + conflicts_with = "kona.endpoint" + )] + pub sequencer_key: Option, + + /// Path to a file containing the sequencer private key. + #[arg( + long = "p2p.sequencer.key.path", + id = "kona.sequencer_key_path", + env = "KONA_NODE_P2P_SEQUENCER_KEY_PATH", + conflicts_with_all = ["kona.sequencer_key", "kona.endpoint"] + )] + pub sequencer_key_path: Option, + + /// URL of the remote signer endpoint. + #[arg( + long = "p2p.signer.endpoint", + id = "kona.endpoint", + env = "KONA_NODE_P2P_SIGNER_ENDPOINT", + requires = "kona.address" + )] + pub endpoint: Option, + + /// Address to sign transactions for (required with remote signer). + #[arg( + long = "p2p.signer.address", + id = "kona.address", + env = "KONA_NODE_P2P_SIGNER_ADDRESS", + requires = "kona.endpoint" + )] + pub address: Option
, + + /// Headers for the remote signer. Format: `key=value`. + #[arg( + long = "p2p.signer.header", + id = "kona.header", + env = "KONA_NODE_P2P_SIGNER_HEADER", + requires = "kona.endpoint" + )] + pub header: Vec, + + /// Path to CA certificates for the remote signer. + #[arg( + long = "p2p.signer.tls.ca", + id = "kona.ca_cert", + env = "KONA_NODE_P2P_SIGNER_TLS_CA", + requires = "kona.endpoint" + )] + pub ca_cert: Option, + + /// Path to the client certificate for the remote signer. + #[arg( + long = "p2p.signer.tls.cert", + id = "kona.cert", + env = "KONA_NODE_P2P_SIGNER_TLS_CERT", + requires = "kona.key", + requires = "kona.endpoint" + )] + pub cert: Option, + + /// Path to the client key for the remote signer. + #[arg( + long = "p2p.signer.tls.key", + id = "kona.key", + env = "KONA_NODE_P2P_SIGNER_TLS_KEY", + requires = "kona.cert", + requires = "kona.endpoint" + )] + pub key: Option, +} + +impl KonaSignerArgs { + /// Convert into an optional [`BlockSigner`]. + fn into_block_signer(self) -> eyre::Result> { + let sequencer_key = match (self.sequencer_key, &self.sequencer_key_path) { + (Some(key), None) => Some(key), + (None, Some(path)) => { + let keypair = kona_cli::SecretKeyLoader::load(path) + .map_err(|e| eyre::Report::msg(format!("Failed to load sequencer key: {e}")))?; + let secp = keypair + .try_into_secp256k1() + .map_err(|_| eyre::Report::msg("Sequencer key is not secp256k1"))?; + Some(B256::from_slice(&secp.secret().to_bytes())) + } + (Some(_), Some(_)) => { + eyre::eyre::bail!( + "Both --p2p.sequencer.key and --p2p.sequencer.key.path cannot be specified" + ); + } + (None, None) => None, + }; + + let remote = self.into_remote_signer()?; + + match (sequencer_key, remote) { + (Some(_), Some(_)) => { + eyre::eyre::bail!("Cannot specify both local sequencer key and remote signer") + } + (Some(key), None) => { + let signer: BlockSigner = PrivateKeySigner::from_bytes(&key)?.into(); + Ok(Some(signer)) + } + (None, Some(remote)) => Ok(Some(remote.into())), + (None, None) => Ok(None), + } + } + + fn into_remote_signer(self) -> eyre::Result> { + let Some(endpoint) = self.endpoint else { + return Ok(None); + }; + let Some(address) = self.address else { + eyre::eyre::bail!("--p2p.signer.address is required with --p2p.signer.endpoint"); + }; + + let headers = self + .header + .iter() + .map(|h| { + let (key, value) = h.split_once('=').ok_or_else(|| { + eyre::Report::msg("Invalid header format, expected key=value") + })?; + Ok((HeaderName::from_str(key)?, HeaderValue::from_str(value)?)) + }) + .collect::>()?; + + let client_cert = self + .cert + .map(|cert| { + Ok::<_, eyre::Report>(ClientCert { + cert, + key: self.key.ok_or_else(|| { + eyre::Report::msg( + "--p2p.signer.tls.key required with --p2p.signer.tls.cert", + ) + })?, + }) + }) + .transpose()?; + + Ok(Some(RemoteSigner { + address, + endpoint, + ca_cert: self.ca_cert, + client_cert, + headers, + })) + } +} + +/// Arguments for the in-process Kona consensus node. +/// +/// When `--kona.enabled` is set, the Kona OP Stack consensus node runs in-process alongside reth, +/// eliminating the need for a separate op-node binary. Engine API calls are dispatched directly +/// via Rust function calls instead of HTTP/IPC. +#[derive(Debug, Clone, PartialEq, clap::Args)] +#[command(next_help_heading = "Kona Consensus Node")] +// `Option` flatten resolves to `Some` only when this presence group is +// matched. The group's sole member is `--kona.enabled`, so passing any other +// `--kona.*`/`--p2p.*` flag without `--kona.enabled` is rejected, and omitting all +// kona flags leaves the field `None`. Mirrors `FlashblocksArgs`' `flashblocks_presence`. +#[group(id = "kona_presence", requires = "kona.enabled")] +pub struct KonaArgs { + /// Enable the in-process Kona consensus node. + /// + /// When enabled, the world-chain binary acts as both the execution and consensus client. + #[arg( + long = "kona.enabled", + id = "kona.enabled", + group = "kona_presence", + required = false + )] + pub enabled: bool, + + /// L1 execution RPC URL for fetching deposits, batches, and finalization signals. + #[arg( + long = "kona.l1-rpc-url", + id = "kona.l1_rpc_url", + env = "KONA_L1_RPC_URL", + requires = "kona.enabled", + default_value = "http://localhost:8545" + )] + pub l1_rpc_url: String, + + /// L1 beacon API URL for fetching blob data (required post-Dencun). + #[arg( + long = "kona.l1-beacon-url", + id = "kona.l1_beacon_url", + env = "KONA_L1_BEACON_URL", + requires = "kona.enabled", + default_value = "http://localhost:5052" + )] + pub l1_beacon_url: String, + + /// Trust the L1 RPC without additional receipt verification. + #[arg( + long = "kona.l1-trust-rpc", + id = "kona.l1_trust_rpc", + requires = "kona.enabled", + default_value_t = false + )] + pub l1_trust_rpc: bool, + + /// P2P network configuration. + #[command(flatten)] + pub p2p: KonaP2PArgs, + + /// Path to the OP Stack rollup configuration JSON file. + /// + /// This file defines the rollup parameters (chain ID, block time, hardfork activation + /// timestamps, genesis hashes, etc.) used by the Kona consensus node. It follows the same + /// format as op-node's `--rollup.config` flag. + #[arg( + long = "kona.rollup-config", + id = "kona.rollup_config_path", + env = "KONA_ROLLUP_CONFIG", + requires = "kona.enabled" + )] + pub rollup_config_path: Option, + + /// Run the Kona consensus node in sequencer mode. + /// + /// When set, the node builds and gossips unsafe blocks rather than only following the chain. + #[arg( + long = "kona.sequencer", + id = "kona.sequencer", + env = "KONA_SEQUENCER", + requires = "kona.enabled", + default_value_t = false + )] + pub sequencer: bool, + + /// Start the sequencer in the stopped state. + /// + /// Block production must be resumed explicitly (e.g. via the admin RPC or op-conductor). + #[arg( + long = "kona.sequencer.stopped", + id = "kona.sequencer_stopped", + env = "KONA_SEQUENCER_STOPPED", + requires = "kona.enabled", + default_value_t = false + )] + pub sequencer_stopped: bool, + + /// Run the sequencer in recovery mode. + #[arg( + long = "kona.sequencer.recover", + id = "kona.sequencer_recovery_mode", + env = "KONA_SEQUENCER_RECOVER", + requires = "kona.enabled", + default_value_t = false + )] + pub sequencer_recovery_mode: bool, + + /// Number of L1 confirmations the sequencer waits on before building from an L1 origin. + #[arg( + long = "kona.sequencer.l1-confs", + id = "kona.l1_confs", + env = "KONA_SEQUENCER_L1_CONFS", + requires = "kona.enabled", + default_value_t = 4 + )] + pub l1_confs: u64, + + /// URL of the op-conductor RPC endpoint. When set, the conductor service is enabled. + #[arg( + long = "kona.conductor.rpc", + id = "kona.conductor_rpc", + env = "KONA_CONDUCTOR_RPC", + requires = "kona.enabled" + )] + pub conductor_rpc: Option, + + /// IP address the Kona node RPC server binds to. + #[arg( + long = "kona.rpc.addr", + id = "kona.rpc_addr", + env = "KONA_RPC_ADDR", + requires = "kona.enabled", + default_value = "0.0.0.0" + )] + pub rpc_addr: IpAddr, + + /// Port the Kona node RPC server binds to. + #[arg( + long = "kona.rpc.port", + id = "kona.rpc_port", + env = "KONA_RPC_PORT", + requires = "kona.enabled", + default_value_t = 8547 + )] + pub rpc_port: u16, + + /// Enable the admin namespace on the Kona node RPC server. + #[arg( + long = "kona.rpc.enable-admin", + id = "kona.rpc_enable_admin", + env = "KONA_RPC_ENABLE_ADMIN", + requires = "kona.enabled", + default_value_t = false + )] + pub rpc_enable_admin: bool, + + /// Disable the Kona node RPC server entirely. + #[arg( + long = "kona.rpc.disabled", + id = "kona.rpc_disabled", + env = "KONA_RPC_DISABLED", + requires = "kona.enabled", + default_value_t = false + )] + pub rpc_disabled: bool, + + /// Override the L1 slot duration (in seconds) used by the L1 watcher. + #[arg( + long = "kona.l1-slot-duration-override", + id = "kona.l1_slot_duration_override", + env = "KONA_L1_SLOT_DURATION_OVERRIDE", + requires = "kona.enabled" + )] + pub l1_slot_duration_override: Option, +} + +#[cfg(test)] +mod p2p_bootnode_tests { + use super::*; + use kona_genesis::RollupConfig; + + /// Reproduces the alphanet devnet config exactly: DNS-hostname enode bootnodes + + /// listen 0.0.0.0. Asserts what NetworkConfig we actually hand to kona. + #[test] + fn diagnose_alphanet_p2p_config() { + // DNS-hostname enode bootnode (the alphanet failure mode). `localhost` stands in + // for the Kubernetes service DNS name so the test is hermetic; previously this + // panicked node startup in `parse_bootnode`. + let args = KonaP2PArgs { + bootnodes: vec![ + "enode://1dcba3bc20f1853497075f69f3d15070aa87e9476a7f059c4b28eb62c1fad99c0411f1523e269ace77958859c12795d0b9658ddd4058f3f948e8bd2949bd2075@localhost:9222?discport=9222".to_string(), + ], + ..Default::default() + }; + assert_eq!(args.listen_ip.to_string(), "0.0.0.0", "default listen ip"); + let cfg = args + .build_network_config(&RollupConfig::default(), 5496749) + .expect("build_network_config"); + eprintln!("gossip_address = {}", cfg.gossip_address); + eprintln!("bootnodes len = {}", cfg.bootnodes.len()); + assert!( + cfg.gossip_address.to_string().contains("0.0.0.0"), + "gossip addr: {}", + cfg.gossip_address + ); + assert_eq!( + cfg.bootnodes.len(), + 1, + "DNS bootnode should survive parsing" + ); + } + + #[test] + fn resolve_bootnode_host_rewrites_dns_to_ip() { + // localhost is deterministically resolvable in CI without network egress. + let enode = "enode://1dcba3bc20f1853497075f69f3d15070aa87e9476a7f059c4b28eb62c1fad99c0411f1523e269ace77958859c12795d0b9658ddd4058f3f948e8bd2949bd2075@localhost:9222?discport=9222"; + let resolved = resolve_bootnode_host(enode).expect("resolve"); + assert!( + resolved.contains("@127.0.0.1:9222") || resolved.contains("@[::1]:9222"), + "resolved host should be an IP literal: {resolved}" + ); + // The resolved form must now parse through kona without panicking. + let _ = BootNode::parse_bootnode(&resolved); + } + + #[test] + fn resolve_bootnode_host_passes_through_ip_and_enr() { + let ip_enode = "enode://1dcba3bc20f1853497075f69f3d15070aa87e9476a7f059c4b28eb62c1fad99c0411f1523e269ace77958859c12795d0b9658ddd4058f3f948e8bd2949bd2075@10.2.195.220:9222?discport=9222"; + assert_eq!(resolve_bootnode_host(ip_enode).unwrap(), ip_enode); + let enr = "enr:-abc"; + assert_eq!(resolve_bootnode_host(enr).unwrap(), enr); + } +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index d840a7117..f96bb433d 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -7,6 +7,7 @@ pub mod config; pub use app::{Cli, CliApp, Commands}; pub use chainspec::WorldChainSpecParser; pub use cli::{ - BuilderArgs, FlashblocksArgs, PbhArgs, WorldChainArgs, WorldChainRpcModuleValidator, + BuilderArgs, FlashblocksArgs, KonaArgs, KonaP2PArgs, KonaSignerArgs, PbhArgs, WorldChainArgs, + WorldChainRpcModuleValidator, }; pub use config::{FlashblocksPayloadBuilderConfig, WorldChainNodeConfig}; diff --git a/crates/devnet/src/full_stack.rs b/crates/devnet/src/full_stack.rs index 6b05a65fc..1894f855c 100644 --- a/crates/devnet/src/full_stack.rs +++ b/crates/devnet/src/full_stack.rs @@ -64,9 +64,6 @@ use crate::{ }; const ANVIL_RPC_PORT: u16 = 8545; -const OP_NODE_RPC_PORT: u16 = 9545; -const OP_NODE_METRICS_PORT: u16 = 7300; -const OP_NODE_P2P_PORT: u16 = 9222; const OP_BATCHER_MAX_CHANNEL_DURATION_L1_BLOCKS: &str = "4"; const OP_PROPOSER_PERMISSIONED_GAME_TYPE: &str = "1"; const OP_TXMGR_NETWORK_TIMEOUT: &str = "30s"; @@ -109,6 +106,11 @@ const DEVNET_PRIVATE_KEY: &str = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; const UNSAFE_BLOCK_SIGNER_PRIVATE_KEY: &str = "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e"; +/// Address of [`UNSAFE_BLOCK_SIGNER_PRIVATE_KEY`], matching `unsafeBlockSigner` in the deployer +/// intent. Passed to every node so the kona P2P gossip layer expects the correct unsafe-block +/// signer immediately, rather than defaulting to the zero address before the L1 system config is +/// fetched (which makes followers reject the leader's gossiped unsafe blocks). +const UNSAFE_BLOCK_SIGNER_ADDRESS: &str = "0x976EA74026E726554dB657fA54763abd0C3a0aa9"; const BATCHER_PRIVATE_KEY: &str = "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356"; const PROPOSER_PRIVATE_KEY: &str = @@ -155,7 +157,6 @@ pub struct FullStackWorldDevnet { _sp1_worker: Option, prover_service_url: Option, _conductors: Vec, - _op_nodes: Vec, sequencers: Vec, observability: Option, l1: L1DevChain, @@ -171,7 +172,16 @@ struct SequencerService { ws_url: String, auth_url: String, flashblocks_url: String, + /// Native (host-reachable) URL of the in-process kona consensus node RPC. + /// + /// Serves `optimism_syncStatus`, `optimism_rollupConfig`, and the + /// `admin_*Sequencer` admin namespace. Reachable from the host at + /// `127.0.0.1:` and from containers via + /// `host.docker.internal:`. + kona_rpc_url: String, + kona_rpc_host_port: u16, p2p_host_port: u16, + kona_p2p_host_port: u16, metrics_target: MetricsTarget, binary: PathBuf, _process: NativeProcess, @@ -186,26 +196,15 @@ struct SequencerPlan { p2p_host_port: u16, p2p_secret_key: String, trusted_peer: String, -} - -#[derive(Clone, Debug)] -struct OpNodePlan { - rpc_host_port: u16, - metrics_host_port: u16, - p2p_host_port: u16, - bootnode: String, - private_key_path: String, -} - -#[derive(Debug)] -struct OpNodeService { - id: String, - rpc_url: String, - p2p_host_port: u16, - bootnodes: Vec, - metrics_target: MetricsTarget, - image: ContainerImage, - _container: ContainerAsync, + /// Host port for the in-process kona consensus node RPC (native bind on + /// `0.0.0.0`, directly reachable at `127.0.0.1:`). + kona_rpc_host_port: u16, + /// Host port for the in-process kona consensus P2P listener. + kona_p2p_host_port: u16, + /// secp256k1 secret key (hex, no `0x`) for the kona consensus P2P identity. + kona_p2p_secret_key: String, + /// Consensus enode advertised to the other sequencers' kona P2P bootstore. + kona_consensus_enode: String, } #[derive(Clone, Debug)] @@ -397,6 +396,18 @@ impl FullStackWorldDevnet { .map(plan_sequencer) .collect::>>() .wrap_err("failed to plan world-chain EL peer mesh")?; + + // Plan conductors before starting the sequencers: each monomorphic + // sequencer runs kona in-process and needs its conductor's RPC host port + // up front (kona's outbound `--kona.conductor.rpc` client). The conductor + // containers themselves are still started afterwards. + let mut conductor_plans = Vec::with_capacity(sequencer_count); + for index in 0..sequencer_count { + conductor_plans.push(plan_conductor(index, port_mode)?); + } + + let rollup_config_path = workdir_path.join("rollup.json"); + let l1_slot_duration_secs = block_time.as_secs().max(1); let trusted_peers = sequencer_plans .iter() .map(|plan| plan.trusted_peer.clone()) @@ -407,43 +418,25 @@ impl FullStackWorldDevnet { .enumerate() .filter_map(|(peer_index, peer)| (peer_index != index).then_some(peer.clone())) .collect::>(); + let kona_consensus_bootnodes = kona_consensus_bootnodes(&sequencer_plans, index); start_world_chain_el( index, &workdir_path, &sequencer_plans[index], trusted_peers, + WorldChainElKona { + rollup_config_path: &rollup_config_path, + l1_rpc_url: &l1_public_rpc, + l1_slot_duration_secs, + conductor_rpc_host_port: conductor_plans[index].rpc_host_port, + consensus_bootnodes: kona_consensus_bootnodes, + }, access_list, ) })) .await?; connect_execution_peers(&sequencers).await?; - - let mut conductor_plans = Vec::with_capacity(sequencer_count); - for index in 0..sequencer_count { - conductor_plans.push(plan_conductor(index, port_mode)?); - } - - let op_node_plans = plan_op_nodes(sequencer_count, &workdir_path) - .wrap_err("failed to plan op-node bootnode peer mesh")?; - let op_node_bootnodes: Vec<_> = (0..sequencer_count) - .map(|index| op_node_bootnodes(&op_node_plans, index)) - .collect(); - let l1_slot_duration_secs = block_time.as_secs().max(1); - let op_nodes = try_join_all((0..sequencer_count).map(|index| { - start_op_node( - index, - &workdir_path, - &config.images.op_node, - &op_node_plans[index], - &op_node_bootnodes[index], - l1_slot_duration_secs, - &l1_internal_rpc, - &sequencers[index], - &conductor_plans[index].rpc_url, - ) - })) - .await?; - wait_for_op_node_peer_mesh(&op_nodes).await?; + wait_for_kona_consensus_mesh(&sequencers).await?; let mut conductors = Vec::with_capacity(sequencer_count); conductors.push( @@ -459,12 +452,12 @@ impl FullStackWorldDevnet { ); wait_for_conductor_leader(&conductors[0], Duration::from_secs(90)).await?; - start_bootstrap_sequencer(&op_nodes[0]).await?; + start_bootstrap_sequencer(&sequencers[0]).await?; wait_for_l2_blocks_with_logs( &sequencers[0].rpc_url, 1, Duration::from_secs(120), - &op_nodes, + &sequencers, &conductors, ) .await?; @@ -489,7 +482,7 @@ impl FullStackWorldDevnet { &sequencers[0].rpc_url, 2, Duration::from_secs(120), - &op_nodes, + &sequencers, &conductors, ) .await?; @@ -539,11 +532,13 @@ impl FullStackWorldDevnet { }; let world_proposer = if let Some(deployment) = proof_system.as_ref() { - let output_root_rpc = op_nodes + let output_root_rpc = sequencers .first() - .map(|node| node.rpc_url.clone()) + .map(|sequencer| sequencer.kona_rpc_url.clone()) .ok_or_else(|| { - eyre!("full-stack devnet has no op-node for the World Chain proposer") + eyre!( + "full-stack devnet has no kona consensus node for the World Chain proposer" + ) })?; Some(start_world_chain_proposer(&l1_public_rpc, &output_root_rpc, deployment).await?) } else { @@ -551,11 +546,11 @@ impl FullStackWorldDevnet { }; let world_challenger = if let Some(deployment) = proof_system.as_ref() { - let output_root_rpc = op_nodes + let output_root_rpc = sequencers .first() - .map(|node| node.rpc_url.clone()) + .map(|sequencer| sequencer.kona_rpc_url.clone()) .ok_or_else(|| { - eyre!("full-stack devnet has no op-node for the World Chain challenger") + eyre!("full-stack devnet has no kona consensus node for the World Chain challenger") })?; Some(start_world_chain_challenger(&l1_public_rpc, &output_root_rpc, deployment).await?) } else { @@ -566,42 +561,40 @@ impl FullStackWorldDevnet { // enabled by the `DEVNET_SP1_WORKER_PROVER` env var. The defender enqueues proof // requests for challenged valid games; the worker leases SP1 jobs, builds witnesses // from the devnet L1/L2 RPCs, and proves them with the selected backend. - let (prover_service, sp1_worker, world_defender, prover_service_url) = - match (proof_system.as_ref(), sp1_worker_prover_kind()) { - (Some(deployment), Some(kind)) => { - let output_root_rpc = op_nodes - .first() - .map(|node| node.rpc_url.clone()) - .ok_or_else(|| { - eyre!("full-stack devnet has no op-node for the World Chain defender") - })?; - let l2_rpc = sequencers + let (prover_service, sp1_worker, world_defender, prover_service_url) = match ( + proof_system.as_ref(), + sp1_worker_prover_kind(), + ) { + (Some(deployment), Some(kind)) => { + let output_root_rpc = sequencers .first() - .map(|sequencer| sequencer.rpc_url.clone()) + .map(|sequencer| sequencer.kona_rpc_url.clone()) .ok_or_else(|| { - eyre!("full-stack devnet has no sequencer for the SP1 worker") + eyre!("full-stack devnet has no kona consensus node for the World Chain defender") })?; - let (service, url) = start_prover_service().await?; - let defender = start_world_chain_defender( - &l1_public_rpc, - &output_root_rpc, - &url, - deployment, - ) - .await?; - let worker = start_sp1_worker( - &l1_public_rpc, - &l2_rpc, - &url, - &artifacts.rollup_path, - deployment, - kind, - ) - .await?; - (Some(service), Some(worker), Some(defender), Some(url)) - } - _ => (None, None, None, None), - }; + let l2_rpc = sequencers + .first() + .map(|sequencer| sequencer.rpc_url.clone()) + .ok_or_else(|| { + eyre!("full-stack devnet has no sequencer for the SP1 worker") + })?; + let (service, url) = start_prover_service().await?; + let defender = + start_world_chain_defender(&l1_public_rpc, &output_root_rpc, &url, deployment) + .await?; + let worker = start_sp1_worker( + &l1_public_rpc, + &l2_rpc, + &url, + &artifacts.rollup_path, + deployment, + kind, + ) + .await?; + (Some(service), Some(worker), Some(defender), Some(url)) + } + _ => (None, None, None, None), + }; let mut metrics_targets = Vec::new(); metrics_targets.extend( @@ -609,11 +602,6 @@ impl FullStackWorldDevnet { .iter() .map(|service| service.metrics_target.clone()), ); - metrics_targets.extend( - op_nodes - .iter() - .map(|service| service.metrics_target.clone()), - ); metrics_targets.extend( conductors .iter() @@ -634,7 +622,6 @@ impl FullStackWorldDevnet { &config, &l1_public_rpc, &sequencers, - &op_nodes, &conductors, batcher.as_ref(), proposer.as_ref(), @@ -657,7 +644,6 @@ impl FullStackWorldDevnet { _sp1_worker: sp1_worker, prover_service_url, _conductors: conductors, - _op_nodes: op_nodes, sequencers, observability, l1, @@ -690,11 +676,12 @@ impl FullStackWorldDevnet { } pub async fn safe_block_number(&self) -> Result { - let op_node = self - ._op_nodes + let sequencer = self + .sequencers .first() - .ok_or_else(|| eyre!("full-stack devnet has no op-node"))?; - let sync_status = json_rpc(&op_node.rpc_url, "optimism_syncStatus", json!([])).await?; + .ok_or_else(|| eyre!("full-stack devnet has no sequencer"))?; + let sync_status = + json_rpc(&sequencer.kona_rpc_url, "optimism_syncStatus", json!([])).await?; let safe_number = sync_status .pointer("/safe_l2/number") .ok_or_else(|| eyre!("optimism_syncStatus missing safe_l2.number: {sync_status}"))?; @@ -1341,6 +1328,14 @@ fn plan_sequencer(_index: usize) -> Result { let p2p_secret_key = random_p2p_secret_key(); let p2p_host_port = reserve_host_port()?; let trusted_peer = devnet_enode(&p2p_secret_key, p2p_host_port)?; + + let kona_p2p_secret_key = random_p2p_secret_key(); + let kona_p2p_host_port = reserve_host_port()?; + // The in-process kona consensus node advertises 127.0.0.1: it is native, so + // its peers (the other native sequencers) reach it directly on the host. + let kona_consensus_enode = + devnet_trusted_peer(&kona_p2p_secret_key, "127.0.0.1", kona_p2p_host_port)?; + Ok(SequencerPlan { rpc_host_port: reserve_host_port()?, ws_host_port: reserve_host_port()?, @@ -1349,14 +1344,51 @@ fn plan_sequencer(_index: usize) -> Result { p2p_host_port, p2p_secret_key, trusted_peer, + kona_rpc_host_port: reserve_host_port()?, + kona_p2p_host_port, + kona_p2p_secret_key, + kona_consensus_enode, }) } +/// Returns the consensus enodes of every sequencer except `source_index`, used +/// to seed the in-process kona P2P bootstore so the sequencer mesh forms. +fn kona_consensus_bootnodes(plans: &[SequencerPlan], source_index: usize) -> Vec { + plans + .iter() + .enumerate() + .filter(|&(target_index, _)| target_index != source_index) + .map(|(_, plan)| plan.kona_consensus_enode.clone()) + .collect() +} + +/// Inputs the monomorphic world-chain client needs to run kona in-process as +/// the consensus/sequencer. +/// +/// All URLs and paths here are addressed from the NATIVE world-chain process: +/// host-reachable URLs (the public Anvil L1, the host-mapped conductor RPC) and +/// native filesystem paths (the workdir rollup config), never the `/work` mount +/// or `host.docker.internal` used by the OP Stack containers. +struct WorldChainElKona<'a> { + /// Native path to the patched rollup config (`/rollup.json`). + rollup_config_path: &'a Path, + /// Host-reachable public Anvil L1 RPC (also used as the L1 beacon endpoint). + l1_rpc_url: &'a str, + /// L1 slot duration override, mirroring the dev L1 block time. + l1_slot_duration_secs: u64, + /// Host port of this sequencer's op-conductor RPC (kona's outbound client + /// reaches it at `http://127.0.0.1:` since the binary is native). + conductor_rpc_host_port: u16, + /// Consensus enodes of the other sequencers for the kona P2P bootstore. + consensus_bootnodes: Vec, +} + async fn start_world_chain_el( index: usize, workdir: &Path, plan: &SequencerPlan, trusted_peers: Vec, + kona: WorldChainElKona<'_>, access_list: bool, ) -> Result { let data_dir = workdir.join(format!("l2data-{index}")); @@ -1396,6 +1428,10 @@ async fn start_world_chain_el( let p2p_port_arg = p2p_port.to_string(); let metrics_arg = format!("0.0.0.0:{metrics_port}"); let p2p_secret_key = plan.p2p_secret_key.clone(); + // Each node needs a distinct IPC socket: reth's default `--ipcpath` is a global path + // (`/tmp/reth.ipc`), so co-located nodes would collide. The in-process kona client connects to + // reth's standard (non-engine) RPC over this IPC endpoint. + let ipc_path_arg = data_dir.join("reth.ipc").to_string_lossy().to_string(); let mut args = vec![ "node".to_string(), "--chain".to_string(), @@ -1407,7 +1443,8 @@ async fn start_world_chain_el( "--p2p-secret-key-hex".to_string(), p2p_secret_key, "--no-persist-peers".to_string(), - "--ipcdisable".to_string(), + "--ipcpath".to_string(), + ipc_path_arg, "--http".to_string(), "--http.addr".to_string(), "0.0.0.0".to_string(), @@ -1470,6 +1507,82 @@ async fn start_world_chain_el( args.push("--flashblocks.access-list".to_string()); } + // Monomorphic client: run kona in-process as the consensus/sequencer. When + // `--kona.enabled` is set the binary auto-wires kona to reth's own launched + // auth Engine API + JWT, so no engine URL/jwt is passed here. + // + // All addressing is NATIVE: the rollup config is the workdir path, the L1 + // and conductor URLs are host-reachable. The kona node RPC binds on + // `0.0.0.0:` and is directly reachable at + // `127.0.0.1:` (and from containers at + // `host.docker.internal:`). + let kona_rpc_port = plan.kona_rpc_host_port; + let kona_p2p_port = plan.kona_p2p_host_port; + let kona_rpc_port_arg = kona_rpc_port.to_string(); + let kona_p2p_port_arg = kona_p2p_port.to_string(); + let conductor_rpc_url = format!("http://127.0.0.1:{}", kona.conductor_rpc_host_port); + let rollup_config_arg = kona.rollup_config_path.to_string_lossy().to_string(); + let kona_bootstore_arg = workdir + .join(format!("kona-bootstore-{index}")) + .to_string_lossy() + .to_string(); + let kona_p2p_priv_arg = format!("0x{}", plan.kona_p2p_secret_key); + args.extend([ + "--kona.enabled".to_string(), + "--kona.sequencer".to_string(), + // Start stopped: op-conductor starts the active leader's sequencer via + // `admin_startSequencer` (see `start_bootstrap_sequencer`), mirroring the + // old op-node `--sequencer.stopped` + bootstrap flow. + "--kona.sequencer.stopped".to_string(), + "--kona.sequencer.l1-confs".to_string(), + "0".to_string(), + "--kona.rollup-config".to_string(), + rollup_config_arg, + "--kona.l1-rpc-url".to_string(), + kona.l1_rpc_url.to_string(), + // Dev L1 (Anvil) has no separate beacon node; the EL RPC doubles as the + // beacon endpoint, mirroring the old op-node `--l1-beacon = l1_rpc`. + "--kona.l1-beacon-url".to_string(), + kona.l1_rpc_url.to_string(), + "--kona.l1-trust-rpc".to_string(), + "--kona.l1-slot-duration-override".to_string(), + kona.l1_slot_duration_secs.to_string(), + "--kona.rpc.addr".to_string(), + "0.0.0.0".to_string(), + "--kona.rpc.port".to_string(), + kona_rpc_port_arg, + "--kona.rpc.enable-admin".to_string(), + "--kona.conductor.rpc".to_string(), + conductor_rpc_url, + "--p2p.sequencer.key".to_string(), + UNSAFE_BLOCK_SIGNER_PRIVATE_KEY.to_string(), + "--p2p.unsafe.block.signer".to_string(), + UNSAFE_BLOCK_SIGNER_ADDRESS.to_string(), + "--p2p.listen.ip".to_string(), + "0.0.0.0".to_string(), + "--p2p.listen.tcp".to_string(), + kona_p2p_port_arg.clone(), + "--p2p.listen.udp".to_string(), + kona_p2p_port_arg.clone(), + "--p2p.advertise.ip".to_string(), + "127.0.0.1".to_string(), + "--p2p.advertise.tcp".to_string(), + kona_p2p_port_arg.clone(), + "--p2p.advertise.udp".to_string(), + kona_p2p_port_arg, + "--p2p.priv.raw".to_string(), + kona_p2p_priv_arg, + "--p2p.no-discovery".to_string(), + "--p2p.bootstore".to_string(), + kona_bootstore_arg, + ]); + if !kona.consensus_bootnodes.is_empty() { + args.extend([ + "--p2p.bootnodes".to_string(), + kona.consensus_bootnodes.join(","), + ]); + } + let mut process = spawn_native_process(&format!("world-chain-el-{index}"), &binary, &args) .wrap_err_with(|| format!("failed to spawn native world-chain EL process {index}"))?; @@ -1484,14 +1597,33 @@ async fn start_world_chain_el( format!("world-chain EL {index} RPC did not become ready; process_status={status:?}") })?; + // The in-process kona node RPC is native, so it is reachable at 127.0.0.1. + let kona_rpc_url = format!("http://127.0.0.1:{kona_rpc_port}"); + wait_for_json_rpc( + &kona_rpc_url, + "optimism_rollupConfig", + json!([]), + Duration::from_secs(120), + ) + .await + .wrap_err_with(|| { + let status = process.child.try_wait().ok().flatten(); + format!( + "in-process kona consensus RPC for sequencer {index} did not become ready; \ + process_status={status:?}" + ) + })?; + info!( index, rpc_url = %rpc_url, auth_url = %auth_url, + kona_rpc_url = %kona_rpc_url, p2p = %format!("127.0.0.1:{p2p_port}"), + kona_p2p = %format!("127.0.0.1:{kona_p2p_port}"), metrics = %format!("127.0.0.1:{metrics_port}"), binary = %binary.display(), - "native world-chain EL started" + "native monomorphic world-chain client started (reth EL + in-process kona sequencer)" ); Ok(SequencerService { @@ -1500,7 +1632,10 @@ async fn start_world_chain_el( ws_url, auth_url, flashblocks_url: format!("ws://127.0.0.1:{ws_port}"), + kona_rpc_url, + kona_rpc_host_port: kona_rpc_port, p2p_host_port: p2p_port, + kona_p2p_host_port: kona_p2p_port, metrics_target: MetricsTarget::new( format!("world-chain-el-{index}"), format!("host.docker.internal:{metrics_port}"), @@ -1700,34 +1835,6 @@ fn plan_conductor(index: usize, port_mode: DevnetPortMode) -> Result Result> { - let mut plans = Vec::with_capacity(count); - for index in 0..count { - let private_key = random_p2p_secret_key(); - let p2p_host_port = reserve_host_port()?; - let filename = format!("op-node-{index}-p2p-priv.txt"); - fs::write(workdir.join(&filename), &private_key) - .wrap_err_with(|| format!("failed to write op-node P2P key {filename}"))?; - plans.push(OpNodePlan { - rpc_host_port: 19_545 + index as u16, - metrics_host_port: reserve_host_port()?, - p2p_host_port, - bootnode: devnet_trusted_peer(&private_key, "host.docker.internal", p2p_host_port)?, - private_key_path: format!("/work/{filename}"), - }); - } - Ok(plans) -} - -fn op_node_bootnodes(plans: &[OpNodePlan], source_index: usize) -> Vec { - plans - .iter() - .enumerate() - .filter(|&(target_index, _target)| target_index != source_index) - .map(|(_target_index, target)| target.bootnode.clone()) - .collect() -} - fn random_p2p_secret_key() -> String { loop { let bytes = rand::rng().random::<[u8; 32]>(); @@ -1766,7 +1873,12 @@ async fn start_conductor( sequencer: &SequencerService, plan: &ConductorPlan, ) -> Result { - let node_rpc = format!("http://host.docker.internal:{}", 19_545 + index as u16); + // op-conductor runs in a container and reaches the native in-process kona + // node RPC via host.docker.internal on its directly-bound host port. + let node_rpc = format!( + "http://host.docker.internal:{}", + sequencer.kona_rpc_host_port + ); let execution_rpc = host_internal_url(&sequencer.rpc_url)?; let min_peer_count = sequencer_count.saturating_sub(1).max(1).to_string(); let mut cmd = vec![ @@ -1861,189 +1973,50 @@ async fn start_conductor( Ok(service) } -async fn start_op_node( - index: usize, - workdir: &Path, - image: &ContainerImage, - plan: &OpNodePlan, - bootnodes: &[String], - l1_slot_duration_secs: u64, - l1_rpc: &str, - sequencer: &SequencerService, - conductor_rpc_url: &str, -) -> Result { - let conductor_rpc = host_internal_url(conductor_rpc_url)?; - let l2_engine_rpc = host_internal_url(&sequencer.auth_url)?; - let p2p_host_port = plan.p2p_host_port.to_string(); - let l1_slot_duration_secs = l1_slot_duration_secs.to_string(); - let mut cmd = vec![ - "-vvv".to_string(), - "--logs.stdout.format=logfmt".to_string(), - "node".to_string(), - "--chain".to_string(), - DEV_CHAIN_ID.to_string(), - "--metrics.enabled".to_string(), - "--metrics.addr".to_string(), - "0.0.0.0".to_string(), - "--metrics.port".to_string(), - OP_NODE_METRICS_PORT.to_string(), - "--mode".to_string(), - "Sequencer".to_string(), - "--sequencer.stopped".to_string(), - "--sequencer.max-safe-lag".to_string(), - "0".to_string(), - "--sequencer.l1-confs".to_string(), - "0".to_string(), - "--conductor.rpc".to_string(), - conductor_rpc, - "--conductor.rpc.timeout".to_string(), - "5".to_string(), - "--l2-config-file".to_string(), - "/work/rollup.json".to_string(), - "--l1-config-file".to_string(), - "/work/l1-genesis.json".to_string(), - "--l1-eth-rpc".to_string(), - l1_rpc.to_string(), - "--l1-beacon".to_string(), - l1_rpc.to_string(), - "--l1-slot-duration-override".to_string(), - l1_slot_duration_secs, - "--l1-trust-rpc".to_string(), - "--l2-engine-rpc".to_string(), - l2_engine_rpc, - "--l2-engine-jwt-secret".to_string(), - "/work/jwt.hex".to_string(), - "--l2-trust-rpc".to_string(), - "--p2p.sequencer.key".to_string(), - UNSAFE_BLOCK_SIGNER_PRIVATE_KEY.to_string(), - "--p2p.listen.ip".to_string(), - "0.0.0.0".to_string(), - "--p2p.listen.tcp".to_string(), - OP_NODE_P2P_PORT.to_string(), - "--p2p.listen.udp".to_string(), - OP_NODE_P2P_PORT.to_string(), - "--p2p.advertise.ip".to_string(), - "host.docker.internal".to_string(), - "--p2p.advertise.tcp".to_string(), - p2p_host_port.clone(), - "--p2p.advertise.udp".to_string(), - p2p_host_port, - "--p2p.priv.path".to_string(), - plan.private_key_path.clone(), - "--p2p.bootstore".to_string(), - format!("/work/kona-bootstore-{index}"), - "--p2p.no-discovery".to_string(), - "--p2p.redial".to_string(), - "0".to_string(), - "--rpc.addr".to_string(), - "0.0.0.0".to_string(), - "--rpc.enable-admin".to_string(), - "--port".to_string(), - OP_NODE_RPC_PORT.to_string(), - ]; - if !bootnodes.is_empty() { - cmd.push("--p2p.bootnodes".to_string()); - cmd.push(bootnodes.join(",")); - } - - let mut request = GenericImage::new(image.repository.clone(), image.tag.clone()) - .with_entrypoint("kona-node") - .with_wait_for(WaitFor::seconds(5)) - .with_exposed_port(OP_NODE_RPC_PORT.tcp()) - .with_exposed_port(OP_NODE_METRICS_PORT.tcp()) - .with_exposed_port(OP_NODE_P2P_PORT.tcp()) - .with_exposed_port(OP_NODE_P2P_PORT.udp()) - .with_log_consumer(container_log_consumer( - format!("op-node-{index}"), - ProcessLogTarget::OpNode, - )) - .with_cmd(cmd) - .with_startup_timeout(Duration::from_secs(120)) - .with_mount(Mount::bind_mount( - workdir.to_string_lossy().to_string(), - "/work", - )); - request = request.with_mapped_port(plan.rpc_host_port, OP_NODE_RPC_PORT.tcp()); - request = request.with_mapped_port(plan.metrics_host_port, OP_NODE_METRICS_PORT.tcp()); - request = request.with_mapped_port(plan.p2p_host_port, OP_NODE_P2P_PORT.tcp()); - request = request.with_mapped_port(plan.p2p_host_port, OP_NODE_P2P_PORT.udp()); - - let container = request - .start() - .await - .wrap_err_with(|| format!("failed to start op-node {index}"))?; - - let host = container.get_host().await?; - let rpc_url = format!("http://{host}:{}", plan.rpc_host_port); - if let Err(err) = wait_for_json_rpc( - &rpc_url, - "optimism_rollupConfig", - json!([]), - Duration::from_secs(60), - ) - .await - { - let logs = container_logs(&container).await; - return Err(err).wrap_err_with(|| { - format!("op-node {index} RPC did not become ready; container logs:\n{logs}") - }); - } - - Ok(OpNodeService { - id: format!("op-node-{index}"), - rpc_url, - p2p_host_port: plan.p2p_host_port, - bootnodes: bootnodes.to_vec(), - metrics_target: MetricsTarget::new( - format!("op-node-{index}"), - format!("host.docker.internal:{}", plan.metrics_host_port), - ), - image: image.clone(), - _container: container, - }) -} - -async fn wait_for_op_node_peer_mesh(op_nodes: &[OpNodeService]) -> Result<()> { - if op_nodes.len() <= 1 { +async fn wait_for_kona_consensus_mesh(sequencers: &[SequencerService]) -> Result<()> { + if sequencers.len() <= 1 { return Ok(()); } - let expected = op_nodes.len().saturating_sub(1) as u64; + let expected = sequencers.len().saturating_sub(1) as u64; info!( - nodes = op_nodes.len(), + nodes = sequencers.len(), expected_peers_per_node = expected, - "waiting for op-node bootnode peer mesh" + "waiting for in-process kona consensus P2P mesh" ); - for node in op_nodes { + for sequencer in sequencers { retry_until( - Duration::from_secs(30), + Duration::from_secs(60), Duration::from_millis(500), || async { - let peers = json_rpc(&node.rpc_url, "opp2p_peers", json!([true])).await?; + let peers = json_rpc(&sequencer.kona_rpc_url, "opp2p_peers", json!([true])).await?; let connected = peers .get("totalConnected") .and_then(Value::as_u64) .ok_or_else(|| { eyre!( "opp2p_peers for {} missing totalConnected: {peers}", - node.id + sequencer.id ) })?; - if connected == expected { + if connected >= expected { Ok(()) } else { bail!( - "{} has {connected} connected op-node peers, expected {expected}", - node.id + "{} has {connected} connected kona consensus peers, expected {expected}", + sequencer.id ) } }, ) .await - .wrap_err_with(|| format!("op-node bootnode P2P mesh did not form for {}", node.id))?; + .wrap_err_with(|| format!("kona consensus P2P mesh did not form for {}", sequencer.id))?; } - info!(count = op_nodes.len(), "op-node P2P mesh connected"); + info!( + count = sequencers.len(), + "in-process kona consensus P2P mesh connected" + ); Ok(()) } @@ -2159,12 +2132,13 @@ async fn wait_for_conductor_leader(bootstrap: &ConductorService, timeout: Durati Ok(()) } -async fn start_bootstrap_sequencer(op_node: &OpNodeService) -> Result<()> { +async fn start_bootstrap_sequencer(sequencer: &SequencerService) -> Result<()> { + let kona_rpc_url = &sequencer.kona_rpc_url; retry_until( Duration::from_secs(30), Duration::from_millis(500), || async { - let active = json_rpc(&op_node.rpc_url, "admin_sequencerActive", json!([])) + let active = json_rpc(kona_rpc_url, "admin_sequencerActive", json!([])) .await? .as_bool() .ok_or_else(|| eyre!("admin_sequencerActive did not return a bool"))?; @@ -2172,32 +2146,29 @@ async fn start_bootstrap_sequencer(op_node: &OpNodeService) -> Result<()> { return Ok(()); } - let sync_status = json_rpc(&op_node.rpc_url, "optimism_syncStatus", json!([])).await?; + let sync_status = json_rpc(kona_rpc_url, "optimism_syncStatus", json!([])).await?; let unsafe_hash = sync_status .pointer("/unsafe_l2/hash") .and_then(Value::as_str) .ok_or_else(|| eyre!("optimism_syncStatus missing unsafe_l2.hash: {sync_status}"))? .to_string(); - json_rpc( - &op_node.rpc_url, - "admin_startSequencer", - json!([unsafe_hash]), - ) - .await - .map(|_| ()) + json_rpc(kona_rpc_url, "admin_startSequencer", json!([unsafe_hash])) + .await + .map(|_| ()) }, ) .await .wrap_err_with(|| { format!( - "failed to explicitly start bootstrap sequencer {}", - op_node.id + "failed to explicitly start in-process kona bootstrap sequencer {}", + sequencer.id ) })?; info!( - op_node = %op_node.id, - "bootstrap op-node sequencer started" + sequencer = %sequencer.id, + kona_rpc_url = %sequencer.kona_rpc_url, + "bootstrap in-process kona sequencer started" ); Ok(()) } @@ -2872,7 +2843,6 @@ fn build_components( config: &HaSequencerConfig, l1_rpc_url: &str, sequencers: &[SequencerService], - op_nodes: &[OpNodeService], conductors: &[ConductorService], batcher: Option<&ContainerService>, proposer: Option<&ContainerService>, @@ -2909,9 +2879,14 @@ fn build_components( .with_endpoint("rpc", service.rpc_url.clone()) .with_endpoint("ws", service.ws_url.clone()) .with_endpoint("engine", service.auth_url.clone()) + .with_endpoint("kona-rpc", service.kona_rpc_url.clone()) .with_endpoint("p2p", format!("127.0.0.1:{}", service.p2p_host_port)) + .with_endpoint( + "kona-p2p", + format!("127.0.0.1:{}", service.kona_p2p_host_port), + ) .with_note( - "native direct-sequencing World Chain execution node with flashblocks enabled and trusted EL peers", + "monomorphic World Chain client: native reth EL + in-process kona consensus/sequencer, flashblocks enabled and trusted EL peers", ) .with_note(format!( "PBH disabled with zero reserved blockspace and sentinel entrypoint {PBH_DISABLED_ENTRYPOINT}" @@ -2932,23 +2907,6 @@ fn build_components( ); } - for service in op_nodes { - components.push( - DevnetComponent::new( - service.id.clone(), - DevnetComponentKind::OpNode, - DevnetComponentStatus::Running, - ) - .with_image(service.image.clone()) - .with_endpoint("rpc", service.rpc_url.clone()) - .with_endpoint( - "p2p", - format!("host.docker.internal:{}", service.p2p_host_port), - ) - .with_note(format!("bootnodes={}", service.bootnodes.join(","))), - ); - } - for service in conductors { components.push( DevnetComponent::new( @@ -3157,25 +3115,25 @@ async fn wait_for_l2_blocks_with_logs( rpc_url: &str, min_block: u64, timeout: Duration, - op_nodes: &[OpNodeService], + sequencers: &[SequencerService], conductors: &[ConductorService], ) -> Result<()> { if let Err(err) = wait_for_l2_blocks(rpc_url, min_block, timeout).await { let mut diagnostics = String::new(); - diagnostics.push_str("op-node status:\n"); - for node in op_nodes { - let sync_status = json_rpc(&node.rpc_url, "optimism_syncStatus", json!([])) - .await - .map(|value| value.to_string()) - .unwrap_or_else(|err| format!("error: {err}")); - let sequencer_active = json_rpc(&node.rpc_url, "admin_sequencerActive", json!([])) + diagnostics.push_str("in-process kona consensus status:\n"); + for sequencer in sequencers { + let sync_status = json_rpc(&sequencer.kona_rpc_url, "optimism_syncStatus", json!([])) .await .map(|value| value.to_string()) .unwrap_or_else(|err| format!("error: {err}")); - let logs = tail_text(&container_logs(&node._container).await, 160); + let sequencer_active = + json_rpc(&sequencer.kona_rpc_url, "admin_sequencerActive", json!([])) + .await + .map(|value| value.to_string()) + .unwrap_or_else(|err| format!("error: {err}")); diagnostics.push_str(&format!( - "\n{} rpc={} sequencer_active={} sync_status={}\nlogs:\n{}\n", - node.id, node.rpc_url, sequencer_active, sync_status, logs + "\n{} kona_rpc={} sequencer_active={} sync_status={}\n", + sequencer.id, sequencer.kona_rpc_url, sequencer_active, sync_status )); } diff --git a/crates/devnet/src/process_logs.rs b/crates/devnet/src/process_logs.rs index facbbaa41..9f65c9271 100644 --- a/crates/devnet/src/process_logs.rs +++ b/crates/devnet/src/process_logs.rs @@ -16,9 +16,11 @@ macro_rules! emit_at_level { #[derive(Clone, Copy, Debug)] pub(crate) enum ProcessLogTarget { L1DevChain, + /// The native monomorphic world-chain client: reth execution layer plus the + /// in-process kona consensus/sequencer. Its log stream carries both reth and + /// kona (op-node-style) output. WorldChainEl, OpDeployer, - OpNode, OpConductor, OpBatcher, OpProposer, @@ -58,7 +60,6 @@ pub(crate) fn emit_process_log(target: ProcessLogTarget, process: &str, line: &s ProcessLogTarget::OpDeployer => { emit_at_level!("op_deployer", level, process, line.as_str()) } - ProcessLogTarget::OpNode => emit_at_level!("op_node", level, process, line.as_str()), ProcessLogTarget::OpConductor => { emit_at_level!("op_conductor", level, process, line.as_str()) } @@ -78,7 +79,9 @@ pub(crate) fn emit_process_log(target: ProcessLogTarget, process: &str, line: &s } fn normalize_process_level(target: ProcessLogTarget, line: &str, level: Level) -> Level { - if matches!(target, ProcessLogTarget::OpNode) + // The in-process kona consensus engine emits an expected error-level reset + // signal at startup; it flows through the native monomorphic client stream. + if matches!(target, ProcessLogTarget::WorldChainEl) && matches!(level, Level::ERROR) && line.contains("Sequencer encountered reset signal, aborting work") && line.contains("cannot continue derivation until Engine has been reset") @@ -210,10 +213,10 @@ mod tests { } #[test] - fn demotes_expected_op_node_startup_reset() { + fn demotes_expected_in_process_kona_startup_reset() { assert_eq!( normalize_process_level( - ProcessLogTarget::OpNode, + ProcessLogTarget::WorldChainEl, r#"lvl=error msg="Sequencer encountered reset signal, aborting work" err="reset: cannot continue derivation until Engine has been reset""#, Level::ERROR, ), diff --git a/crates/kona/Cargo.toml b/crates/kona/Cargo.toml new file mode 100644 index 000000000..d80cfcc34 --- /dev/null +++ b/crates/kona/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "world-chain-kona" +description = "Kona OP Stack consensus node integration for World Chain — in-process Engine API transport" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +# World-chain +world-chain-cli.workspace = true +world-chain-primitives.workspace = true + +# Kona crates — canonical ethereum-optimism +kona-node-service.workspace = true +kona-engine.workspace = true +kona-derive.workspace = true +kona-genesis.workspace = true +kona-protocol.workspace = true +kona-providers-alloy.workspace = true +kona-registry.workspace = true +kona-rpc.workspace = true + +# Reth (in-process engine handle, payload store, optimism payload types) +reth-engine-primitives.workspace = true +reth-payload-builder.workspace = true +reth-payload-primitives.workspace = true +reth-optimism-node.workspace = true + +# Alloy - shared Ethereum types +alloy-eips.workspace = true +alloy-network.workspace = true +alloy-primitives.workspace = true +alloy-provider = { workspace = true, features = ["ipc"] } +alloy-rpc-client = { workspace = true, features = ["ipc"] } +alloy-rpc-types-engine.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-transport.workspace = true +alloy-transport-http.workspace = true + +# Alloy - OP +op-alloy-network.workspace = true +op-alloy-provider.workspace = true +op-alloy-rpc-types.workspace = true +op-alloy-rpc-types-engine.workspace = true + +# General +ed25519-dalek.workspace = true +async-trait.workspace = true +jsonrpsee.workspace = true +futures.workspace = true +tokio = { workspace = true, features = ["full"] } +tokio-util.workspace = true +tracing.workspace = true +eyre.workspace = true +url.workspace = true + +[features] +default = [] diff --git a/crates/kona/src/client.rs b/crates/kona/src/client.rs new file mode 100644 index 000000000..d74992cd7 --- /dev/null +++ b/crates/kona/src/client.rs @@ -0,0 +1,409 @@ +//! In-process Engine API client for Kona. + +use std::sync::Arc; + +use alloy_eips::{BlockId, eip1898::BlockNumberOrTag}; +use alloy_network::{Ethereum, Network}; +use alloy_primitives::{Address, B256, BlockHash, StorageKey}; +use alloy_provider::{EthGetBlock, Provider, RootProvider, RpcWithBlock}; +use alloy_rpc_types_engine::{ + ClientCode, ClientVersionV1, ExecutionPayloadBodiesV1, ExecutionPayloadEnvelopeV2, + ExecutionPayloadInputV2, ExecutionPayloadV1, ExecutionPayloadV3, ForkchoiceState, + ForkchoiceUpdated, PayloadId, PayloadStatus, +}; +use alloy_rpc_types_eth::{Block, EIP1186AccountProofResponse}; +use alloy_transport::{TransportErrorKind, TransportResult}; +use alloy_transport_http::Http; +use async_trait::async_trait; + +use kona_engine::{EngineClient, EngineClientError, HyperAuthClient}; +use kona_genesis::RollupConfig; +use kona_protocol::L2BlockInfo; + +use op_alloy_network::Optimism; +use op_alloy_provider::ext::engine::OpEngineApi; +use op_alloy_rpc_types::Transaction; +use op_alloy_rpc_types_engine::{ + OpExecutionData, OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, + OpExecutionPayloadV4, OpPayloadAttributes, +}; + +use ed25519_dalek::{SigningKey, VerifyingKey}; +use reth_engine_primitives::ConsensusEngineHandle; +use reth_optimism_node::OpEngineTypes; +use reth_payload_builder::PayloadStore; +use reth_payload_primitives::PayloadTypes; +use tokio::sync::watch; +use world_chain_primitives::p2p::Authorization; + +/// OP `engine_forkchoiceUpdatedV3` payload-id version, matching the version the Flashblocks +/// payload-job generator expects authorization payload ids to carry. +const OP_PAYLOAD_ID_V3: u8 = 3; + +/// Self-authorization keys for the in-process Kona node to mint Flashblocks +/// [`Authorization`]s for the payloads it builds, mirroring rollup-boost. +#[derive(Clone)] +pub struct AuthorizerKeys { + /// The authorizer's signing key (`--flashblocks.override-authorizer-sk`). Its verifying key + /// must equal the `--flashblocks.authorizer-vk` the payload-job generator verifies against. + pub authorizer_sk: SigningKey, + /// The verifying key of the builder being authorized (`--flashblocks.builder-sk`). + pub builder_vk: VerifyingKey, +} + +/// Notifies the Flashblocks payload-job generator on each attributes-bearing forkchoice update. +#[derive(Clone)] +pub struct FlashblocksAuthorizationNotifier { + /// Watch channel to the payload-job generator. + pub to_jobs_generator: watch::Sender>, + /// Self-authorization keys, when authorizations are enabled. + pub keys: Option, +} + +// Manual `Debug` because `ed25519_dalek::SigningKey` (in `keys`) is intentionally not `Debug`; +// we only surface whether self-authorization is enabled, never the key material. +impl std::fmt::Debug for FlashblocksAuthorizationNotifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FlashblocksAuthorizationNotifier") + .field("self_authorizing", &self.keys.is_some()) + .finish_non_exhaustive() + } +} + +impl FlashblocksAuthorizationNotifier { + /// Notifies the payload-job generator for an attributes-bearing forkchoice update, minting a + /// full authorization when self-authorization keys are configured. + fn notify(&self, attributes: &OpPayloadAttributes, head_block_hash: B256) { + let authorization = self.keys.as_ref().map(|keys| { + let payload_id = attributes.payload_id(&head_block_hash, OP_PAYLOAD_ID_V3); + Authorization::new( + payload_id, + attributes.payload_attributes.timestamp, + &keys.authorizer_sk, + keys.builder_vk, + ) + }); + + self.to_jobs_generator + .send_modify(|slot| *slot = authorization); + } +} + +/// An in-process Engine API client that bridges Kona's consensus layer to reth's execution engine +/// for the consensus hot path, without any network transport. +pub struct WorldChainKonaEngineClient { + /// The OP Stack rollup configuration, shared between Kona and reth. + cfg: Arc, + /// Handle to reth's consensus engine tree. `new_payload` and `fork_choice_updated` calls are + /// dispatched here over the same channel reth's authenticated Engine API uses internally. + engine_handle: ConsensusEngineHandle, + /// Reth's payload store, used to resolve built payloads for `get_payload_v*`. + payload_store: PayloadStore, + /// L2 EL provider over reth's standard IPC RPC, used for the infrequent read methods that the + /// engine actor performs during sync and forkchoice reconstruction. + l2_provider: RootProvider, + /// L1 EL provider, used for `get_l1_block`. Reth only stores L2 data. + l1_provider: RootProvider, + /// Flashblocks payload-job authorizer, when Flashblocks is enabled. See + /// [`FlashblocksAuthorizationNotifier`]; [`None`] disables the notification entirely. + flashblocks_authorizer: Option, +} + +impl std::fmt::Debug for WorldChainKonaEngineClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WorldChainKonaEngineClient") + .field("l2_chain_id", &self.cfg.l2_chain_id) + .finish_non_exhaustive() + } +} + +impl WorldChainKonaEngineClient { + /// Creates a new in-process engine client. + /// + /// # Arguments + /// + /// * `cfg` — The OP Stack rollup configuration, shared between Kona and reth. + /// * `engine_handle` — A handle to reth's consensus engine tree, obtained from the node's + /// [`AddOnsContext`](reth_node_api::AddOnsContext) after launch. + /// * `payload_store` — Reth's store of in-progress and completed payloads. + /// * `l2_provider` — An alloy provider connected to reth's standard L2 IPC RPC, used for reads. + /// * `l1_provider` — An alloy provider for the L1 chain (deposits, finalization). + /// * `flashblocks_authorizer` — Optional [`FlashblocksAuthorizationNotifier`] (see its docs); pass [`None`] + /// when Flashblocks is disabled. + pub const fn new( + cfg: Arc, + engine_handle: ConsensusEngineHandle, + payload_store: PayloadStore, + l2_provider: RootProvider, + l1_provider: RootProvider, + flashblocks_authorizer: Option, + ) -> Self { + Self { + cfg, + engine_handle, + payload_store, + l2_provider, + l1_provider, + flashblocks_authorizer, + } + } + + /// Dispatches an [`OpExecutionData`] payload to reth's engine and maps the result into a + /// [`TransportResult`], as the Kona trait surface expects. + /// + /// The OP execution data is converted into the engine's native execution-data type via the + /// [`From`] bound on `Engine::ExecutionData`. + async fn on_new_payload(&self, data: OpExecutionData) -> TransportResult + where + Engine::ExecutionData: From, + { + self.engine_handle + .new_payload(data.into()) + .await + .map_err(|e| TransportErrorKind::custom_str(&e.to_string())) + } + + /// Dispatches a forkchoice update to reth's engine and maps the result into a + /// [`TransportResult`]. + /// + /// The OP payload attributes are converted into the engine's native attributes type via the + /// [`From`] bound on `Engine::PayloadAttributes`. + async fn on_forkchoice_updated( + &self, + state: ForkchoiceState, + attrs: Option, + ) -> TransportResult + where + Engine::PayloadAttributes: From, + { + // A forkchoice update *with* attributes starts a payload build. The Flashblocks job + // generator blocks each build until it observes a change on the jobs-generator channel, so + // we notify it here before dispatching the FCU — minting a full authorization when + // self-authorization keys are configured (as rollup-boost does), otherwise sending `None`. + // This replicates `OpEngineApiExt::{engine,flashblocks}_forkchoiceUpdatedV3`, which Kona + // bypasses by driving the engine handle directly. Without it the build hangs forever. + if let (Some(attributes), Some(authorizer)) = (&attrs, &self.flashblocks_authorizer) { + authorizer.notify(attributes, state.head_block_hash); + } + + self.engine_handle + .fork_choice_updated(state, attrs.map(Into::into)) + .await + .map_err(|e| TransportErrorKind::custom_str(&e.to_string())) + } + + /// Resolves a built payload from reth's [`PayloadStore`] by id, mapping the absence of a job + /// or a build error into a [`TransportError`]. + async fn on_get_payload(&self, payload_id: PayloadId) -> TransportResult { + match self.payload_store.resolve(payload_id).await { + Some(Ok(payload)) => Ok(payload), + Some(Err(e)) => Err(TransportErrorKind::custom_str(&e.to_string())), + None => Err(TransportErrorKind::custom_str( + "payload job not found in reth payload store", + )), + } + } +} + +#[async_trait] +impl EngineClient for WorldChainKonaEngineClient +where + Engine: PayloadTypes, + Engine::ExecutionData: From, + Engine::PayloadAttributes: From, + Engine::BuiltPayload: Into + + Into + + Into, +{ + fn cfg(&self) -> &RollupConfig { + &self.cfg + } + + fn get_l1_block(&self, block: BlockId) -> EthGetBlock<::BlockResponse> { + // L1 blocks must be fetched from the external L1 RPC — reth only has L2 data. + self.l1_provider.get_block(block) + } + + fn get_l2_block(&self, block: BlockId) -> EthGetBlock<::BlockResponse> { + self.l2_provider.get_block(block) + } + + fn get_proof( + &self, + address: Address, + keys: Vec, + ) -> RpcWithBlock<(Address, Vec), EIP1186AccountProofResponse> { + self.l2_provider.get_proof(address, keys) + } + + async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> TransportResult { + // V1 (pre-Shanghai) is unused in OP Stack post-Bedrock, but the engine task queue still + // calls it via the version-dispatched insert task. Dispatch it in-process for completeness. + self.on_new_payload(OpExecutionData::v2(ExecutionPayloadInputV2 { + execution_payload: payload, + withdrawals: None, + })) + .await + } + + async fn l2_block_by_label( + &self, + numtag: BlockNumberOrTag, + ) -> Result>, EngineClientError> { + Ok(self.l2_provider.get_block_by_number(numtag).full().await?) + } + + async fn l2_block_info_by_label( + &self, + numtag: BlockNumberOrTag, + ) -> Result, EngineClientError> { + let Some(block) = self.l2_provider.get_block_by_number(numtag).full().await? else { + return Ok(None); + }; + Ok(Some(L2BlockInfo::from_block_and_genesis( + &block.into_consensus(), + &self.cfg.genesis, + )?)) + } +} + +#[async_trait] +impl OpEngineApi> for WorldChainKonaEngineClient +where + Engine: PayloadTypes, + Engine::ExecutionData: From, + Engine::PayloadAttributes: From, + Engine::BuiltPayload: Into + + Into + + Into, +{ + async fn new_payload_v2( + &self, + payload: ExecutionPayloadInputV2, + ) -> TransportResult { + self.on_new_payload(OpExecutionData::v2(payload)).await + } + + async fn new_payload_v3( + &self, + payload: ExecutionPayloadV3, + parent_beacon_block_root: B256, + ) -> TransportResult { + // OP `newPayloadV3` carries no versioned hashes (they must be empty). + self.on_new_payload(OpExecutionData::v3( + payload, + Vec::new(), + parent_beacon_block_root, + )) + .await + } + + async fn new_payload_v4( + &self, + payload: OpExecutionPayloadV4, + parent_beacon_block_root: B256, + ) -> TransportResult { + // Isthmus variant. OP carries no versioned hashes and no execution requests on L2. + self.on_new_payload(OpExecutionData::v4( + payload, + Vec::new(), + parent_beacon_block_root, + Default::default(), + )) + .await + } + + async fn fork_choice_updated_v2( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> TransportResult { + self.on_forkchoice_updated(fork_choice_state, payload_attributes) + .await + } + + async fn fork_choice_updated_v3( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> TransportResult { + self.on_forkchoice_updated(fork_choice_state, payload_attributes) + .await + } + + async fn get_payload_v2( + &self, + payload_id: PayloadId, + ) -> TransportResult { + Ok(self.on_get_payload(payload_id).await?.into()) + } + + async fn get_payload_v3( + &self, + payload_id: PayloadId, + ) -> TransportResult { + Ok(self.on_get_payload(payload_id).await?.into()) + } + + async fn get_payload_v4( + &self, + payload_id: PayloadId, + ) -> TransportResult { + Ok(self.on_get_payload(payload_id).await?.into()) + } + + async fn get_payload_v5( + &self, + payload_id: PayloadId, + ) -> TransportResult { + // Osaka (Karst on the OP Stack) bumps the engine method version but adds no new payload + // fields on L2, so the V4-shaped envelope is reused. + Ok(self.on_get_payload(payload_id).await?.into()) + } + + async fn get_payload_bodies_by_hash_v1( + &self, + block_hashes: Vec, + ) -> TransportResult { + // Not on the consensus hot path; delegate to reth's standard RPC. + OpEngineApi::>::get_payload_bodies_by_hash_v1( + &self.l2_provider, + block_hashes, + ) + .await + } + + async fn get_payload_bodies_by_range_v1( + &self, + start: u64, + count: u64, + ) -> TransportResult { + OpEngineApi::>::get_payload_bodies_by_range_v1( + &self.l2_provider, + start, + count, + ) + .await + } + + async fn get_client_version_v1( + &self, + _client_version: ClientVersionV1, + ) -> TransportResult> { + Ok(vec![ClientVersionV1 { + // No World Chain client code exists in the enum; reth is the closest match. + code: ClientCode::RH, + name: "world-chain".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + commit: "unknown".to_string(), + }]) + } + + async fn exchange_capabilities( + &self, + capabilities: Vec, + ) -> TransportResult> { + // In-process, we support everything the engine supports; echo the peer's capabilities. + Ok(capabilities) + } +} diff --git a/crates/kona/src/config.rs b/crates/kona/src/config.rs new file mode 100644 index 000000000..d8368b5eb --- /dev/null +++ b/crates/kona/src/config.rs @@ -0,0 +1,121 @@ +//! Configuration for the in-process Kona integration. +//! +//! Bridges World Chain's node configuration to the [`KonaService`](crate::KonaService) inputs. +//! Unlike the previous HTTP transport, there is no engine RPC URL or JWT here: the engine is driven +//! in-process via an [`WorldChainKonaEngineClient`](crate::WorldChainKonaEngineClient) supplied by the node +//! add-ons. + +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, +}; + +use kona_genesis::RollupConfig; +use kona_node_service::SequencerConfig; +use kona_rpc::RpcBuilder; +use url::Url; +use world_chain_cli::KonaP2PArgs; + +/// Static configuration for the in-process Kona node. +/// +/// The dynamic, post-launch pieces (the reth engine handle, payload store, and L2 provider) are +/// injected by the add-ons when assembling the [`KonaService`](crate::KonaService). +#[derive(Debug, Clone)] +pub struct KonaConfig { + /// The OP Stack rollup configuration, shared with reth. + pub rollup_config: Arc, + + /// L1 RPC endpoint URL for fetching L1 block data. + pub l1_rpc_url: Url, + + /// L1 beacon API endpoint URL for fetching blob data. + pub l1_beacon_url: Url, + + /// Whether to trust the L1 RPC without additional receipt verification. + pub l1_trust_rpc: bool, + + /// Whether to run in sequencer mode. + pub sequencer_mode: bool, + + /// Whether the sequencer should start in the stopped state. + pub sequencer_stopped: bool, + + /// Whether the sequencer runs in recovery mode. + pub sequencer_recovery_mode: bool, + + /// Optional op-conductor RPC endpoint. When [`Some`], the conductor service is enabled. + pub conductor_rpc_url: Option, + + /// Number of L1 confirmations the sequencer waits on before building from an L1 origin. + pub l1_confs: u64, + + /// P2P network configuration arguments. + pub p2p: KonaP2PArgs, + + /// IP address the Kona node RPC server binds to. + pub rpc_addr: IpAddr, + + /// Port the Kona node RPC server binds to. + pub rpc_port: u16, + + /// Whether the admin namespace is enabled on the Kona node RPC server. + pub rpc_enable_admin: bool, + + /// Whether the Kona node RPC server is enabled. + pub rpc_enabled: bool, + + /// Optional override for L1 slot duration in seconds. + pub l1_slot_duration_override: Option, +} + +impl KonaConfig { + /// Creates a basic configuration for a validator node. + pub fn validator( + rollup_config: Arc, + l1_rpc_url: Url, + l1_beacon_url: Url, + ) -> Self { + Self { + rollup_config, + l1_rpc_url, + l1_beacon_url, + l1_trust_rpc: false, + sequencer_mode: false, + sequencer_stopped: false, + sequencer_recovery_mode: false, + conductor_rpc_url: None, + l1_confs: 4, + p2p: KonaP2PArgs::default(), + rpc_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), + rpc_port: 8547, + rpc_enable_admin: false, + rpc_enabled: true, + l1_slot_duration_override: None, + } + } + + /// Builds the [`SequencerConfig`] driving kona's sequencer actor. + pub fn make_sequencer_config(&self) -> SequencerConfig { + SequencerConfig { + sequencer_stopped: self.sequencer_stopped, + sequencer_recovery_mode: self.sequencer_recovery_mode, + conductor_rpc_url: self.conductor_rpc_url.clone(), + l1_conf_delay: self.l1_confs, + } + } + + /// Builds the [`RpcBuilder`] for kona's node RPC server, if RPC is enabled. + /// + /// Returns [`None`] when the RPC server is disabled. The server is exposed over HTTP only; + /// websocket and dev endpoints are disabled, and admin state is not persisted. + pub fn make_rpc_builder(&self) -> Option { + self.rpc_enabled.then(|| RpcBuilder { + no_restart: false, + socket: SocketAddr::new(self.rpc_addr, self.rpc_port), + enable_admin: self.rpc_enable_admin, + admin_persistence: None, + ws_enabled: false, + dev_enabled: false, + }) + } +} diff --git a/crates/kona/src/lib.rs b/crates/kona/src/lib.rs new file mode 100644 index 000000000..1abc5d8c2 --- /dev/null +++ b/crates/kona/src/lib.rs @@ -0,0 +1,40 @@ +//! # World Chain Kona Integration +//! +//! This crate runs the [Kona](https://github.com/ethereum-optimism/optimism) OP Stack consensus +//! node **in-process** alongside the reth execution engine, in the same binary. +//! +//! ## Architecture +//! +//! ```text +//! ┌──────────────────────────────────────────────────────────────┐ +//! │ world-chain binary │ +//! │ │ +//! │ ┌──────────────────┐ in-process Rust calls ┌─────────────┐ │ +//! │ │ Kona actors │ ──────────────────────► │ reth Engine │ │ +//! │ │ (consensus/deriv)│ ConsensusEngineHandle │ (EL tree) │ │ +//! │ └──────────────────┘ + PayloadStore └─────────────┘ │ +//! └──────────────────────────────────────────────────────────────┘ +//! ``` +//! +//! Unlike canonical kona — which drives reth over the authenticated Engine API (HTTP + JWT) — the +//! consensus hot path (`fork_choice_updated`, `new_payload`, `get_payload`) is dispatched directly +//! to reth's [`reth_engine_primitives::ConsensusEngineHandle`] and +//! [`reth_payload_builder::PayloadStore`] via [`WorldChainKonaEngineClient`]. There is no separate node +//! process and no network transport on that path. +//! +//! ## Key Components +//! +//! - [`WorldChainKonaEngineClient`] — Implements kona's [`kona_engine::EngineClient`] trait by +//! dispatching Engine API calls in-process to reth. +//! - [`KonaService`] — Manually assembles the kona actor graph (engine, derivation, network, L1 +//! watcher, optional sequencer, optional RPC) around the in-process engine client. +//! - [`KonaServiceHandle`] — Owns the spawned service task and its cancellation token. +//! - [`KonaConfig`] — Bridges World Chain's node configuration to the kona service inputs. + +mod client; +mod config; +mod service; + +pub use client::{AuthorizerKeys, FlashblocksAuthorizationNotifier, WorldChainKonaEngineClient}; +pub use config::KonaConfig; +pub use service::{KonaService, KonaServiceHandle, L2RpcEndpoint}; diff --git a/crates/kona/src/service.rs b/crates/kona/src/service.rs new file mode 100644 index 000000000..8da4b5f14 --- /dev/null +++ b/crates/kona/src/service.rs @@ -0,0 +1,753 @@ +//! Manual assembly of the Kona rollup node actors around an in-process engine client. +//! +//! Canonical kona drives reth's execution engine over the authenticated Engine API (HTTP + JWT) +//! via [`kona_node_service::RollupNode::start`], which hard-wires its [`EngineActor`] to an +//! [`kona_engine::OpEngineClient`]. To run the consensus hot path **in-process**, we reproduce the +//! same actor graph here but inject an [`WorldChainKonaEngineClient`](crate::WorldChainKonaEngineClient) into +//! the [`EngineActor`]/[`EngineRpcActor`]. +//! +//! The actor graph is identical to upstream: +//! +//! ```text +//! ┌───────────┐ ┌──────────────┐ ┌────────────┐ +//! │ L1 Watcher│ │ Derivation │ │ Network │ +//! └─────┬─────┘ └──────┬───────┘ └─────┬──────┘ +//! │ │ │ +//! │ ┌─────────▼─────────┐ │ +//! └─────►│ Engine Actor │◄──────┘ +//! │ (in-process EL) │ +//! └─────────┬─────────┘ +//! (+ optional Sequencer actor, + optional RPC actor) +//! ``` + +use std::{sync::Arc, time::Duration}; + +use alloy_primitives::{Address, B256, U256, b256}; +use alloy_provider::{IpcConnect, Provider, RootProvider}; +use alloy_rpc_client::ClientBuilder; +use futures::StreamExt as _; +use jsonrpsee::RpcModule; +use kona_derive::StatefulAttributesBuilder; +use kona_engine::{Engine, EngineClient, EngineState}; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_node_service::{ + BlockStream, ConductorClient, DelayedL1OriginSelectorProvider, DerivationActor, EngineActor, + EngineRpcActor, JsonrpseeServerLauncher, L1OriginSelector, L1WatcherActor, NetworkActor, + NetworkBuilder, NetworkConfig, NetworkHandler, NodeActor, QueuedDerivationEngineClient, + QueuedEngineDerivationClient, QueuedEngineRpcClient, QueuedL1WatcherDerivationClient, + QueuedNetworkEngineClient, QueuedSequencerAdminAPIClient, QueuedSequencerEngineClient, + QueuedUnsafePayloadGossipClient, RpcActor, RpcServerLauncher, SequencerActor, SequencerConfig, +}; +use kona_protocol::L2BlockInfo; +use kona_providers_alloy::{ + AlloyChainProvider, AlloyL2ChainProvider, OnlineBeaconClient, OnlineBlobProvider, + OnlinePipeline, +}; +use kona_registry::L1Config as RegisteredL1Config; +use kona_rpc::{ + AdminApiServer, AdminRpc, DevEngineApiServer, DevEngineRpc, HealthzApiServer, HealthzRpc, + OpP2PApiServer, P2pRpc, RollupNodeApiServer, RollupRpc, RpcBuilder, WsRPC, WsServer, +}; +use op_alloy_network::Optimism; +use std::ops::Not as _; +use tokio::{ + sync::{mpsc, watch}, + task::JoinSet, +}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +use reth_engine_primitives::ConsensusEngineHandle; +use reth_optimism_node::OpEngineTypes; +use reth_payload_builder::PayloadStore; +use url::Url; + +use crate::{FlashblocksAuthorizationNotifier, KonaConfig, WorldChainKonaEngineClient}; + +/// How the in-process Kona node reaches reth's standard (non-engine) L2 RPC. +/// +/// This transport is used only for the derivation pipeline and the engine actor's infrequent +/// reads — never for the consensus hot path, which is dispatched in-process via +/// [`ConsensusEngineHandle`]. IPC is preferred when reth's IPC server is enabled; otherwise we +/// fall back to reth's HTTP RPC endpoint. +#[derive(Debug, Clone)] +pub enum L2RpcEndpoint { + /// reth's IPC RPC endpoint (socket path). + Ipc(String), + /// reth's HTTP RPC endpoint. + Http(Url), +} + +const DERIVATION_PROVIDER_CACHE_SIZE: usize = 1024; +const HEAD_STREAM_POLL_INTERVAL: u64 = 4; +const FINALIZED_STREAM_POLL_INTERVAL: u64 = 60; +const CHANNEL_SIZE: usize = 1024; + +fn load_registered_l1_chain_config(l1_chain_id: u64) -> Arc { + match RegisteredL1Config::get_l1_genesis(l1_chain_id) { + Ok(config) => { + info!( + target: "world_chain::kona", + l1_chain_id, + "Loaded registered L1 chain config for in-process Kona" + ); + Arc::new(config.into()) + } + Err(error) => { + warn!( + target: "world_chain::kona", + l1_chain_id, + %error, + "failed to load registered L1 chain config; falling back to default" + ); + Arc::new(L1ChainConfig::default()) + } + } +} + +/// Inputs required to assemble and run the Kona consensus node in-process. +/// +/// These are the canonical kona node settings *minus* the Engine API transport, which is replaced +/// by the in-process [`WorldChainKonaEngineClient`]. +pub struct KonaService { + /// The OP Stack rollup configuration, shared with reth. + pub rollup_config: Arc, + /// The L1 chain configuration. + pub l1_chain_config: Arc, + /// Whether to trust the L1 RPC without receipt re-verification. + pub l1_trust_rpc: bool, + /// The L1 EL provider (used by derivation, sequencer origin selection, and L1 watcher). + pub l1_provider: RootProvider, + /// The L1 beacon client (blob data availability). + pub l1_beacon: OnlineBeaconClient, + /// The L2 EL provider over reth's standard IPC RPC (used by derivation for safe-head reads). + pub l2_provider: RootProvider, + /// The in-process engine client driving reth's execution engine. + pub engine_client: Arc, + /// Whether the node runs in sequencer mode. + pub sequencer_mode: bool, + /// The sequencer configuration. + pub sequencer_config: SequencerConfig, + /// The P2P network configuration. + pub p2p_config: NetworkConfig, + /// The Kona node RPC server configuration, if enabled. + pub rpc_builder: Option, +} + +impl KonaService { + /// Assembles a [`KonaService`] from a [`KonaConfig`] and the reth execution-layer handles + /// obtained from the node add-ons after launch. + /// + /// Constructs the [`WorldChainKonaEngineClient`] (wrapping the engine handle + payload store + L2/L1 + /// providers), the L1 beacon client, and the P2P network configuration. The `l2_endpoint` should + /// point at reth's standard (unauthenticated) RPC — IPC when available, otherwise HTTP; it is + /// used only for the derivation pipeline and the engine actor's infrequent reads, never for the + /// consensus hot path. The L1 provider remains an HTTP connection (`--kona.l1-rpc-url`). + pub async fn build( + config: KonaConfig, + engine_handle: ConsensusEngineHandle, + payload_store: PayloadStore, + l2_endpoint: L2RpcEndpoint, + flashblocks_authorizer: Option, + ) -> eyre::Result { + let l1_provider = RootProvider::new_http(config.l1_rpc_url.clone()); + let l1_chain_id: u64 = config.rollup_config.l1_chain_id; + let chain_config = load_registered_l1_chain_config(l1_chain_id); + let l2_client = match l2_endpoint { + L2RpcEndpoint::Ipc(path) => ClientBuilder::default().ipc(IpcConnect::new(path)).await?, + L2RpcEndpoint::Http(url) => ClientBuilder::default().http(url), + }; + let l2_provider = RootProvider::::new(l2_client); + + let engine_client = Arc::new(WorldChainKonaEngineClient::new( + config.rollup_config.clone(), + engine_handle, + payload_store, + l2_provider.clone(), + l1_provider.clone(), + flashblocks_authorizer, + )); + + let l2_chain_id: u64 = config.rollup_config.l2_chain_id.into(); + let mut p2p_config = config + .p2p + .clone() + .build_network_config(&config.rollup_config, l2_chain_id)?; + + // The unsafe-block signer (the address P2P gossip validates each block's signature against) + // lives in the L1 `SystemConfig` contract. Canonical kona seeds it from L1 at startup and the + // L1 watcher keeps it updated via `SystemConfigUpdate` events. Our P2P arg builder only honors + // an explicit `--p2p.unsafe.block.signer` override and otherwise leaves it zero, which makes + // gossip reject every block with `Signer { expected: 0x0, .. }`. Seed it from L1 here when not + // overridden (the L1 watcher still applies any later on-chain changes). + if p2p_config.unsafe_block_signer.is_zero() { + match fetch_unsafe_block_signer( + &l1_provider, + config.rollup_config.l1_system_config_address, + ) + .await + { + Ok(signer) => { + info!(target: "world_chain::kona", %signer, "Loaded unsafe block signer from L1 system config"); + p2p_config.unsafe_block_signer = signer; + } + Err(e) => { + warn!(target: "world_chain::kona", error = %e, "failed to fetch unsafe block signer from L1; P2P gossip will reject blocks until a SystemConfig update is observed"); + } + } + } + + let mut l1_beacon = OnlineBeaconClient::new_http(config.l1_beacon_url.to_string()); + if let Some(slot) = config.l1_slot_duration_override { + l1_beacon = l1_beacon.with_l1_slot_duration_override(slot); + } + + Ok(Self { + rollup_config: config.rollup_config.clone(), + l1_chain_config: chain_config, + l1_trust_rpc: config.l1_trust_rpc, + l1_provider, + l1_beacon, + l2_provider, + engine_client, + sequencer_mode: config.sequencer_mode, + sequencer_config: config.make_sequencer_config(), + p2p_config, + rpc_builder: config.make_rpc_builder(), + }) + } + + /// Builds an [`AlloyChainProvider`] for the L1 chain. + fn l1_derivation_provider(&self) -> AlloyChainProvider { + AlloyChainProvider::new_with_trust( + self.l1_provider.clone(), + DERIVATION_PROVIDER_CACHE_SIZE, + self.l1_trust_rpc, + ) + } + + /// Builds an [`AlloyL2ChainProvider`] over reth's standard L2 RPC. + fn l2_derivation_provider(&self) -> AlloyL2ChainProvider { + AlloyL2ChainProvider::new_with_trust( + self.l2_provider.clone(), + self.rollup_config.clone(), + DERIVATION_PROVIDER_CACHE_SIZE, + false, + ) + } + + /// Builds the [`StatefulAttributesBuilder`] used by the sequencer actor. + fn create_attributes_builder( + &self, + ) -> StatefulAttributesBuilder { + StatefulAttributesBuilder::new( + self.rollup_config.clone(), + self.l1_chain_config.clone(), + self.l2_derivation_provider(), + self.l1_derivation_provider(), + None, + ) + } + + /// Builds the online (polled) derivation pipeline. + async fn create_pipeline(&self) -> OnlinePipeline { + OnlinePipeline::new_polled( + self.rollup_config.clone(), + self.l1_chain_config.clone(), + OnlineBlobProvider::init(self.l1_beacon.clone()).await, + self.l1_derivation_provider(), + self.l2_derivation_provider(), + None, + ) + } + + /// Assembles the [`EngineActor`] and [`EngineRpcActor`] around our + /// [`WorldChainKonaEngineClient`]. + /// + /// This mirrors `RollupNode::build_engine_actors`, but injects the in-process client instead of + /// building an [`kona_engine::OpEngineClient`] from an [`kona_node_service::EngineConfig`]. + /// + /// When `el_sync_finished` is `true`, the engine starts with EL sync already considered + /// complete. This is used when reth's execution layer already holds a chain (a restart from an + /// existing/snapshotted database): rather than waiting for a gossip-driven forkchoice update to + /// confirm EL sync — which never arrives if no peer is actively gossiping unsafe blocks — the + /// engine immediately performs its initial reset (reading the EL head and issuing the bootstrap + /// forkchoice update) so derivation can proceed from L1. This mirrors Base's follower, which + /// seeds its engine from the local EL head on startup. + #[allow(clippy::type_complexity)] + fn create_engine_actors( + &self, + engine_request_rx: mpsc::Receiver, + engine_rpc_request_rx: mpsc::Receiver, + derivation_client: QueuedEngineDerivationClient, + unsafe_head_tx: watch::Sender, + el_sync_finished: bool, + ) -> ( + EngineActor, + EngineRpcActor, + watch::Receiver, + ) { + let engine_state = EngineState { + el_sync_finished, + ..Default::default() + }; + let (engine_state_tx, engine_state_rx) = watch::channel(engine_state); + let (engine_queue_length_tx, engine_queue_length_rx) = watch::channel(0); + let engine = Engine::new(engine_state, engine_state_tx, engine_queue_length_tx); + + // The unsafe-head feed is only meaningful in sequencer mode; validators ignore it. + let unsafe_head_tx_opt = self.sequencer_mode.then_some(unsafe_head_tx); + + let engine_actor = EngineActor::new( + self.engine_client.clone(), + self.rollup_config.clone(), + derivation_client, + engine, + unsafe_head_tx_opt, + engine_request_rx, + ); + + // A second receiver on the engine state, handed back to the caller to gate the L1 + // finality feed on the engine's safe head (see the finalized-stream wiring in `run`). + let engine_state_rx_for_gate = engine_state_rx.clone(); + + let engine_rpc_actor = EngineRpcActor::new( + self.engine_client.clone(), + self.rollup_config.clone(), + engine_state_rx, + engine_queue_length_rx, + engine_rpc_request_rx, + ); + + (engine_actor, engine_rpc_actor, engine_state_rx_for_gate) + } + + /// Spawns the full Kona actor graph on the current tokio runtime and runs to completion. + /// + /// Returns when any actor errors (cancelling the rest) or when a shutdown signal is observed. + /// The provided `cancellation` token is shared by all actors and is the same token the caller + /// can use to trigger a graceful shutdown. + pub async fn run(self, cancellation: CancellationToken) -> Result<(), String> { + // Cross-actor channels. The network actor's inbound channels (signer, p2p RPC, network + // admin, gossip payload) were previously surfaced via `NetworkInboundData`; upstream now + // requires the caller to own them, so we create them here. + let (derivation_actor_request_tx, derivation_actor_request_rx) = + mpsc::channel(CHANNEL_SIZE); + let (engine_actor_request_tx, engine_actor_request_rx) = mpsc::channel(CHANNEL_SIZE); + let (engine_rpc_request_tx, engine_rpc_request_rx) = mpsc::channel(CHANNEL_SIZE); + let (l1_query_tx, l1_query_rx) = mpsc::channel(CHANNEL_SIZE); + let (sequencer_admin_api_tx, sequencer_admin_api_rx) = mpsc::channel(CHANNEL_SIZE); + let (signer_tx, signer_rx) = mpsc::channel(CHANNEL_SIZE); + let (p2p_rpc_tx, p2p_rpc_rx) = mpsc::channel(CHANNEL_SIZE); + let (network_admin_tx, network_admin_rx) = mpsc::channel(CHANNEL_SIZE); + let (gossip_payload_tx, gossip_payload_rx) = mpsc::channel(CHANNEL_SIZE); + let (unsafe_head_tx, unsafe_head_rx) = watch::channel(L2BlockInfo::default()); + let (l1_head_updates_tx, l1_head_updates_rx) = watch::channel(None); + + // Determine whether reth's EL already holds a chain beyond genesis. If so, EL sync needs no + // gossip-driven snap sync to bootstrap: mark EL sync complete up front so the engine + // performs its initial reset (reading the EL head + forkchoice) and derivation can proceed + // from L1 immediately. Without this, a verifier whose peers are not gossiping unsafe blocks + // (e.g. a devnet replaying history via derivation) deadlocks in `AwaitingELSyncCompletion`. + let el_sync_finished = match self + .engine_client + .l2_block_info_by_label(alloy_eips::BlockNumberOrTag::Latest) + .await + { + Ok(Some(head)) if head.block_info.number > self.rollup_config.genesis.l2.number => { + info!( + target: "world_chain::kona", + el_head = head.block_info.number, + "reth EL already holds a chain; marking EL sync complete to bootstrap derivation from L1 without waiting for unsafe-block gossip" + ); + true + } + Ok(_) => false, + Err(e) => { + warn!( + target: "world_chain::kona", + error = %e, + "failed to read reth EL head at startup; falling back to gossip-driven EL sync" + ); + false + } + }; + + // Engine actors (in-process EL). The returned receiver tracks the engine's safe head and + // is used below to gate the L1 finality feed. + let (engine_actor, engine_rpc_actor, engine_safe_head_rx) = self.create_engine_actors( + engine_actor_request_rx, + engine_rpc_request_rx, + QueuedEngineDerivationClient::new(derivation_actor_request_tx.clone()), + unsafe_head_tx, + el_sync_finished, + ); + + // Derivation actor. + let derivation = DerivationActor::<_, OnlinePipeline>::new( + QueuedDerivationEngineClient { + engine_actor_request_tx: engine_actor_request_tx.clone(), + }, + derivation_actor_request_rx, + self.create_pipeline().await, + ); + + // Network (p2p) actor. The libp2p swarm is built and started first so the constructor + // stays synchronous, mirroring upstream `RollupNode::start`. + let network_handler: NetworkHandler = NetworkBuilder::from(self.p2p_config.clone()) + .build() + .map_err(|e| format!("failed to build network: {e:?}"))? + .start() + .await + .map_err(|e| format!("failed to start network: {e:?}"))?; + let network = NetworkActor::new( + QueuedNetworkEngineClient { + engine_actor_request_tx: engine_actor_request_tx.clone(), + }, + network_handler, + signer_rx, + p2p_rpc_rx, + network_admin_rx, + gossip_payload_rx, + ); + + // L1 origin selection (sequencer only) + L1 watcher. + let delayed_l1_provider = DelayedL1OriginSelectorProvider::new( + self.l1_provider.clone(), + l1_head_updates_rx, + self.sequencer_config.l1_conf_delay, + ); + let delayed_origin_selector = + L1OriginSelector::new(self.rollup_config.clone(), delayed_l1_provider); + + let conductor = self + .sequencer_config + .conductor_rpc_url + .clone() + .map(ConductorClient::new_http); + + // Both streams are boxed to a single concrete type because `L1WatcherActor` is generic + // over one `BlockStream` type shared by its head and finalized streams. + let head_stream = BlockStream::new_as_stream( + self.l1_provider.clone(), + alloy_eips::BlockNumberOrTag::Latest, + Duration::from_secs(HEAD_STREAM_POLL_INTERVAL), + ) + .map_err(|e| format!("failed to build L1 head stream: {e}"))? + .boxed(); + + // Clamp the L1 finality feed to the engine's safe head. + // + // kona's `FinalizeTask` rejects a finalize target above the current safe head with a + // `Critical` error, which tears down the in-process engine — and, since the engine is + // reth's only driver, the whole node. That target is the highest L2 block derived from a + // finalized L1 block, so we clamp each finalized L1 block to strictly below the safe head's + // L1 origin. Every L2 block whose L1 origin is `<=` the clamped value then lies in an epoch + // before the safe head's, so it is already safe and the derived target can never exceed the + // safe head. (Strictly below, not at: consecutive L2 blocks usually share an L1 origin, so + // the safe head's own epoch may still contain an as-yet-unsafe block.) + // + // Only `BlockInfo::number` is consumed downstream — the L1 watcher forwards the block as-is + // and kona's finalizer keys solely off the number — so clamping the number suffices. In a + // synced node the finalized L1 block already trails the safe head's origin by far more than + // this, so the clamp is a no-op; it only engages while the node replays history behind the + // finalized point (e.g. a restart bootstrapping derivation from L1 with no unsafe-block + // gossip), which is exactly the window where the unclamped feed crashes the node. + let finalized_stream = BlockStream::new_as_stream( + self.l1_provider.clone(), + alloy_eips::BlockNumberOrTag::Finalized, + Duration::from_secs(FINALIZED_STREAM_POLL_INTERVAL), + ) + .map_err(|e| format!("failed to build L1 finalized stream: {e}"))? + .map(move |mut finalized| { + let safe_origin = engine_safe_head_rx + .borrow() + .sync_state + .safe_head() + .l1_origin + .number; + finalized.number = finalized.number.clamp(0, safe_origin.saturating_sub(1)); + finalized + }) + .boxed(); + + let l1_watcher = L1WatcherActor::new( + self.rollup_config.clone(), + self.l1_provider.clone(), + l1_query_rx, + l1_head_updates_tx, + QueuedL1WatcherDerivationClient { + derivation_actor_request_tx, + }, + signer_tx, + head_stream, + finalized_stream, + ); + + // Optional sequencer actor. + let sequencer_actor = if self.sequencer_mode { + let sequencer_engine_client = QueuedSequencerEngineClient { + engine_actor_request_tx: engine_actor_request_tx.clone(), + unsafe_head_rx, + }; + let queued_gossip_client = QueuedUnsafePayloadGossipClient::new(gossip_payload_tx); + + Some(SequencerActor::new( + sequencer_admin_api_rx, + self.create_attributes_builder(), + conductor, + sequencer_engine_client, + self.sequencer_config.sequencer_stopped.not(), + self.sequencer_config.sequencer_recovery_mode, + delayed_origin_selector, + self.rollup_config.clone(), + queued_gossip_client, + )) + } else { + None + }; + let sequencer_admin_client = sequencer_actor + .is_some() + .then(|| QueuedSequencerAdminAPIClient::new(sequencer_admin_api_tx)); + + // Optional RPC actor. Upstream `RollupNode::build_rpc_actor` now assembles the JSON-RPC + // module set, performs the initial server launch, and hands the live handle to the actor; + // the actor is no longer driven by an `RpcContext`. + let rpc = if let Some(config) = self.rpc_builder.clone() { + let engine_rpc_client = QueuedEngineRpcClient::new(engine_rpc_request_tx); + let mut modules = RpcModule::new(()); + modules + .merge(HealthzApiServer::into_rpc(HealthzRpc {})) + .map_err(|e| format!("failed to register healthz module: {e:?}"))?; + modules + .merge(P2pRpc::new(p2p_rpc_tx).into_rpc()) + .map_err(|e| format!("failed to register p2p module: {e:?}"))?; + modules + .merge(AdminRpc::new(sequencer_admin_client, network_admin_tx).into_rpc()) + .map_err(|e| format!("failed to register admin module: {e:?}"))?; + modules + .merge(RollupRpc::new(engine_rpc_client.clone(), l1_query_tx).into_rpc()) + .map_err(|e| format!("failed to register rollup module: {e:?}"))?; + if config.dev_enabled() { + modules + .merge(DevEngineRpc::new(engine_rpc_client.clone()).into_rpc()) + .map_err(|e| format!("failed to register dev engine module: {e:?}"))?; + } + if config.ws_enabled() { + modules + .merge(WsRPC::new(engine_rpc_client.clone()).into_rpc()) + .map_err(|e| format!("failed to register ws module: {e:?}"))?; + } + + let restarts_remaining = config.restart_count(); + let launcher = JsonrpseeServerLauncher::new(config); + let handle = launcher + .launch(modules.clone()) + .await + .map_err(|e: std::io::Error| format!("failed to launch rpc server: {e:?}"))?; + Some(RpcActor::new(launcher, modules, handle, restarts_remaining)) + } else { + None + }; + + // Spawn all actors, cancelling the rest on first failure. This reimplements kona's + // crate-private `spawn_and_wait!` macro and `shutdown_signal()` helper: each actor's + // `step` is driven in a loop until it errors or the shared cancellation token fires. + let mut tasks: JoinSet> = JoinSet::new(); + + macro_rules! spawn_actor { + ($actor:expr) => {{ + if let Some(mut actor) = $actor { + let cancel = cancellation.clone(); + tasks.spawn(async move { + let _guard = cancel.clone().drop_guard(); + loop { + tokio::select! { + biased; + _ = cancel.cancelled() => return Ok(()), + result = actor.step() => { + result.map_err(|e| format!("{e:?}"))?; + } + } + } + }); + } + }}; + } + + spawn_actor!(rpc); + spawn_actor!(sequencer_actor); + spawn_actor!(Some(network)); + spawn_actor!(Some(l1_watcher)); + spawn_actor!(Some(derivation)); + spawn_actor!(Some(engine_actor)); + spawn_actor!(Some(engine_rpc_actor)); + + let shutdown = shutdown_signal(); + tokio::pin!(shutdown); + + loop { + tokio::select! { + _ = &mut shutdown => { + info!(target: "world_chain::kona", "Received shutdown signal, cancelling Kona actors"); + cancellation.cancel(); + return Ok(()); + } + _ = cancellation.cancelled() => { + return Ok(()); + } + result = tasks.join_next() => { + match result { + Some(Ok(Ok(()))) => {} + Some(Ok(Err(e))) => { + error!(target: "world_chain::kona", error = %e, "Kona actor failed"); + cancellation.cancel(); + return Err(e); + } + Some(Err(e)) => { + let msg = format!("Kona actor task join error: {e}"); + error!(target: "world_chain::kona", error = %e, "Kona actor task join error"); + cancellation.cancel(); + return Err(msg); + } + None => return Ok(()), + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_eips::eip7840::BlobParams; + + const SEPOLIA_L1_CHAIN_ID: u64 = 11_155_111; + const SEPOLIA_BPO2_TIMESTAMP: u64 = 1_761_607_008; + + #[test] + fn registered_l1_config_uses_sepolia_blob_schedule() { + let chain_config = load_registered_l1_chain_config(SEPOLIA_L1_CHAIN_ID); + + assert_eq!(chain_config.bpo2_time, Some(SEPOLIA_BPO2_TIMESTAMP)); + assert_ne!(chain_config.bpo2_time, L1ChainConfig::default().bpo2_time); + + let blob_schedule = chain_config.blob_schedule_blob_params(); + assert_eq!( + blob_schedule.active_scheduled_params_at_timestamp(SEPOLIA_BPO2_TIMESTAMP), + Some(&BlobParams::bpo2()) + ); + } +} + +/// Handle to a running Kona consensus node assembled in-process. +/// +/// Owns the shared [`CancellationToken`] and the spawned tokio task. Designed to be held by the +/// node's add-ons and dropped on node exit. +#[derive(Debug)] +pub struct KonaServiceHandle { + cancellation: CancellationToken, + /// Wrapped in `Option` so [`stopped`](Self::stopped) can take ownership without conflicting + /// with the `Drop` impl. + task_handle: Option>>, +} + +impl KonaServiceHandle { + /// Spawns the assembled [`KonaService`] on the current tokio runtime. + pub fn spawn(service: KonaService) -> Self { + let cancellation = CancellationToken::new(); + let token = cancellation.clone(); + let task_handle = tokio::spawn(async move { service.run(token).await }); + Self { + cancellation, + task_handle: Some(task_handle), + } + } + + /// Returns a future that resolves when the Kona service stops. + pub async fn stopped(&mut self) -> Result<(), String> { + let handle = self + .task_handle + .take() + .ok_or_else(|| "Kona service already stopped".to_string())?; + + match handle.await { + Ok(result) => result, + Err(join_error) => { + error!(target: "world_chain::kona", %join_error, "Kona service task panicked"); + Err(format!("Kona service task panicked: {join_error}")) + } + } + } + + /// Initiates graceful shutdown of the Kona service. + pub fn shutdown(&self) { + warn!(target: "world_chain::kona", "Shutting down Kona consensus node"); + self.cancellation.cancel(); + } + + /// Returns a reference to the shared cancellation token. + pub fn cancellation_token(&self) -> &CancellationToken { + &self.cancellation + } +} + +impl Drop for KonaServiceHandle { + fn drop(&mut self) { + self.cancellation.cancel(); + } +} + +/// Reads the unsafe-block signer address from the L1 `SystemConfig` contract. +/// +/// Mirrors canonical kona's startup fetch (`bin/node`'s `unsafe_block_signer`): the address is held +/// at storage slot `bytes32(uint256(keccak256("systemconfig.unsafeblocksigner")) - 1)` and read at +/// the latest L1 block. The low 20 bytes of the slot are the signer address. +async fn fetch_unsafe_block_signer( + l1_provider: &RootProvider, + system_config_address: Address, +) -> eyre::Result
{ + /// `bytes32(uint256(keccak256("systemconfig.unsafeblocksigner")) - 1)`. + const UNSAFE_BLOCK_SIGNER_SLOT: B256 = + b256!("0x65a7ed542fb37fe237fdfbdd70b31598523fe5b32879e307bae27a0bd9581c08"); + + let value = l1_provider + .get_storage_at( + system_config_address, + U256::from_be_bytes(UNSAFE_BLOCK_SIGNER_SLOT.0), + ) + .await?; + Ok(Address::from_slice(&value.to_be_bytes::<32>()[12..])) +} + +/// Listens for OS shutdown signals (SIGTERM, SIGINT). Reimplements kona's crate-private +/// `service::shutdown_signal`. +async fn shutdown_signal() { + let ctrl_c = async { + if let Err(e) = tokio::signal::ctrl_c().await { + error!(target: "world_chain::kona", error = %e, "failed to install Ctrl+C handler"); + } + }; + + #[cfg(unix)] + let terminate = async { + match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { + Ok(mut sig) => { + sig.recv().await; + } + Err(e) => { + error!(target: "world_chain::kona", error = %e, "failed to install SIGTERM handler"); + std::future::pending::<()>().await; + } + } + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {} + _ = terminate => {} + } +} diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 12be5c17f..01373e80d 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -21,6 +21,7 @@ world-chain-cli.workspace = true world-chain-payload.workspace = true world-chain-chainspec.workspace = true world-chain-evm.workspace = true +world-chain-kona.workspace = true reth-node-builder.workspace = true reth-chainspec.workspace = true @@ -35,6 +36,8 @@ reth-optimism-forks.workspace = true reth-provider.workspace = true reth-transaction-pool.workspace = true reth-node-api.workspace = true +reth-engine-primitives.workspace = true +reth-tasks.workspace = true reth-network.workspace = true reth-network-peers.workspace = true reth-eth-wire-types.workspace = true @@ -50,19 +53,21 @@ reth-codecs.workspace = true alloy-primitives.workspace = true alloy-rpc-types.workspace = true -alloy-rpc-types-eth.workspace = true -alloy-rpc-types-engine.workspace = true alloy-consensus.workspace = true -alloy-eips.workspace = true op-alloy-consensus.workspace = true -op-alloy-rpc-types-engine.workspace = true +kona-genesis.workspace = true + +# Optional deps for the `test` feature (also present in [dev-dependencies] for `cargo test`). +alloy-rpc-types-eth = { workspace = true, optional = true } +alloy-rpc-types-engine = { workspace = true, optional = true } tokio.workspace = true eyre.workspace = true tracing.workspace = true ed25519-dalek.workspace = true hex.workspace = true +serde_json.workspace = true [build-dependencies] vergen.workspace = true @@ -75,3 +80,13 @@ reth-provider.workspace = true reth-node-api.workspace = true reth-node-builder.workspace = true reth-db.workspace = true +alloy-eips.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-rpc-types-engine.workspace = true +op-alloy-rpc-types-engine.workspace = true + + +[features] +test = ["dep:alloy-rpc-types-eth", "dep:alloy-rpc-types-engine"] + +default = [] \ No newline at end of file diff --git a/crates/node/src/add_ons.rs b/crates/node/src/add_ons.rs index fe5c001ef..0e3599350 100644 --- a/crates/node/src/add_ons.rs +++ b/crates/node/src/add_ons.rs @@ -2,6 +2,10 @@ use core::marker::PhantomData; +use reth_engine_primitives::ConsensusEngineHandle; +use reth_payload_builder::PayloadStore; +use reth_tasks::TaskExecutor; + use alloy_consensus::{Block, BlockBody, Header}; use alloy_primitives::Sealed; use op_alloy_consensus::{OpTransaction, TxPostExec}; @@ -16,7 +20,7 @@ use reth_node_builder::rpc::{ use reth_optimism_chainspec::OpHardfork; use reth_optimism_evm::ConfigurePostExecEvm; use reth_optimism_forks::OpHardforks; -use reth_optimism_node::{OpEngineApiBuilder, txpool::OpPooledTx}; +use reth_optimism_node::{OpEngineApiBuilder, OpEngineTypes, txpool::OpPooledTx}; use reth_optimism_payload_builder::{ OpPayloadBuilderAttributes, OpPayloadPrimitives, config::{OpDAConfig, OpGasLimitConfig}, @@ -36,9 +40,15 @@ use reth_rpc_api::{ }; use reth_rpc_server_types::RethRpcModule; use reth_transaction_pool::TransactionPool; -use tracing::{debug, info}; +use tracing::{debug, error, info}; use world_chain_chainspec::WorldChainSpec; +use world_chain_cli::KonaArgs; use world_chain_evm::OpTx; +use world_chain_kona::{ + FlashblocksAuthorizationNotifier, KonaConfig, KonaService, KonaServiceHandle, L2RpcEndpoint, +}; + +use crate::context::build_kona_config; use world_chain_rpc::{ EthApiExtServer, SequencerClient as WorldChainSequencerClient, Simulate, SimulateApiServer, WorldChainEthApiExt, @@ -110,6 +120,21 @@ pub struct WorldChainAddOns< min_suggested_priority_fee: u64, /// Enables the World Chain simulate namespace. simulate_enabled: bool, + /// In-process Kona consensus startup intent (the enabled `--kona.*` CLI args). + /// + /// When [`Some`], [`launch_add_ons`](NodeAddOns::launch_add_ons) builds the [`KonaConfig`] from + /// these args (failing the launch if the rollup config is missing/unreadable/unparsable), + /// assembles a [`WorldChainKonaEngineClient`] from reth's engine handle, and spawns the Kona + /// consensus node in-process. The build is deferred to launch so misconfiguration aborts node + /// startup rather than being silently swallowed. + /// + /// [`KonaConfig`]: world_chain_kona::KonaConfig + /// [`WorldChainKonaEngineClient`]: world_chain_kona::WorldChainKonaEngineClient + kona_args: Option, + /// Flashblocks payload-job authorizer, plumbed into the in-process Kona engine client so a + /// forkchoice update with attributes notifies (and optionally authorizes) the generator. See + /// [`FlashblocksAuthorizationNotifier`]; [`None`] when Flashblocks is disabled. + flashblocks_authorizer: Option, /// Transaction type carried by the node primitives. _tx: PhantomData Tx>, } @@ -143,6 +168,8 @@ where enable_tx_conditional, min_suggested_priority_fee, simulate_enabled, + kona_args: None, + flashblocks_authorizer: None, _tx: PhantomData, } } @@ -154,6 +181,28 @@ where N: FullNodeComponents, EthB: EthApiBuilder, { + /// Sets the enabled `--kona.*` CLI args which signal the add-ons to build a + /// [`KonaConfig`](world_chain_kona::KonaConfig) and spawn an in-process Consensus Engine during + /// launch. + /// + /// Passing [`Some`] defers the fallible config build to + /// [`launch_add_ons`](NodeAddOns::launch_add_ons), so a misconfigured-but-enabled Kona aborts + /// node startup instead of silently disabling consensus. + pub fn with_kona_args(mut self, kona_args: Option) -> Self { + self.kona_args = kona_args; + self + } + + /// Sets the Flashblocks payload-job authorizer plumbed into the in-process Kona engine client, + /// so a forkchoice update with attributes notifies (and optionally authorizes) the generator. + pub fn with_flashblocks_authorizer( + mut self, + flashblocks_authorizer: Option, + ) -> Self { + self.flashblocks_authorizer = flashblocks_authorizer; + self + } + /// Maps the [`EngineApiBuilder`] builder type. pub fn with_engine_api( self, @@ -299,7 +348,7 @@ impl NodeAddOns for WorldChainAddOns where N: FullNodeComponents< - Types: NodeTypes, + Types: NodeTypes, Evm: ConfigurePostExecEvm< NextBlockEnvCtx: BuildNextEnv< OpPayloadBuilderAttributes, @@ -342,9 +391,36 @@ where enable_tx_conditional, historical_rpc, simulate_enabled, + kona_args, + flashblocks_authorizer, .. } = self; + // Capture the inputs the in-process Kona consensus node needs from `ctx` *before* + // `launch_add_ons_with` consumes it. The authoritative L2 IPC endpoint is read from the + // live RPC server handle after launch (below), so we only stash the engine-layer handles + // here. We also fail fast if the IPC server is disabled, since Kona connects over it. + // + // The [`KonaConfig`](world_chain_kona::KonaConfig) is built here (not in `add_ons`, which + // cannot return errors) so that an enabled-but-misconfigured Kona — a missing, unreadable, + // or unparsable rollup config — aborts node startup via `?` rather than silently starting + // without a consensus engine. + let kona_inputs = kona_args + .map(|kona_args| -> eyre::Result<_> { + let kona_config = build_kona_config(&kona_args)?; + let engine_handle = ctx.beacon_engine_handle.clone(); + let payload_store = PayloadStore::new(ctx.node.payload_builder_handle().clone()); + let task_executor = ctx.node.task_executor().clone(); + Ok(( + kona_config, + engine_handle, + payload_store, + task_executor, + flashblocks_authorizer, + )) + }) + .transpose()?; + let eth_config = EthConfigHandler::new(ctx.node.provider().clone(), ctx.node.evm_config().clone()); @@ -400,7 +476,7 @@ where let flashblocks_op_api = FlashblocksOpApi; let provider = ctx.node.provider().clone(); - rpc_add_ons + let handle = rpc_add_ons .launch_add_ons_with(ctx, move |container| { let reth_node_builder::rpc::RpcModuleContainer { modules, @@ -446,7 +522,46 @@ where Ok(()) }) - .await + .await?; + + // Now that the RPC server is live, spawn the in-process Kona consensus node (if enabled). + // Kona reaches reth's standard (non-engine) L2 RPC over IPC when the IPC server is enabled, + // otherwise it falls back to the HTTP RPC endpoint. Both are read from the running server so + // they reflect what reth actually bound, rather than being re-derived from config. + if let Some(( + kona_config, + engine_handle, + payload_store, + task_executor, + flashblocks_authorizer, + )) = kona_inputs + { + let rpc = &handle.rpc_server_handles.rpc; + let l2_endpoint = match rpc.ipc_endpoint() { + Some(ipc_path) => L2RpcEndpoint::Ipc(ipc_path), + None => { + let http_url = rpc.http_url().ok_or_else(|| { + eyre::Report::msg( + "--kona.enabled requires reth's IPC or HTTP RPC server \ + (enable at least one of --ipc / --http)", + ) + })?; + L2RpcEndpoint::Http(http_url.parse()?) + } + }; + + spawn_kona( + kona_config, + engine_handle, + payload_store, + task_executor, + l2_endpoint, + flashblocks_authorizer, + ) + .await?; + } + + Ok(handle) } } @@ -454,7 +569,7 @@ impl RethRpcAddOns for WorldChainAddOns where N: FullNodeComponents< - Types: NodeTypes, + Types: NodeTypes, Evm: ConfigurePostExecEvm< NextBlockEnvCtx: BuildNextEnv< OpPayloadBuilderAttributes, @@ -505,3 +620,73 @@ where EngineValidatorAddOn::engine_validator_builder(&self.rpc_add_ons) } } + +/// Assembles the in-process Kona consensus node from reth's engine handle and the live RPC +/// server's IPC endpoint, then spawns it on the node's task executor. +/// +/// Builds the kona service's in-process engine client from: +/// - `engine_handle` — reth's `ConsensusEngineHandle`, for the FCU / new-payload consensus hot +/// path, +/// - `payload_store` — wrapping reth's payload builder handle, for `get_payload`, +/// - an L2 alloy provider connected over `l2_endpoint` (the live RPC endpoint reported by the +/// running server — IPC when enabled, otherwise HTTP — not re-derived from config), for the +/// infrequent reads the engine actor performs during sync, +/// - an L1 alloy provider over HTTP from `--kona.l1-rpc-url`. +/// +/// Connecting the L2 provider may be asynchronous (IPC), so this is an `async fn`. The assembled +/// [`KonaService`] is then run on the provided `task_executor` for the node's lifetime. +async fn spawn_kona( + kona_config: KonaConfig, + engine_handle: ConsensusEngineHandle, + payload_store: PayloadStore, + task_executor: TaskExecutor, + l2_endpoint: L2RpcEndpoint, + flashblocks_authorizer: Option, +) -> eyre::Result<()> { + let sequencer_mode = kona_config.sequencer_mode; + let l1_chain_id = kona_config.rollup_config.l1_chain_id; + let l2_chain_id: u64 = kona_config.rollup_config.l2_chain_id.into(); + + let service = KonaService::build( + kona_config, + engine_handle, + payload_store, + l2_endpoint, + flashblocks_authorizer, + ) + .await?; + + info!( + target: "world_chain::kona", + %l1_chain_id, + %l2_chain_id, + sequencer = sequencer_mode, + "Starting in-process Kona consensus node (direct ConsensusEngineHandle transport)" + ); + + // Spawn on the node's task executor so the service lives for the node's lifetime. + // + // The in-process Kona node is reth's only engine driver: if it stops while the node is + // running, reth keeps serving but silently stops advancing (no FCU / new-payload), leaving a + // half-dead node that liveness probes on the EL don't catch. So its termination is always + // fatal. `spawn_critical_task` only notifies reth's `TaskManager` on a panic (normal + // completion is ignored), and it wraps the future in `select(on_shutdown, ..)` — during a + // legitimate node shutdown `on_shutdown` wins and this future is dropped before `stopped()` + // resolves. So panicking here fires only when Kona dies on its own, bringing the whole node + // down so the orchestrator restarts the pod and op-conductor fails over. + task_executor.spawn_critical_task("kona-consensus", async move { + let mut handle = KonaServiceHandle::spawn(service); + match handle.stopped().await { + Ok(()) => { + error!(target: "world_chain::kona", "Kona consensus node stopped unexpectedly; bringing down the node"); + panic!("Kona consensus node stopped unexpectedly"); + } + Err(error) => { + error!(target: "world_chain::kona", %error, "Kona consensus node exited with error; bringing down the node"); + panic!("Kona consensus node exited with error: {error}"); + } + } + }); + + Ok(()) +} diff --git a/crates/node/src/context.rs b/crates/node/src/context.rs index 6f418d09b..bf7b5c5e4 100644 --- a/crates/node/src/context.rs +++ b/crates/node/src/context.rs @@ -1,6 +1,6 @@ // Module defining World Chain Node Preset contexts for components & add-ons. -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use crate::{ add_ons::WorldChainAddOns, @@ -29,7 +29,8 @@ use reth_optimism_node::{ use reth_optimism_primitives::OpPrimitives; use reth_optimism_rpc::OpEthApiBuilder; use world_chain_chainspec::WorldChainSpec; -use world_chain_cli::{WorldChainArgs, WorldChainNodeConfig}; +use world_chain_cli::{KonaArgs, WorldChainArgs, WorldChainNodeConfig}; +use world_chain_kona::{AuthorizerKeys, FlashblocksAuthorizationNotifier, KonaConfig}; use world_chain_p2p::{ monitor::PeerMonitor, protocol::{ @@ -368,6 +369,31 @@ where 1_000_000, self.config.args.simulate_enabled, ) + .with_kona_args(self.config.args.kona.clone()) + .with_flashblocks_authorizer(self.components_context.as_ref().map( + |flashblocks_components_ctx| { + // Mirror rollup-boost: when self-authorization keys are configured + // (`--flashblocks.override-authorizer-sk` + `--flashblocks.builder-sk`), the + // in-process Kona node mints full authorizations for the payloads it builds. + let keys = self + .config + .args + .flashblocks + .as_ref() + .and_then(|flashblocks| { + let authorizer_sk = flashblocks.override_authorizer_sk.clone()?; + let builder_sk = flashblocks.builder_sk.as_ref()?; + Some(AuthorizerKeys { + authorizer_sk, + builder_vk: builder_sk.verifying_key(), + }) + }); + FlashblocksAuthorizationNotifier { + to_jobs_generator: flashblocks_components_ctx.to_jobs_generator.clone(), + keys, + } + }, + )) } fn ext_context(&self) -> Self::ExtContext { @@ -375,6 +401,49 @@ where } } +/// Builds a [`KonaConfig`](world_chain_kona::KonaConfig) from the parsed `--kona.*` CLI arguments. +/// +/// The rollup configuration is loaded from the JSON file referenced by `--kona.rollup-config`, +/// which is required when Kona is enabled. Returns an error (which the caller propagates to fail +/// node startup) if the rollup config is missing, unreadable, or unparsable. +pub(crate) fn build_kona_config( + kona_args: &KonaArgs, +) -> eyre::Result { + let l1_rpc_url = kona_args.l1_rpc_url.parse()?; + let l1_beacon_url = kona_args.l1_beacon_url.parse()?; + + let rollup_config_path = kona_args.rollup_config_path.as_ref().ok_or_else(|| { + eyre::Report::msg("--kona.rollup-config is required when --kona.enabled is set") + })?; + let config_json = std::fs::read_to_string(rollup_config_path).map_err(|e| { + eyre::Report::msg(format!( + "failed to read rollup config from {}: {e}", + rollup_config_path.display() + )) + })?; + + let rollup_config: kona_genesis::RollupConfig = serde_json::from_str(&config_json) + .map_err(|e| eyre::Report::msg(format!("failed to parse rollup config: {e}")))?; + + Ok(KonaConfig { + rollup_config: Arc::new(rollup_config), + l1_rpc_url, + l1_beacon_url, + l1_trust_rpc: kona_args.l1_trust_rpc, + sequencer_mode: kona_args.sequencer, + sequencer_stopped: kona_args.sequencer_stopped, + sequencer_recovery_mode: kona_args.sequencer_recovery_mode, + conductor_rpc_url: kona_args.conductor_rpc.clone(), + l1_confs: kona_args.l1_confs, + p2p: kona_args.p2p.clone(), + rpc_addr: kona_args.rpc_addr, + rpc_port: kona_args.rpc_port, + rpc_enable_admin: kona_args.rpc_enable_admin, + rpc_enabled: !kona_args.rpc_disabled, + l1_slot_duration_override: kona_args.l1_slot_duration_override, + }) +} + #[derive(Clone, Debug)] pub struct FlashblocksComponentsContext { pub flashblocks_handle: FlashblocksHandle, diff --git a/crates/node/src/dev.rs b/crates/node/src/dev.rs new file mode 100644 index 000000000..54a812002 --- /dev/null +++ b/crates/node/src/dev.rs @@ -0,0 +1,87 @@ +use std::sync::Arc; + +use alloy_consensus::{BlockHeader, Header}; +use alloy_primitives::{Address, B64}; +use reth_chainspec::{BaseFeeParams, EthereumHardforks}; +use reth_node_api::PayloadAttributesBuilder; +use reth_optimism_forks::OpHardforks; +use reth_optimism_node::OpPayloadAttributes; +use reth_optimism_payload_builder::OpPayloadAttrs; +use reth_primitives_traits::SealedHeader; + +/// Builds [`OpPayloadAttrs`] for local/dev-mode payload generation. +/// +/// Mirrors `reth_optimism_node::node::OpLocalPayloadAttributesBuilder`, which is +/// not re-exported by upstream. Used by [`DebugNode`] when running in dev mode. +pub(crate) struct OpLocalPayloadAttributesBuilder { + pub(crate) chain_spec: Arc, +} + +impl PayloadAttributesBuilder + for OpLocalPayloadAttributesBuilder +where + ChainSpec: EthereumHardforks + OpHardforks + Send + Sync + 'static, +{ + fn build(&self, parent: &SealedHeader
) -> OpPayloadAttrs { + let timestamp = std::cmp::max( + parent.timestamp().saturating_add(1), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + + let eth_attrs = alloy_rpc_types_engine::PayloadAttributes { + timestamp, + prev_randao: alloy_primitives::B256::random(), + suggested_fee_recipient: Address::random(), + withdrawals: self + .chain_spec + .is_shanghai_active_at_timestamp(timestamp) + .then(Default::default), + parent_beacon_block_root: self + .chain_spec + .is_cancun_active_at_timestamp(timestamp) + .then(alloy_primitives::B256::random), + slot_number: self + .chain_spec + .is_amsterdam_active_at_timestamp(timestamp) + .then_some(0), + }; + + // Dummy system transaction for dev mode. + // OP Mainnet transaction at index 0 in block 124665056. + const TX_SET_L1_BLOCK: [u8; 251] = alloy_primitives::hex!( + "7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985" + ); + + let default_params = BaseFeeParams::optimism(); + let denominator = std::env::var("OP_DEV_EIP1559_DENOMINATOR") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(default_params.max_change_denominator as u32); + let elasticity = std::env::var("OP_DEV_EIP1559_ELASTICITY") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(default_params.elasticity_multiplier as u32); + let gas_limit = std::env::var("OP_DEV_GAS_LIMIT") + .ok() + .and_then(|v| v.parse::().ok()); + + let mut eip1559_bytes = [0u8; 8]; + eip1559_bytes[0..4].copy_from_slice(&denominator.to_be_bytes()); + eip1559_bytes[4..8].copy_from_slice(&elasticity.to_be_bytes()); + + OpPayloadAttrs(OpPayloadAttributes { + payload_attributes: eth_attrs, + transactions: Some(vec![TX_SET_L1_BLOCK.into()]), + no_tx_pool: None, + gas_limit, + eip_1559_params: Some(B64::from(eip1559_bytes)), + min_base_fee: self + .chain_spec + .is_jovian_active_at_timestamp(timestamp) + .then_some(0), + }) + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index a49e195ed..821e9979d 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -1,5 +1,8 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] +#[cfg(any(test, feature = "test"))] +mod dev; + pub mod add_ons; pub mod context; pub mod engine; diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index 89df72568..e9d0df933 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -1,35 +1,35 @@ -use std::{fmt::Debug, sync::Arc}; +use std::fmt::Debug; use crate::pool::WorldChainPoolBuilder; -use alloy_consensus::{Block, BlockBody, BlockHeader, Header}; -use alloy_eips::eip1559::BaseFeeParams; -use alloy_primitives::{Address, B64}; -use op_alloy_consensus::OpTxEnvelope; -use op_alloy_rpc_types_engine::OpPayloadAttributes; +use alloy_consensus::{Block, BlockBody, Header}; + use reth_chainspec::EthChainSpec; use reth_codecs::{Compress, Decompress}; use reth_evm::ConfigureEvm; -use reth_node_api::{ - BlockTy, BuiltPayload, FullNodeTypes, NodeAddOns, NodePrimitives, NodeTypes, - PayloadAttributesBuilder, -}; +use reth_node_api::{BuiltPayload, FullNodeTypes, NodeAddOns, NodePrimitives, NodeTypes}; use reth_node_builder::{ - DebugNode, FullNodeComponents, Node, NodeAdapter, NodeComponents, NodeComponentsBuilder, - PayloadTypes, + Node, NodeAdapter, NodeComponents, NodeComponentsBuilder, PayloadTypes, components::{ComponentsBuilder, NetworkBuilder, PayloadServiceBuilder}, rpc::{EngineValidatorAddOn, RethRpcAddOns}, }; -use reth_node_core::primitives::EthereumHardforks; use reth_optimism_evm::OpNextBlockEnvAttributes; -use reth_optimism_forks::OpHardforks; use reth_optimism_node::{OpStorage, node::OpConsensusBuilder, payload::OpPayloadAttrs}; -use reth_optimism_primitives::OpPrimitives; -use reth_primitives_traits::{ReceiptTy, SealedHeader, TxTy}; +use reth_primitives_traits::{ReceiptTy, TxTy}; use reth_rpc_eth_api::EthApiTypes; use reth_transaction_pool::TransactionPool; use world_chain_cli::WorldChainNodeConfig; use world_chain_evm::WorldChainExecutorBuilder; +#[cfg(any(test, feature = "test"))] +use { + op_alloy_consensus::OpTxEnvelope, + reth_node_api::{BlockTy, PayloadAttributesBuilder}, + reth_node_builder::{DebugNode, FullNodeComponents}, + reth_node_core::primitives::EthereumHardforks, + reth_optimism_forks::OpHardforks, + reth_optimism_primitives::OpPrimitives, +}; + /// Primitive types for a World Chain node implementation. /// /// This trait parameterizes `NodeTypes` inherited via `WorldChainNode`. @@ -208,6 +208,7 @@ where } } +#[cfg(any(test, feature = "test"))] impl DebugNode for WorldChainNode where N: FullNodeComponents, @@ -224,6 +225,10 @@ where fn local_payload_attributes_builder( chain_spec: &Self::ChainSpec, ) -> impl PayloadAttributesBuilder<::PayloadAttributes> { + use std::sync::Arc; + + use crate::dev::OpLocalPayloadAttributesBuilder; + OpLocalPayloadAttributesBuilder { chain_spec: Arc::new(chain_spec.clone()), } @@ -236,80 +241,3 @@ impl NodeTypes for WorldChainNode { type Storage = OpStorage>; type Payload = T::Payload; } - -/// Builds [`OpPayloadAttrs`] for local/dev-mode payload generation. -/// -/// Mirrors `reth_optimism_node::node::OpLocalPayloadAttributesBuilder`, which is -/// not re-exported by upstream. Used by [`DebugNode`] when running in dev mode. -struct OpLocalPayloadAttributesBuilder { - chain_spec: Arc, -} - -impl PayloadAttributesBuilder - for OpLocalPayloadAttributesBuilder -where - ChainSpec: EthereumHardforks + OpHardforks + Send + Sync + 'static, -{ - fn build(&self, parent: &SealedHeader
) -> OpPayloadAttrs { - let timestamp = std::cmp::max( - parent.timestamp().saturating_add(1), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(), - ); - - let eth_attrs = alloy_rpc_types_engine::PayloadAttributes { - timestamp, - prev_randao: alloy_primitives::B256::random(), - suggested_fee_recipient: Address::random(), - withdrawals: self - .chain_spec - .is_shanghai_active_at_timestamp(timestamp) - .then(Default::default), - parent_beacon_block_root: self - .chain_spec - .is_cancun_active_at_timestamp(timestamp) - .then(alloy_primitives::B256::random), - slot_number: self - .chain_spec - .is_amsterdam_active_at_timestamp(timestamp) - .then_some(0), - }; - - // Dummy system transaction for dev mode. - // OP Mainnet transaction at index 0 in block 124665056. - const TX_SET_L1_BLOCK: [u8; 251] = alloy_primitives::hex!( - "7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985" - ); - - let default_params = BaseFeeParams::optimism(); - let denominator = std::env::var("OP_DEV_EIP1559_DENOMINATOR") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(default_params.max_change_denominator as u32); - let elasticity = std::env::var("OP_DEV_EIP1559_ELASTICITY") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(default_params.elasticity_multiplier as u32); - let gas_limit = std::env::var("OP_DEV_GAS_LIMIT") - .ok() - .and_then(|v| v.parse::().ok()); - - let mut eip1559_bytes = [0u8; 8]; - eip1559_bytes[0..4].copy_from_slice(&denominator.to_be_bytes()); - eip1559_bytes[4..8].copy_from_slice(&elasticity.to_be_bytes()); - - OpPayloadAttrs(OpPayloadAttributes { - payload_attributes: eth_attrs, - transactions: Some(vec![TX_SET_L1_BLOCK.into()]), - no_tx_pool: None, - gas_limit, - eip_1559_params: Some(B64::from(eip1559_bytes)), - min_base_fee: self - .chain_spec - .is_jovian_active_at_timestamp(timestamp) - .then_some(0), - }) - } -} diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 7244984ba..332c3bbf3 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -123,6 +123,7 @@ pub fn test_config_with_peers_and_gossip( builder, pbh, flashblocks, + kona: None, tx_peers, disable_bootnodes: true, simulate_enabled: false, diff --git a/e2e-tests/src/it/testsuite.rs b/e2e-tests/src/it/testsuite.rs index 060307411..fa05d8bd9 100644 --- a/e2e-tests/src/it/testsuite.rs +++ b/e2e-tests/src/it/testsuite.rs @@ -2351,6 +2351,7 @@ async fn test_peer_monitoring() -> eyre::Result<()> { builder, pbh, flashblocks: Some(test_flashblocks_args(&authorizer_sk, &builder_sk)), + kona: None, tx_peers: None, disable_bootnodes: true, simulate_enabled: false, diff --git a/pkg/contracts/test/FeeEscrow.t.sol b/pkg/contracts/test/FeeEscrow.t.sol index 3fc8d5b49..b6629be35 100644 --- a/pkg/contracts/test/FeeEscrow.t.sol +++ b/pkg/contracts/test/FeeEscrow.t.sol @@ -188,6 +188,34 @@ contract TestFeeEscrow is Test { function test_BurnInvalid_RevertsInsufficientBurn() public { deal(address(escrow), 1 ether); vm.warp(block.timestamp + MINIMUM_INTERVAL + 1); + + // Force `expectedBurn` far above any plausible swap output so the burn is + // deterministically insufficient, independent of the live pool/oracle spread. + // A very low WLD price inflates the required WLD-per-ETH well beyond what a + // 1 ETH swap can deliver. + vm.mockCall( + ETH_USD_ORACLE, + abi.encodeWithSelector(IChainLinkPriceFeed.priceFeed.selector), + abi.encode( + IChainLinkPriceFeed.PriceFeedData({ + price: int192(2500e8), + timestamp: uint32(block.timestamp), + expiresAt: uint32(block.timestamp + 1 hours) + }) + ) + ); + vm.mockCall( + WLD_USD_ORACLE, + abi.encodeWithSelector(IChainLinkPriceFeed.priceFeed.selector), + abi.encode( + IChainLinkPriceFeed.PriceFeedData({ + price: int192(0.01e8), + timestamp: uint32(block.timestamp), + expiresAt: uint32(block.timestamp + 1 hours) + }) + ) + ); + burnExecutor.setShouldRepay(false); vm.prank(address(burnExecutor)); vm.expectPartialRevert(FeeEscrow.InsufficientBurn.selector); diff --git a/specs/cli/reference.md b/specs/cli/reference.md index c4f147950..938810211 100644 --- a/specs/cli/reference.md +++ b/specs/cli/reference.md @@ -231,6 +231,298 @@ Flashblocks: [env: FLASHBLOCKS_FORCE_RECEIVE_PEERS=] +Kona Consensus Node: + --kona.enabled + Enable the in-process Kona consensus node. + + When enabled, the world-chain binary acts as both the execution and consensus client. + + --kona.l1-rpc-url + L1 execution RPC URL for fetching deposits, batches, and finalization signals + + [env: KONA_L1_RPC_URL=] + [default: http://localhost:8545] + + --kona.l1-beacon-url + L1 beacon API URL for fetching blob data (required post-Dencun) + + [env: KONA_L1_BEACON_URL=] + [default: http://localhost:5052] + + --kona.l1-trust-rpc + Trust the L1 RPC without additional receipt verification + +Kona P2P: + --p2p.no-discovery + Disable Discv5 (node discovery) + + [env: KONA_NODE_P2P_NO_DISCOVERY=] + + --p2p.priv.path + Read the hex-encoded 32-byte private key for the peer ID from this txt file. Created if not already exists. Important to persist to keep the same network identity after restarting + + [env: KONA_NODE_P2P_PRIV_PATH=] + + --p2p.priv.raw + The hex-encoded 32-byte private key for the peer ID + + [env: KONA_NODE_P2P_PRIV_RAW=] + + --p2p.advertise.ip + IP address or DNS hostname to advertise to external peers from Discv5. Uses `p2p.listen.ip` if not set. Setting this disables dynamic ENR updates + + [env: KONA_NODE_P2P_ADVERTISE_IP=] + + --p2p.advertise.tcp + TCP port to advertise. Same as `p2p.listen.tcp` if not set + + [env: KONA_NODE_P2P_ADVERTISE_TCP_PORT=] + + --p2p.advertise.udp + UDP port to advertise. Same as `p2p.listen.udp` if not set + + [env: KONA_NODE_P2P_ADVERTISE_UDP_PORT=] + + --p2p.listen.ip + IP address or DNS hostname to bind LibP2P/Discv5 to + + [env: KONA_NODE_P2P_LISTEN_IP=] + [default: 0.0.0.0] + + --p2p.listen.tcp + TCP port to bind LibP2P to. Any available system port if set to 0 + + [env: KONA_NODE_P2P_LISTEN_TCP_PORT=] + [default: 9222] + + --p2p.listen.udp + UDP port to bind Discv5 to. Same as TCP port if left 0 + + [env: KONA_NODE_P2P_LISTEN_UDP_PORT=] + [default: 9223] + + --p2p.peers.lo + Low-tide peer count. The node actively searches for new peer connections if below this + + [env: KONA_NODE_P2P_PEERS_LO=] + [default: 20] + + --p2p.peers.hi + High-tide peer count. The node starts pruning peer connections after reaching this + + [env: KONA_NODE_P2P_PEERS_HI=] + [default: 30] + + --p2p.peers.grace + Grace period (seconds) to keep a newly connected peer around + + [env: KONA_NODE_P2P_PEERS_GRACE=] + [default: 30] + + --p2p.gossip.mesh.d + GossipSub topic stable mesh target count (desired outbound degree) + + [env: KONA_NODE_P2P_GOSSIP_MESH_D=] + [default: 8] + + --p2p.gossip.mesh.lo + GossipSub topic stable mesh low watermark + + [env: KONA_NODE_P2P_GOSSIP_MESH_DLO=] + [default: 6] + + --p2p.gossip.mesh.dhi + GossipSub topic stable mesh high watermark + + [env: KONA_NODE_P2P_GOSSIP_MESH_DHI=] + [default: 12] + + --p2p.gossip.mesh.dlazy + GossipSub gossip target (announcements of IHAVE) + + [env: KONA_NODE_P2P_GOSSIP_MESH_DLAZY=] + [default: 6] + + --p2p.gossip.mesh.floodpublish + Publish messages to all known peers on the topic, outside of the mesh + + [env: KONA_NODE_P2P_GOSSIP_FLOOD_PUBLISH=] + + --p2p.scoring + Peer scoring strategy: none or light + + [env: KONA_NODE_P2P_SCORING=] + [default: light] + + --p2p.ban.peers + Ban peers based on their score + + [env: KONA_NODE_P2P_BAN_PEERS=] + + --p2p.ban.threshold + Score threshold below which peers are banned + + [env: KONA_NODE_P2P_BAN_THRESHOLD=] + [default: -100] + + --p2p.ban.duration + Duration in minutes to ban a peer for + + [env: KONA_NODE_P2P_BAN_DURATION=] + [default: 60] + + --p2p.discovery.interval + Interval in seconds to find peers using the discovery service + + [env: KONA_NODE_P2P_DISCOVERY_INTERVAL=] + [default: 5] + + --p2p.discovery.randomize + Seconds to wait before removing a random peer from discovery to rotate the peer set + + [env: KONA_NODE_P2P_DISCOVERY_RANDOMIZE=] + + --p2p.bootstore + Directory to store the bootstore + + [env: KONA_NODE_P2P_BOOTSTORE=] + + --p2p.no-bootstore + Disable the bootstore + + [env: KONA_NODE_P2P_NO_BOOTSTORE=] + + --p2p.redial + Max redial attempts for a disconnected peer. 0 = unlimited + + [env: KONA_NODE_P2P_REDIAL=] + [default: 500] + + --p2p.redial.period + Duration in minutes of the peer dial period + + [env: KONA_NODE_P2P_REDIAL_PERIOD=] + [default: 60] + + --p2p.bootnodes + Comma-separated list of bootnode ENRs or enode URLs + + [env: KONA_NODE_P2P_BOOTNODES=] + + --p2p.topic-scoring + Enable topic scoring (being phased out, for backwards-compat/debugging only) + + [env: KONA_NODE_P2P_TOPIC_SCORING=] + + --p2p.unsafe.block.signer + Override the unsafe block signer address. By default fetched from rollup config's system config on L1 + + [env: KONA_NODE_P2P_UNSAFE_BLOCK_SIGNER=] + + --p2p.sequencer.key + Local private key for the sequencer to sign unsafe blocks + + [env: KONA_NODE_P2P_SEQUENCER_KEY=] + + --p2p.sequencer.key.path + Path to a file containing the sequencer private key + + [env: KONA_NODE_P2P_SEQUENCER_KEY_PATH=] + + --p2p.signer.endpoint + URL of the remote signer endpoint + + [env: KONA_NODE_P2P_SIGNER_ENDPOINT=] + + --p2p.signer.address + Address to sign transactions for (required with remote signer) + + [env: KONA_NODE_P2P_SIGNER_ADDRESS=] + + --p2p.signer.header + Headers for the remote signer. Format: `key=value` + + [env: KONA_NODE_P2P_SIGNER_HEADER=] + + --p2p.signer.tls.ca + Path to CA certificates for the remote signer + + [env: KONA_NODE_P2P_SIGNER_TLS_CA=] + + --p2p.signer.tls.cert + Path to the client certificate for the remote signer + + [env: KONA_NODE_P2P_SIGNER_TLS_CERT=] + + --p2p.signer.tls.key + Path to the client key for the remote signer + + [env: KONA_NODE_P2P_SIGNER_TLS_KEY=] + + --kona.rollup-config + Path to the OP Stack rollup configuration JSON file. + + This file defines the rollup parameters (chain ID, block time, hardfork activation timestamps, genesis hashes, etc.) used by the Kona consensus node. It follows the same format as op-node's `--rollup.config` flag. + + [env: KONA_ROLLUP_CONFIG=] + + --kona.sequencer + Run the Kona consensus node in sequencer mode. + + When set, the node builds and gossips unsafe blocks rather than only following the chain. + + [env: KONA_SEQUENCER=] + + --kona.sequencer.stopped + Start the sequencer in the stopped state. + + Block production must be resumed explicitly (e.g. via the admin RPC or op-conductor). + + [env: KONA_SEQUENCER_STOPPED=] + + --kona.sequencer.recover + Run the sequencer in recovery mode + + [env: KONA_SEQUENCER_RECOVER=] + + --kona.sequencer.l1-confs + Number of L1 confirmations the sequencer waits on before building from an L1 origin + + [env: KONA_SEQUENCER_L1_CONFS=] + [default: 4] + + --kona.conductor.rpc + URL of the op-conductor RPC endpoint. When set, the conductor service is enabled + + [env: KONA_CONDUCTOR_RPC=] + + --kona.rpc.addr + IP address the Kona node RPC server binds to + + [env: KONA_RPC_ADDR=] + [default: 0.0.0.0] + + --kona.rpc.port + Port the Kona node RPC server binds to + + [env: KONA_RPC_PORT=] + [default: 8547] + + --kona.rpc.enable-admin + Enable the admin namespace on the Kona node RPC server + + [env: KONA_RPC_ENABLE_ADMIN=] + + --kona.rpc.disabled + Disable the Kona node RPC server entirely + + [env: KONA_RPC_DISABLED=] + + --kona.l1-slot-duration-override + Override the L1 slot duration (in seconds) used by the L1 watcher + + [env: KONA_L1_SLOT_DURATION_OVERRIDE=] + --tx-peers Comma-separated list of peer IDs to which transactions should be propagated