diff --git a/.gitignore b/.gitignore index e24e971..487d601 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ /target -CLAUDE.md .DS_Store /docs -# Local test artifacts -local-test/ +CLAUDE.md + diff --git a/Cargo.lock b/Cargo.lock index f94aa36..3cae88d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4742,20 +4742,19 @@ name = "morph-engine-api" version = "0.7.5" dependencies = [ "alloy-consensus", + "alloy-eips", "alloy-genesis", + "alloy-hardforks", "alloy-primitives", "alloy-rpc-types-engine", "async-trait", "auto_impl", - "dashmap 6.1.0", - "eyre", "jsonrpsee", "morph-chainspec", "morph-payload-types", "morph-primitives", - "reth-execution-types", + "parking_lot", "reth-node-api", - "reth-node-builder", "reth-payload-builder", "reth-payload-primitives", "reth-primitives-traits", @@ -4802,6 +4801,7 @@ dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", "clap", + "dashmap 6.1.0", "eyre", "morph-chainspec", "morph-consensus", @@ -4812,10 +4812,13 @@ dependencies = [ "morph-primitives", "morph-rpc", "morph-txpool", + "parking_lot", + "reqwest", "reth-chainspec", "reth-db", "reth-engine-local", "reth-engine-tree", + "reth-errors", "reth-node-api", "reth-node-builder", "reth-node-core", @@ -4829,7 +4832,11 @@ dependencies = [ "reth-tasks", "reth-tracing", "reth-transaction-pool", + "reth-trie", + "serde", + "serde_json", "tokio", + "tokio-stream", ] [[package]] @@ -6330,7 +6337,7 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "reth-basic-payload-builder" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6354,7 +6361,7 @@ dependencies = [ [[package]] name = "reth-chain-state" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6385,7 +6392,7 @@ dependencies = [ [[package]] name = "reth-chainspec" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -6405,7 +6412,7 @@ dependencies = [ [[package]] name = "reth-cli" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-genesis", "clap", @@ -6419,7 +6426,7 @@ dependencies = [ [[package]] name = "reth-cli-commands" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -6497,7 +6504,7 @@ dependencies = [ [[package]] name = "reth-cli-runner" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "reth-tasks", "tokio", @@ -6507,7 +6514,7 @@ dependencies = [ [[package]] name = "reth-cli-util" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -6524,7 +6531,7 @@ dependencies = [ [[package]] name = "reth-codecs" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6544,7 +6551,7 @@ dependencies = [ [[package]] name = "reth-codecs-derive" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "proc-macro2", "quote", @@ -6554,7 +6561,7 @@ dependencies = [ [[package]] name = "reth-config" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "eyre", "humantime-serde", @@ -6570,7 +6577,7 @@ dependencies = [ [[package]] name = "reth-consensus" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -6583,7 +6590,7 @@ dependencies = [ [[package]] name = "reth-consensus-common" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6595,7 +6602,7 @@ dependencies = [ [[package]] name = "reth-consensus-debug-client" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6621,7 +6628,7 @@ dependencies = [ [[package]] name = "reth-db" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "derive_more", @@ -6647,7 +6654,7 @@ dependencies = [ [[package]] name = "reth-db-api" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -6675,7 +6682,7 @@ dependencies = [ [[package]] name = "reth-db-common" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -6705,7 +6712,7 @@ dependencies = [ [[package]] name = "reth-db-models" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -6720,7 +6727,7 @@ dependencies = [ [[package]] name = "reth-discv4" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -6745,7 +6752,7 @@ dependencies = [ [[package]] name = "reth-discv5" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -6769,7 +6776,7 @@ dependencies = [ [[package]] name = "reth-dns-discovery" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "data-encoding", @@ -6793,7 +6800,7 @@ dependencies = [ [[package]] name = "reth-downloaders" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6824,7 +6831,7 @@ dependencies = [ [[package]] name = "reth-ecies" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "aes", "alloy-primitives", @@ -6852,7 +6859,7 @@ dependencies = [ [[package]] name = "reth-engine-local" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -6875,7 +6882,7 @@ dependencies = [ [[package]] name = "reth-engine-primitives" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6900,7 +6907,7 @@ dependencies = [ [[package]] name = "reth-engine-service" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "futures", "pin-project", @@ -6923,7 +6930,7 @@ dependencies = [ [[package]] name = "reth-engine-tree" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eip7928", @@ -6977,7 +6984,7 @@ dependencies = [ [[package]] name = "reth-engine-util" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -7005,7 +7012,7 @@ dependencies = [ [[package]] name = "reth-era" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7020,7 +7027,7 @@ dependencies = [ [[package]] name = "reth-era-downloader" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "bytes", @@ -7036,7 +7043,7 @@ dependencies = [ [[package]] name = "reth-era-utils" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7058,7 +7065,7 @@ dependencies = [ [[package]] name = "reth-errors" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "reth-consensus", "reth-execution-errors", @@ -7069,7 +7076,7 @@ dependencies = [ [[package]] name = "reth-eth-wire" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-chains", "alloy-primitives", @@ -7097,7 +7104,7 @@ dependencies = [ [[package]] name = "reth-eth-wire-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -7118,7 +7125,7 @@ dependencies = [ [[package]] name = "reth-ethereum-cli" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "clap", "eyre", @@ -7140,7 +7147,7 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7156,7 +7163,7 @@ dependencies = [ [[package]] name = "reth-ethereum-engine-primitives" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -7174,7 +7181,7 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -7187,7 +7194,7 @@ dependencies = [ [[package]] name = "reth-ethereum-payload-builder" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7216,7 +7223,7 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7236,7 +7243,7 @@ dependencies = [ [[package]] name = "reth-etl" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "rayon", "reth-db-api", @@ -7246,7 +7253,7 @@ dependencies = [ [[package]] name = "reth-evm" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7270,7 +7277,7 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7291,7 +7298,7 @@ dependencies = [ [[package]] name = "reth-execution-errors" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-evm", "alloy-primitives", @@ -7304,7 +7311,7 @@ dependencies = [ [[package]] name = "reth-execution-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7322,7 +7329,7 @@ dependencies = [ [[package]] name = "reth-exex" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7360,7 +7367,7 @@ dependencies = [ [[package]] name = "reth-exex-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -7374,7 +7381,7 @@ dependencies = [ [[package]] name = "reth-fs-util" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "serde", "serde_json", @@ -7384,7 +7391,7 @@ dependencies = [ [[package]] name = "reth-invalid-block-hooks" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7412,7 +7419,7 @@ dependencies = [ [[package]] name = "reth-ipc" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "bytes", "futures", @@ -7432,7 +7439,7 @@ dependencies = [ [[package]] name = "reth-libmdbx" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "bitflags 2.10.0", "byteorder", @@ -7448,7 +7455,7 @@ dependencies = [ [[package]] name = "reth-mdbx-sys" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "bindgen 0.71.1", "cc", @@ -7457,7 +7464,7 @@ dependencies = [ [[package]] name = "reth-metrics" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "futures", "metrics", @@ -7469,7 +7476,7 @@ dependencies = [ [[package]] name = "reth-net-banlist" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "ipnet", @@ -7478,7 +7485,7 @@ dependencies = [ [[package]] name = "reth-net-nat" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "futures-util", "if-addrs", @@ -7492,7 +7499,7 @@ dependencies = [ [[package]] name = "reth-network" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7548,7 +7555,7 @@ dependencies = [ [[package]] name = "reth-network-api" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7573,7 +7580,7 @@ dependencies = [ [[package]] name = "reth-network-p2p" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7595,7 +7602,7 @@ dependencies = [ [[package]] name = "reth-network-peers" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -7610,7 +7617,7 @@ dependencies = [ [[package]] name = "reth-network-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eip2124", "humantime-serde", @@ -7624,7 +7631,7 @@ dependencies = [ [[package]] name = "reth-nippy-jar" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "anyhow", "bincode", @@ -7641,7 +7648,7 @@ dependencies = [ [[package]] name = "reth-node-api" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-rpc-types-engine", "eyre", @@ -7665,7 +7672,7 @@ dependencies = [ [[package]] name = "reth-node-builder" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7734,7 +7741,7 @@ dependencies = [ [[package]] name = "reth-node-core" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7790,7 +7797,7 @@ dependencies = [ [[package]] name = "reth-node-ethereum" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-network", @@ -7828,7 +7835,7 @@ dependencies = [ [[package]] name = "reth-node-ethstats" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7852,7 +7859,7 @@ dependencies = [ [[package]] name = "reth-node-events" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7876,7 +7883,7 @@ dependencies = [ [[package]] name = "reth-node-metrics" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "bytes", "eyre", @@ -7899,7 +7906,7 @@ dependencies = [ [[package]] name = "reth-node-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "reth-chainspec", "reth-db-api", @@ -7911,7 +7918,7 @@ dependencies = [ [[package]] name = "reth-optimism-primitives" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7926,7 +7933,7 @@ dependencies = [ [[package]] name = "reth-payload-builder" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7947,7 +7954,7 @@ dependencies = [ [[package]] name = "reth-payload-builder-primitives" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "pin-project", "reth-payload-primitives", @@ -7959,7 +7966,7 @@ dependencies = [ [[package]] name = "reth-payload-primitives" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7982,7 +7989,7 @@ dependencies = [ [[package]] name = "reth-payload-util" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7992,7 +7999,7 @@ dependencies = [ [[package]] name = "reth-payload-validator" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -8002,7 +8009,7 @@ dependencies = [ [[package]] name = "reth-primitives-traits" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8035,7 +8042,7 @@ dependencies = [ [[package]] name = "reth-provider" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8078,7 +8085,7 @@ dependencies = [ [[package]] name = "reth-prune" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8106,7 +8113,7 @@ dependencies = [ [[package]] name = "reth-prune-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "arbitrary", @@ -8121,7 +8128,7 @@ dependencies = [ [[package]] name = "reth-revm" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "reth-primitives-traits", @@ -8134,7 +8141,7 @@ dependencies = [ [[package]] name = "reth-rpc" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -8216,7 +8223,7 @@ dependencies = [ [[package]] name = "reth-rpc-api" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eip7928", "alloy-eips", @@ -8246,7 +8253,7 @@ dependencies = [ [[package]] name = "reth-rpc-builder" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-network", "alloy-provider", @@ -8287,7 +8294,7 @@ dependencies = [ [[package]] name = "reth-rpc-convert" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-evm", @@ -8308,7 +8315,7 @@ dependencies = [ [[package]] name = "reth-rpc-engine-api" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8338,7 +8345,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-api" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -8382,7 +8389,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8430,7 +8437,7 @@ dependencies = [ [[package]] name = "reth-rpc-layer" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-rpc-types-engine", "http", @@ -8444,7 +8451,7 @@ dependencies = [ [[package]] name = "reth-rpc-server-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8460,7 +8467,7 @@ dependencies = [ [[package]] name = "reth-stages" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8505,7 +8512,7 @@ dependencies = [ [[package]] name = "reth-stages-api" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8532,7 +8539,7 @@ dependencies = [ [[package]] name = "reth-stages-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "arbitrary", @@ -8546,7 +8553,7 @@ dependencies = [ [[package]] name = "reth-static-file" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "parking_lot", @@ -8566,7 +8573,7 @@ dependencies = [ [[package]] name = "reth-static-file-types" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "clap", @@ -8579,7 +8586,7 @@ dependencies = [ [[package]] name = "reth-storage-api" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8603,7 +8610,7 @@ dependencies = [ [[package]] name = "reth-storage-errors" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8620,7 +8627,7 @@ dependencies = [ [[package]] name = "reth-tasks" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "auto_impl", "dyn-clone", @@ -8638,7 +8645,7 @@ dependencies = [ [[package]] name = "reth-tokio-util" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "tokio", "tokio-stream", @@ -8648,7 +8655,7 @@ dependencies = [ [[package]] name = "reth-tracing" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "clap", "eyre", @@ -8665,7 +8672,7 @@ dependencies = [ [[package]] name = "reth-tracing-otlp" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "clap", "eyre", @@ -8682,7 +8689,7 @@ dependencies = [ [[package]] name = "reth-transaction-pool" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8722,7 +8729,7 @@ dependencies = [ [[package]] name = "reth-trie" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8748,7 +8755,7 @@ dependencies = [ [[package]] name = "reth-trie-common" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8775,7 +8782,7 @@ dependencies = [ [[package]] name = "reth-trie-db" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "metrics", @@ -8795,7 +8802,7 @@ dependencies = [ [[package]] name = "reth-trie-parallel" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8820,7 +8827,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8839,7 +8846,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse-parallel" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8857,7 +8864,7 @@ dependencies = [ [[package]] name = "reth-zstd-compressors" version = "1.10.2" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" dependencies = [ "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index 8bb0828..56826a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,58 +54,59 @@ morph-rpc = { path = "crates/rpc" } morph-revm = { path = "crates/revm", default-features = false } morph-txpool = { path = "crates/txpool", default-features = false } -reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-chain-state = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-cli = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-cli-commands = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-cli-util = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-codecs = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-consensus = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-consensus-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-db-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-e2e-test-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-engine-local = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-engine-tree = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-errors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-eth-wire-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-ethereum-cli = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-ethereum-consensus = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-ethereum-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2", default-features = false } -reth-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-execution-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-metrics = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-network-peers = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-node-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-node-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-node-core = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-node-metrics = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-payload-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-payload-util = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2", default-features = false } -reth-provider = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-rpc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-rpc-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-rpc-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-rpc-eth-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-rpc-server-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-storage-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-tasks = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-tracing = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2" } -reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2", default-features = false } +reth-basic-payload-builder = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-chain-state = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-chainspec = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-cli = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-cli-commands = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-cli-util = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-codecs = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-codecs-derive = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-consensus = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-consensus-common = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-db = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-db-api = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-e2e-test-utils = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-engine-local = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-engine-primitives = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-engine-tree = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-errors = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-eth-wire-types = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-ethereum = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-ethereum-cli = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-ethereum-consensus = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-ethereum-engine-primitives = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-ethereum-primitives = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1", default-features = false } +reth-evm = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-evm-ethereum = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-execution-types = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-metrics = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-network-peers = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-node-api = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-node-builder = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-node-core = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-node-ethereum = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-node-metrics = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-payload-builder = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-payload-primitives = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-payload-util = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-primitives-traits = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1", default-features = false } +reth-provider = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-rpc = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-rpc-api = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-rpc-builder = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-rpc-convert = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-rpc-eth-api = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-rpc-eth-types = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-rpc-server-types = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-storage-api = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-tasks = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-tracing = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-trie = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-transaction-pool = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1" } +reth-zstd-compressors = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1", default-features = false } -reth-revm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.2", features = [ +reth-revm = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1", features = [ "std", "optional-checks", ] } @@ -170,6 +171,7 @@ test-fuzz = "7" thiserror = "2.0.14" # TODO: restrict this to only the required features tokio = { version = "1.45.1", features = ["full"] } +tokio-stream = "0.1.17" tokio-util = "0.7.16" tracing = "0.1.41" tracing-subscriber = "0.3.19" diff --git a/crates/chainspec/res/genesis/hoodi.json b/crates/chainspec/res/genesis/hoodi.json index 2d394be..6f2bd4d 100644 --- a/crates/chainspec/res/genesis/hoodi.json +++ b/crates/chainspec/res/genesis/hoodi.json @@ -19,6 +19,8 @@ "curieBlock": 0, "terminalTotalDifficulty": 0, "morph203Time": 0, + "viridianTime": 1761544800, + "emeraldTime": 1766988000, "morph": { "useZktrie": true, "maxTxPayloadBytesPerBlock": 122880, diff --git a/crates/chainspec/res/genesis/mainnet.json b/crates/chainspec/res/genesis/mainnet.json index 4f70885..bcc61f0 100644 --- a/crates/chainspec/res/genesis/mainnet.json +++ b/crates/chainspec/res/genesis/mainnet.json @@ -16,6 +16,9 @@ "shanghaiBlock": 0, "bernoulliBlock": 0, "curieBlock": 0, + "morph203Time": 1747029600, + "viridianTime": 1762149600, + "emeraldTime": 1767765600, "terminalTotalDifficulty": 0, "morph": { "useZktrie": true, diff --git a/crates/chainspec/src/constants.rs b/crates/chainspec/src/constants.rs index 6001752..3190fce 100644 --- a/crates/chainspec/src/constants.rs +++ b/crates/chainspec/src/constants.rs @@ -1,6 +1,6 @@ //! Morph chainspec constants. -use alloy_primitives::{B256, b256}; +use alloy_primitives::{Address, B256, U256, address, b256}; /// The Morph Mainnet chain ID. pub const MORPH_MAINNET_CHAIN_ID: u64 = 2818; @@ -35,3 +35,17 @@ pub const MORPH_HOODI_GENESIS_HASH: B256 = /// Source: go-ethereum/params/config.go pub const MORPH_HOODI_GENESIS_STATE_ROOT: B256 = b256!("0a31941eb1853862c0c38f378eb0c519e9e66f0942e39b47dca38c0437ab6b3e"); + +// ============================================================================= +// L2 System Contract Constants +// ============================================================================= + +/// L2 Message Queue contract address. +/// +/// Manages the L1-to-L2 message queue and stores the withdraw trie root. +pub const L2_MESSAGE_QUEUE_ADDRESS: Address = address!("5300000000000000000000000000000000000001"); + +/// Storage slot for the withdraw trie root (`messageRoot`) in L2MessageQueue contract. +/// +/// This is slot 33, which stores the Merkle root for L2->L1 messages. +pub const L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT: U256 = U256::from_limbs([33, 0, 0, 0]); diff --git a/crates/chainspec/src/hardfork.rs b/crates/chainspec/src/hardfork.rs index 81b38db..df2dd5c 100644 --- a/crates/chainspec/src/hardfork.rs +++ b/crates/chainspec/src/hardfork.rs @@ -164,14 +164,19 @@ pub trait MorphHardforks: EthereumHardforks { } impl From for SpecId { + /// Maps a [`MorphHardfork`] to the closest standard [`SpecId`]. + /// + /// The mapping must match go-ethereum Morph's EVM instruction sets: + /// - Bernoulli/Curie/Morph203 = CANCUN gas tables (MCOPY, TSTORE/TLOAD, transient storage) + /// - Viridian = PRAGUE (adds EIP-7702 delegation designator) + /// - Emerald/Jade = OSAKA (adds EIP-7939 CLZ opcode) fn from(value: MorphHardfork) -> Self { match value { - MorphHardfork::Bernoulli => Self::OSAKA, - MorphHardfork::Curie => Self::OSAKA, - MorphHardfork::Morph203 => Self::OSAKA, - MorphHardfork::Viridian => Self::OSAKA, - MorphHardfork::Emerald => Self::OSAKA, - MorphHardfork::Jade => Self::OSAKA, + MorphHardfork::Bernoulli | MorphHardfork::Curie | MorphHardfork::Morph203 => { + Self::CANCUN + } + MorphHardfork::Viridian => Self::PRAGUE, + MorphHardfork::Emerald | MorphHardfork::Jade => Self::OSAKA, } } } @@ -179,22 +184,17 @@ impl From for SpecId { impl From for MorphHardfork { /// Maps a [`SpecId`] to the *latest compatible* [`MorphHardfork`]. /// - /// Note: this is intentionally not a strict inverse of - /// `From for SpecId`, because multiple Morph - /// hardforks may share the same underlying EVM spec. + /// Since multiple Morph hardforks share the same SpecId (e.g. + /// Bernoulli/Curie/Morph203 all map to CANCUN), this returns the + /// latest hardfork for the given spec level. fn from(spec: SpecId) -> Self { - if spec.is_enabled_in(SpecId::from(Self::Jade)) { + if spec.is_enabled_in(SpecId::OSAKA) { Self::Jade - } else if spec.is_enabled_in(SpecId::from(Self::Emerald)) { - Self::Emerald - } else if spec.is_enabled_in(SpecId::from(Self::Viridian)) { + } else if spec.is_enabled_in(SpecId::PRAGUE) { Self::Viridian - } else if spec.is_enabled_in(SpecId::from(Self::Morph203)) { - Self::Morph203 - } else if spec.is_enabled_in(SpecId::from(Self::Curie)) { - Self::Curie } else { - Self::Bernoulli + // CANCUN or below → Morph203 (latest CANCUN-level hardfork) + Self::Morph203 } } } @@ -280,4 +280,21 @@ mod tests { assert!(!MorphHardfork::Emerald.is_jade()); assert!(MorphHardfork::Jade.is_jade()); } + + #[test] + fn test_morph_hardfork_to_specid_mapping() { + assert_eq!(SpecId::from(MorphHardfork::Bernoulli), SpecId::CANCUN); + assert_eq!(SpecId::from(MorphHardfork::Curie), SpecId::CANCUN); + assert_eq!(SpecId::from(MorphHardfork::Morph203), SpecId::CANCUN); + assert_eq!(SpecId::from(MorphHardfork::Viridian), SpecId::PRAGUE); + assert_eq!(SpecId::from(MorphHardfork::Emerald), SpecId::OSAKA); + assert_eq!(SpecId::from(MorphHardfork::Jade), SpecId::OSAKA); + } + + #[test] + fn test_specid_to_morph_hardfork_mapping() { + assert_eq!(MorphHardfork::from(SpecId::CANCUN), MorphHardfork::Morph203); + assert_eq!(MorphHardfork::from(SpecId::PRAGUE), MorphHardfork::Viridian); + assert_eq!(MorphHardfork::from(SpecId::OSAKA), MorphHardfork::Jade); + } } diff --git a/crates/chainspec/src/morph.rs b/crates/chainspec/src/morph.rs index 1004869..6a2ebaf 100644 --- a/crates/chainspec/src/morph.rs +++ b/crates/chainspec/src/morph.rs @@ -48,10 +48,18 @@ mod tests { #[test] fn test_morph_mainnet_hardforks() { - // Block-based hardforks should be active at block 0 + // Block-based hardforks: both Bernoulli and Curie active from block 0 assert!(MORPH_MAINNET.is_bernoulli_active_at_block(0)); - // Curie is activated at a later block on mainnet - // Timestamp-based hardforks may not be active at timestamp 0 on mainnet - // depending on the genesis configuration + assert!(MORPH_MAINNET.is_curie_active_at_block(0)); + + // Timestamp-based hardforks from go-ethereum MorphMainnetChainConfig + assert!(!MORPH_MAINNET.is_morph203_active_at_timestamp(1747029599)); + assert!(MORPH_MAINNET.is_morph203_active_at_timestamp(1747029600)); + + assert!(!MORPH_MAINNET.is_viridian_active_at_timestamp(1762149599)); + assert!(MORPH_MAINNET.is_viridian_active_at_timestamp(1762149600)); + + assert!(!MORPH_MAINNET.is_emerald_active_at_timestamp(1767765599)); + assert!(MORPH_MAINNET.is_emerald_active_at_timestamp(1767765600)); } } diff --git a/crates/chainspec/src/morph_hoodi.rs b/crates/chainspec/src/morph_hoodi.rs index 5b77c71..bf6746b 100644 --- a/crates/chainspec/src/morph_hoodi.rs +++ b/crates/chainspec/src/morph_hoodi.rs @@ -50,9 +50,15 @@ mod tests { // Block-based hardforks should be active at block 0 assert!(MORPH_HOODI.is_bernoulli_active_at_block(0)); assert!(MORPH_HOODI.is_curie_active_at_block(0)); - // Timestamp-based hardforks should be active at timestamp 0 + + // Timestamp-based hardforks from go-ethereum MorphHoodiChainConfig + // Morph203 is active from genesis (timestamp 0) assert!(MORPH_HOODI.is_morph203_active_at_timestamp(0)); - // Note: Viridian and Emerald may not be active at timestamp 0 on Hoodi - // depending on the genesis configuration + + assert!(!MORPH_HOODI.is_viridian_active_at_timestamp(1761544799)); + assert!(MORPH_HOODI.is_viridian_active_at_timestamp(1761544800)); + + assert!(!MORPH_HOODI.is_emerald_active_at_timestamp(1766987999)); + assert!(MORPH_HOODI.is_emerald_active_at_timestamp(1766988000)); } } diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index 178f8cc..5fd9543 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -112,11 +112,12 @@ fn build_hardforks(genesis: &Genesis, chain_info: &MorphGenesisInfo) -> ChainHar // Merge all Morph hardforks into the base hardforks hardforks.extend(block_forks.chain(time_forks)); - // Activate Prague at Emerald time to enable EIP-7702 support - if let Some(emerald_time) = hardfork_info.emerald_time { + // Activate Prague at Viridian time to align with go-ethereum's + // EIP-7702 (SetCode tx) activation point. + if let Some(viridian_time) = hardfork_info.viridian_time { hardforks.insert( EthereumHardfork::Prague, - ForkCondition::Timestamp(emerald_time), + ForkCondition::Timestamp(viridian_time), ); } @@ -713,8 +714,8 @@ mod tests { } #[test] - fn test_prague_activated_with_emerald() { - // Prague should be activated at the same time as Emerald + fn test_prague_activated_with_viridian() { + // Prague should be activated at the same time as Viridian // This enables EIP-7702 in the transaction pool validator let genesis_json = json!({ "config": { @@ -748,23 +749,23 @@ mod tests { serde_json::from_value(genesis_json).expect("genesis should be valid"); let chainspec = MorphChainSpec::from(genesis); - // Prague should not be active before Emerald - assert!(!chainspec.is_prague_active_at_timestamp(2999)); + // Prague should not be active before Viridian + assert!(!chainspec.is_prague_active_at_timestamp(1999)); - // Prague should be active at Emerald time - assert!(chainspec.is_prague_active_at_timestamp(3000)); + // Prague should be active at Viridian time + assert!(chainspec.is_prague_active_at_timestamp(2000)); - // Prague should remain active after Emerald + // Prague should remain active after Viridian assert!(chainspec.is_prague_active_at_timestamp(5000)); // Verify the fork condition is set correctly let prague_activation = chainspec.ethereum_fork_activation(EthereumHardfork::Prague); - assert_eq!(prague_activation, ForkCondition::Timestamp(3000)); + assert_eq!(prague_activation, ForkCondition::Timestamp(2000)); } #[test] - fn test_prague_not_activated_without_emerald() { - // If Emerald is not configured, Prague should not be activated + fn test_prague_not_activated_without_viridian() { + // If Viridian is not configured, Prague should not be activated let genesis_json = json!({ "config": { "chainId": 1337, @@ -786,7 +787,6 @@ mod tests { "bernoulliBlock": 0, "curieBlock": 0, "morph203Time": 1000, - "viridianTime": 2000, "morph": {} }, "alloc": {} @@ -796,7 +796,7 @@ mod tests { serde_json::from_value(genesis_json).expect("genesis should be valid"); let chainspec = MorphChainSpec::from(genesis); - // Prague should not be active since Emerald is not configured + // Prague should not be active since Viridian is not configured assert!(!chainspec.is_prague_active_at_timestamp(0)); assert!(!chainspec.is_prague_active_at_timestamp(5000)); } diff --git a/crates/engine-api/Cargo.toml b/crates/engine-api/Cargo.toml index fec1689..654bab0 100644 --- a/crates/engine-api/Cargo.toml +++ b/crates/engine-api/Cargo.toml @@ -18,25 +18,24 @@ morph-primitives = { workspace = true, features = ["reth-codec"] } # reth reth-node-api.workspace = true -reth-node-builder.workspace = true reth-payload-builder.workspace = true reth-payload-primitives.workspace = true reth-primitives-traits.workspace = true reth-provider.workspace = true reth-rpc-api.workspace = true -reth-execution-types.workspace = true # alloy alloy-consensus.workspace = true +alloy-eips.workspace = true +alloy-hardforks.workspace = true alloy-primitives.workspace = true alloy-rpc-types-engine.workspace = true # misc async-trait.workspace = true auto_impl.workspace = true -dashmap.workspace = true -eyre.workspace = true jsonrpsee = { workspace = true, features = ["server", "macros"] } +parking_lot.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/engine-api/src/api.rs b/crates/engine-api/src/api.rs index 1c044d6..f5c9082 100644 --- a/crates/engine-api/src/api.rs +++ b/crates/engine-api/src/api.rs @@ -5,6 +5,7 @@ //! by the sequencer to produce new blocks. use crate::EngineApiResult; +use alloy_primitives::B256; use morph_payload_types::{AssembleL2BlockParams, ExecutableL2Data, GenericResponse, SafeL2Data}; use morph_primitives::MorphHeader; @@ -20,6 +21,7 @@ use morph_primitives::MorphHeader; /// - `validate_l2_block`: Validate an L2 block without importing it /// - `new_l2_block`: Import and finalize a new L2 block /// - `new_safe_l2_block`: Import a safe L2 block from derivation +/// - `set_block_tags`: Update safe/finalized block tags without importing a block #[async_trait::async_trait] #[auto_impl::auto_impl(Arc, &, Box)] pub trait MorphL2EngineApi: Send + Sync { @@ -45,8 +47,10 @@ pub trait MorphL2EngineApi: Send + Sync { /// Validate an L2 block without importing it. /// - /// This method validates a block by re-executing it and comparing the results. - /// If the block has been previously validated (cached), it returns immediately. + /// This method validates a block by forwarding it to the reth engine tree + /// (`newPayload`) and mapping the returned payload status to a boolean. + /// + /// It does not issue a forkchoice update, so it does not advance canonical head. /// /// # Arguments /// @@ -59,8 +63,8 @@ pub trait MorphL2EngineApi: Send + Sync { /// Import and finalize a new L2 block. /// - /// This method imports a validated block into the chain and updates the - /// canonical head. The block must have been previously validated. + /// This method imports a block through the reth engine pipeline using + /// `newPayload + forkchoiceUpdated`, which advances the canonical head on success. /// /// # Arguments /// @@ -86,4 +90,21 @@ pub trait MorphL2EngineApi: Send + Sync { /// /// Returns the header of the imported block. async fn new_safe_l2_block(&self, data: SafeL2Data) -> EngineApiResult; + + /// Set the safe and finalized block tags. + /// + /// This method updates the safe and finalized block pointers without + /// importing any new block. It aligns with go-ethereum's `engine_setBlockTags`. + /// + /// Either hash can be `B256::ZERO` to skip updating that tag. + /// + /// # Arguments + /// + /// * `safe_block_hash` - Hash of the block to mark as safe + /// * `finalized_block_hash` - Hash of the block to mark as finalized + async fn set_block_tags( + &self, + safe_block_hash: B256, + finalized_block_hash: B256, + ) -> EngineApiResult<()>; } diff --git a/crates/engine-api/src/builder.rs b/crates/engine-api/src/builder.rs index 80aa82b..8b85082 100644 --- a/crates/engine-api/src/builder.rs +++ b/crates/engine-api/src/builder.rs @@ -1,28 +1,30 @@ //! Morph L2 Engine API implementation. //! -//! This module provides both the builder and implementation for creating the L2 Engine API instance -//! that will be registered with the RPC server. +//! This module provides the concrete Morph L2 Engine API implementation and supporting helpers. -use crate::{EngineApiResult, MorphEngineApiError, MorphL2EngineApi, MorphValidationContext}; -use alloy_consensus::BlockHeader; -use alloy_primitives::{Address, B256, Sealable}; +use crate::{EngineApiResult, MorphEngineApiError, MorphL2EngineApi}; +use alloy_consensus::{ + BlockHeader, EMPTY_OMMER_ROOT_HASH, Header, constants::EMPTY_WITHDRAWALS, + proofs::calculate_transaction_root, +}; +use alloy_eips::eip2718::Decodable2718; +use alloy_hardforks::EthereumHardforks; +use alloy_primitives::{Address, B64, B256, Sealable}; use alloy_rpc_types_engine::PayloadAttributes; -use dashmap::DashMap; use morph_chainspec::MorphChainSpec; use morph_payload_types::{ AssembleL2BlockParams, ExecutableL2Data, GenericResponse, MorphBuiltPayload, - MorphPayloadBuilderAttributes, MorphPayloadTypes, SafeL2Data, + MorphExecutionData, MorphPayloadBuilderAttributes, MorphPayloadTypes, SafeL2Data, }; -use morph_primitives::{Block, MorphHeader, MorphPrimitives, MorphReceipt}; -use reth_execution_types::ExecutionOutcome; +use morph_primitives::{Block, BlockBody, MorphHeader, MorphPrimitives, MorphTxEnvelope}; +use parking_lot::RwLock; use reth_payload_builder::PayloadBuilderHandle; -use reth_payload_primitives::PayloadBuilderAttributes; +use reth_payload_primitives::{EngineApiMessageVersion, PayloadBuilderAttributes}; +#[cfg(test)] use reth_primitives_traits::RecoveredBlock; -use reth_provider::{ - BlockReader, BlockWriter, CanonChainTracker, DBProvider, DatabaseProviderFactory, - StateProviderFactory, -}; -use std::{sync::Arc, sync::RwLock}; +use reth_primitives_traits::{SealedBlock, SealedHeader}; +use reth_provider::{BlockIdReader, BlockNumReader, CanonChainTracker, HeaderProvider}; +use std::{sync::Arc, time::Instant}; // ============================================================================= // Real Implementation @@ -43,18 +45,12 @@ pub struct RealMorphL2EngineApi { /// Chain specification for hardfork rules. chain_spec: Arc, - /// Validation context for state root checks. - validation_ctx: MorphValidationContext, - - /// Cache for validated/executed blocks (keyed by block hash). - /// Used to avoid re-executing before import and to persist execution artifacts. - validation_cache: Arc>, + /// Handle to the running reth engine tree pipeline. + engine_handle: reth_node_api::ConsensusEngineHandle, - /// In-memory head tracking for local sequential imports. - /// - /// This bridges the gap before DB persistence is wired, so block `n+1` - /// can still be validated against an accepted in-process head `n`. - in_memory_head: Arc>>, + /// Engine-state tracker updated from consensus engine events (authoritative) and local FCU + /// success hints (fast path). + engine_state_tracker: Arc, } #[derive(Debug, Clone, Copy)] @@ -64,10 +60,40 @@ struct InMemoryHead { timestamp: u64, } -#[derive(Debug, Clone)] -struct CachedExecutionArtifacts { - executable_data: ExecutableL2Data, - executed: reth_payload_primitives::BuiltPayloadExecutedBlock, +/// Tracks engine-visible canonical head for the custom Morph engine API. +/// +/// Updated from `CanonicalChainCommitted` consensus engine events and optimistically +/// on successful local FCU calls to reduce latency before event delivery. +#[derive(Debug, Default)] +pub struct EngineStateTracker { + head: RwLock>, +} + +impl EngineStateTracker { + /// Records a canonical head hint from a locally successful FCU call. + pub fn record_local_head(&self, number: u64, hash: B256, timestamp: u64) { + *self.head.write() = Some(InMemoryHead { + number, + hash, + timestamp, + }); + } + + /// Updates the tracker from a consensus engine event stream item. + pub fn on_consensus_engine_event( + &self, + event: &reth_node_api::ConsensusEngineEvent, + ) { + use reth_node_api::ConsensusEngineEvent; + + if let ConsensusEngineEvent::CanonicalChainCommitted(header, _) = event { + self.record_local_head(header.number(), header.hash(), header.timestamp()); + } + } + + fn current_head(&self) -> Option { + *self.head.read() + } } impl RealMorphL2EngineApi { @@ -76,15 +102,15 @@ impl RealMorphL2EngineApi { provider: Provider, payload_builder: PayloadBuilderHandle, chain_spec: Arc, + engine_handle: reth_node_api::ConsensusEngineHandle, + engine_state_tracker: Arc, ) -> Self { - let validation_ctx = MorphValidationContext::new(chain_spec.clone()); Self { provider, payload_builder, chain_spec, - validation_ctx, - validation_cache: Arc::new(DashMap::new()), - in_memory_head: Arc::new(RwLock::new(None)), + engine_handle, + engine_state_tracker, } } @@ -102,39 +128,28 @@ impl RealMorphL2EngineApi { pub fn chain_spec(&self) -> &MorphChainSpec { &self.chain_spec } - - fn set_in_memory_head(&self, head: InMemoryHead) { - let mut guard = self - .in_memory_head - .write() - .expect("in-memory head lock poisoned"); - *guard = Some(head); - } } #[async_trait::async_trait] impl MorphL2EngineApi for RealMorphL2EngineApi where - Provider: BlockReader
+ Provider: HeaderProvider
+ + BlockIdReader + + BlockNumReader + CanonChainTracker
- + DatabaseProviderFactory - + StateProviderFactory + Clone + Send + Sync + 'static, - ::ProviderRW: - BlockWriter + DBProvider, { async fn assemble_l2_block( &self, params: AssembleL2BlockParams, ) -> EngineApiResult { let built_payload = self.build_l2_payload(params, None, None).await?; - let executable_data = built_payload.executable_data.clone(); - self.cache_built_payload(&built_payload); + let executable_data = built_payload.executable_data; - tracing::info!( + tracing::debug!( target: "morph::engine", block_hash = %executable_data.hash, gas_used = executable_data.gas_used, @@ -146,6 +161,7 @@ where } async fn validate_l2_block(&self, data: ExecutableL2Data) -> EngineApiResult { + let validate_started = Instant::now(); tracing::debug!( target: "morph::engine", block_number = data.number, @@ -153,17 +169,7 @@ where "validating L2 block" ); - // 1. Check if already validated (cached) - if self.validation_cache.contains_key(&data.hash) { - tracing::debug!( - target: "morph::engine", - block_hash = %data.hash, - "block already validated (cached)" - ); - return Ok(GenericResponse { success: true }); - } - - // 2. Enforce canonical continuity against the current head. + // 1. Enforce canonical continuity against the current head. let current_head = self.current_head()?; if data.number != current_head.number + 1 { tracing::warn!( @@ -185,114 +191,66 @@ where return Ok(GenericResponse { success: false }); } - // 3. Re-build the block with the same inputs and compare results. - let rebuild_params = AssembleL2BlockParams { - number: data.number, - transactions: data.transactions.clone(), - timestamp: Some(data.timestamp), + // 2. Convert and forward to reth engine tree (`newPayload` path). + let convert_started = Instant::now(); + let (payload, _) = match self.execution_payload_from_executable_data(&data) { + Ok(v) => v, + Err(err) => { + tracing::warn!( + target: "morph::engine", + block_hash = %data.hash, + error = %err, + "failed to convert executable data for validation" + ); + return Ok(GenericResponse { success: false }); + } }; + let convert_elapsed = convert_started.elapsed(); - let rebuilt_payload = match self - .build_l2_payload(rebuild_params, Some(data.gas_limit), data.base_fee_per_gas) - .await - { - Ok(payload) => payload, - Err(e) => { + let new_payload_started = Instant::now(); + let status = match self.engine_handle.new_payload(payload).await { + Ok(status) => status, + Err(err) => { tracing::warn!( target: "morph::engine", - error = %e, - "failed to rebuild block for validation" + block_hash = %data.hash, + error = %err, + "engine new_payload failed during validate_l2_block" ); return Ok(GenericResponse { success: false }); } }; - let rebuilt = rebuilt_payload.executable_data.clone(); - - // 4. Compare execution results - // Check state root if Jade is active - if self - .validation_ctx - .should_validate_state_root(data.timestamp) - && rebuilt.state_root != data.state_root - { - tracing::warn!( - target: "morph::engine", - expected = %data.state_root, - actual = %rebuilt.state_root, - "state root mismatch" - ); - return Ok(GenericResponse { success: false }); - } - - // Compare other critical fields. - if rebuilt.gas_used != data.gas_used { - tracing::warn!( - target: "morph::engine", - expected = data.gas_used, - actual = rebuilt.gas_used, - tx_count = data.transactions.len(), - "gas used mismatch" - ); - return Ok(GenericResponse { success: false }); - } - - if rebuilt.receipts_root != data.receipts_root { - tracing::warn!( - target: "morph::engine", - expected = %data.receipts_root, - actual = %rebuilt.receipts_root, - "receipts root mismatch" - ); - return Ok(GenericResponse { success: false }); - } - - if rebuilt.logs_bloom != data.logs_bloom { - tracing::warn!( - target: "morph::engine", - "logs bloom mismatch" - ); - return Ok(GenericResponse { success: false }); - } - - if rebuilt.withdraw_trie_root != data.withdraw_trie_root { - tracing::warn!( - target: "morph::engine", - expected = %data.withdraw_trie_root, - actual = %rebuilt.withdraw_trie_root, - "withdraw trie root mismatch" - ); - return Ok(GenericResponse { success: false }); - } - - // 5. Cache validation result with execution artifacts for new_l2_block. - if let Some(executed) = rebuilt_payload.executed().cloned() { - self.validation_cache.insert( - data.hash, - CachedExecutionArtifacts { - executable_data: data.clone(), - executed, - }, - ); - } else { - tracing::warn!( - target: "morph::engine", - block_hash = %data.hash, - "validated payload missing execution artifacts" - ); - return Ok(GenericResponse { success: false }); - } + let new_payload_elapsed = new_payload_started.elapsed(); tracing::debug!( target: "morph::engine", block_hash = %data.hash, - "block validation successful" + status = ?status.status, + "validate_l2_block returned engine payload status" + ); + + let success = matches!( + status.status, + alloy_rpc_types_engine::PayloadStatusEnum::Valid + | alloy_rpc_types_engine::PayloadStatusEnum::Accepted + ); + tracing::info!( + target: "morph::engine", + block_number = data.number, + block_hash = %data.hash, + convert_elapsed = ?convert_elapsed, + new_payload_elapsed = ?new_payload_elapsed, + total_elapsed = ?validate_started.elapsed(), + status = ?status.status, + success, + "validate_l2_block timing" ); - Ok(GenericResponse { success: true }) + Ok(GenericResponse { success }) } async fn new_l2_block(&self, data: ExecutableL2Data) -> EngineApiResult<()> { - tracing::info!( + tracing::debug!( target: "morph::engine", block_number = data.number, block_hash = %data.hash, @@ -344,89 +302,20 @@ where }); } - // 4. Validate block content before import. - let validation = self.validate_l2_block(data.clone()).await?; - if !validation.success { - return Err(MorphEngineApiError::ValidationFailed(format!( - "block content validation failed for {}", - data.hash - ))); - } - - // 5. Read and consume cached execution artifacts produced during assemble/validate. - let (_, cached) = self.validation_cache.remove(&data.hash).ok_or_else(|| { - MorphEngineApiError::Internal(format!( - "missing cached execution artifacts for block {}", - data.hash - )) - })?; - if cached.executable_data != data { - return Err(MorphEngineApiError::ValidationFailed(format!( - "cached execution data mismatch for block {}", - data.hash - ))); - } + let imported_header = self.import_l2_block_via_engine(data).await?; - let executed = cached.executed.into_executed_payload(); - let recovered_with_data = - apply_executable_data_overrides(executed.recovered_block().clone(), &data)?; - let computed_hash = recovered_with_data.hash(); - if computed_hash != data.hash { - return Err(MorphEngineApiError::ValidationFailed(format!( - "block hash mismatch: expected {}, computed {}", - data.hash, computed_hash - ))); - } - - let (block, senders) = recovered_with_data.split(); - // Keep external block hash as canonical identity for engine API compatibility. - let recovered_for_import = RecoveredBlock::new(block, senders, data.hash); - - let execution_outcome = ExecutionOutcome::from(( - executed.execution_outcome().clone(), - executed.block_number(), - )); - let hashed_state = Arc::unwrap_or_clone(executed.hashed_state()); - - let provider_rw = self - .provider - .database_provider_rw() - .map_err(|e| MorphEngineApiError::Database(e.to_string()))?; - provider_rw - .append_blocks_with_state( - vec![recovered_for_import.clone()], - &execution_outcome, - hashed_state, - ) - .map_err(|e| MorphEngineApiError::Database(e.to_string()))?; - provider_rw - .commit() - .map_err(|e| MorphEngineApiError::Database(e.to_string()))?; - - let sealed_block = recovered_for_import.sealed_block(); - - tracing::info!( + tracing::debug!( target: "morph::engine", - block_hash = %sealed_block.hash(), - block_number = sealed_block.number(), - "L2 block accepted" + block_hash = %imported_header.hash_slow(), + block_number = imported_header.number(), + "L2 block accepted via engine tree" ); - // Keep reth's canonical in-memory view in sync so eth_blockNumber advances. - self.provider - .set_canonical_head(sealed_block.clone_sealed_header()); - - self.set_in_memory_head(InMemoryHead { - number: sealed_block.number(), - hash: sealed_block.hash(), - timestamp: sealed_block.timestamp(), - }); - Ok(()) } async fn new_safe_l2_block(&self, data: SafeL2Data) -> EngineApiResult { - tracing::info!( + tracing::debug!( target: "morph::engine", block_number = data.number, "importing safe L2 block from L1 derivation" @@ -453,25 +342,27 @@ where .build_l2_payload(assemble_params, Some(data.gas_limit), data.base_fee_per_gas) .await?; let executable_data = built_payload.executable_data.clone(); - self.cache_built_payload(&built_payload); - - // 3. Import the block - self.new_l2_block(executable_data.clone()).await?; - // 4. Return the persisted header. + // 3. Import the block through reth engine tree and return the in-path header + // (do not rely on immediate DB visibility after FCU). let header = self - .provider - .sealed_header(data.number) - .map_err(|e| MorphEngineApiError::Database(e.to_string()))? - .ok_or_else(|| { - MorphEngineApiError::Internal(format!( - "imported safe block header {} not found", - data.number - )) - })? - .into_header(); + .import_l2_block_via_engine(executable_data.clone()) + .await?; - tracing::info!( + // Update safe block tag separately, matching geth's decoupled design. + // Best-effort: block import already succeeded, so don't fail the whole + // call if only the tag update encounters an issue. The tag can be + // corrected later via engine_setBlockTags. + if let Err(e) = self.set_block_tags(executable_data.hash, B256::ZERO).await { + tracing::warn!( + target: "morph::engine", + block_hash = %executable_data.hash, + error = %e, + "failed to update safe tag after block import; tag can be set later via setBlockTags" + ); + } + + tracing::debug!( target: "morph::engine", block_hash = %header.hash_slow(), "safe L2 block imported successfully" @@ -479,6 +370,52 @@ where Ok(header) } + + async fn set_block_tags( + &self, + safe_block_hash: B256, + finalized_block_hash: B256, + ) -> EngineApiResult<()> { + // Match geth's SetBlockTags: look up the header by hash and call set_finalized / + // set_safe on the provider directly, skipping zero hashes. This avoids a full + // FCU round-trip through the async engine pipeline for what is purely a tag + // update, and correctly skips the update when the caller passes B256::ZERO. + if finalized_block_hash != B256::ZERO { + let sealed = self + .provider + .sealed_header_by_hash(finalized_block_hash) + .map_err(|e| MorphEngineApiError::Internal(e.to_string()))? + .ok_or_else(|| { + MorphEngineApiError::Internal(format!( + "finalized block {finalized_block_hash} not found" + )) + })?; + self.provider.set_finalized(sealed); + tracing::info!( + target: "morph::engine", + %finalized_block_hash, + "finalized block tag updated" + ); + } + + if safe_block_hash != B256::ZERO { + let sealed = self + .provider + .sealed_header_by_hash(safe_block_hash) + .map_err(|e| MorphEngineApiError::Internal(e.to_string()))? + .ok_or_else(|| { + MorphEngineApiError::Internal(format!("safe block {safe_block_hash} not found")) + })?; + self.provider.set_safe(sealed); + tracing::info!( + target: "morph::engine", + %safe_block_hash, + "safe block tag updated" + ); + } + + Ok(()) + } } impl RealMorphL2EngineApi { @@ -489,7 +426,8 @@ impl RealMorphL2EngineApi { base_fee_override: Option, ) -> EngineApiResult where - Provider: BlockReader
+ Clone + Send + Sync + 'static, + Provider: + HeaderProvider
+ BlockNumReader + Clone + Send + Sync + 'static, { tracing::debug!( target: "morph::engine", @@ -509,18 +447,15 @@ impl RealMorphL2EngineApi { // 2. Build payload attributes. let parent_hash = current_head.hash; - let timestamp = match params.timestamp { - Some(t) => t, - None => { - let now = std::time::SystemTime::now() + let timestamp = params.timestamp.unwrap_or_else(|| { + std::cmp::max( + current_head.timestamp + 1, + std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| { - MorphEngineApiError::BlockBuildError(format!("system time error: {e}")) - })? - .as_secs(); - std::cmp::max(current_head.timestamp + 1, now) - } - }; + .unwrap_or_default() + .as_secs(), + ) + }); let base_fee_override = base_fee_override .map(|fee| { u64::try_from(fee).map_err(|_| { @@ -567,7 +502,7 @@ impl RealMorphL2EngineApi { })?; self.payload_builder - .resolve_kind(payload_id, reth_payload_builder::PayloadKind::Earliest) + .best_payload(payload_id) .await .ok_or_else(|| { MorphEngineApiError::Internal(format!("no payload response for id {payload_id:?}")) @@ -577,33 +512,204 @@ impl RealMorphL2EngineApi { }) } - fn cache_built_payload(&self, payload: &MorphBuiltPayload) { - let Some(executed) = payload.executed().cloned() else { - tracing::warn!( - target: "morph::engine", - block_hash = %payload.executable_data.hash, - "built payload missing execution artifacts" - ); - return; + async fn import_l2_block_via_engine( + &self, + data: ExecutableL2Data, + ) -> EngineApiResult + where + Provider: HeaderProvider
+ + BlockIdReader + + BlockNumReader + + CanonChainTracker
, + { + let import_started = Instant::now(); + let convert_started = Instant::now(); + let (payload, header) = self.execution_payload_from_executable_data(&data)?; + let convert_elapsed = convert_started.elapsed(); + + let new_payload_started = Instant::now(); + let payload_status = self + .engine_handle + .new_payload(payload) + .await + .map_err(|e| MorphEngineApiError::ExecutionFailed(e.to_string()))?; + let new_payload_elapsed = new_payload_started.elapsed(); + self.ensure_payload_status_acceptable(&payload_status, "newPayload")?; + + // FCU only advances canonical head. Safe/finalized tags are managed + // separately via set_block_tags, matching geth's engine_setBlockTags design. + let forkchoice = alloy_rpc_types_engine::ForkchoiceState { + head_block_hash: data.hash, + safe_block_hash: B256::ZERO, + finalized_block_hash: B256::ZERO, }; - self.validation_cache.insert( - payload.executable_data.hash, - CachedExecutionArtifacts { - executable_data: payload.executable_data.clone(), - executed, - }, + + self.provider.on_forkchoice_update_received(&forkchoice); + + let fcu_started = Instant::now(); + let fcu_result = self + .engine_handle + .fork_choice_updated(forkchoice, None, Self::engine_api_version()) + .await + .map_err(|e| MorphEngineApiError::ExecutionFailed(e.to_string()))?; + let fcu_elapsed = fcu_started.elapsed(); + self.ensure_payload_status_acceptable(&fcu_result.payload_status, "forkchoiceUpdated")?; + + // Synchronously update the canonical head so that eth_blockNumber immediately + // reflects the new block. The background write pipeline updates + // canonical_in_memory_state asynchronously; without this call, morph-node + // would see eth_blockNumber return the old block number and reject the next + // block as ErrWrongBlockNumber. + self.provider + .set_canonical_head(SealedHeader::new(header.clone(), data.hash)); + + self.engine_state_tracker + .record_local_head(header.number(), data.hash, header.timestamp()); + + tracing::info!( + target: "morph::engine", + block_number = data.number, + block_hash = %data.hash, + convert_elapsed = ?convert_elapsed, + new_payload_elapsed = ?new_payload_elapsed, + fcu_elapsed = ?fcu_elapsed, + total_elapsed = ?import_started.elapsed(), + new_payload_status = ?payload_status.status, + fcu_status = ?fcu_result.payload_status.status, + "new_l2_block engine timing" ); + + Ok(header) + } + + fn execution_payload_from_executable_data( + &self, + data: &ExecutableL2Data, + ) -> EngineApiResult<(MorphExecutionData, MorphHeader)> { + let base_fee_per_gas = data + .base_fee_per_gas + .map(|fee| { + u64::try_from(fee).map_err(|_| { + MorphEngineApiError::ValidationFailed(format!( + "base_fee_per_gas exceeds u64 in block {}", + data.hash + )) + }) + }) + .transpose()?; + if data.logs_bloom.len() != 256 { + return Err(MorphEngineApiError::ValidationFailed(format!( + "logs_bloom must be 256 bytes, got {} bytes in block {}", + data.logs_bloom.len(), + data.hash + ))); + } + + let mut txs = Vec::with_capacity(data.transactions.len()); + for (index, tx_bytes) in data.transactions.iter().enumerate() { + let mut buf = tx_bytes.as_ref(); + let tx = MorphTxEnvelope::decode_2718(&mut buf).map_err(|e| { + MorphEngineApiError::InvalidTransaction { + index, + message: e.to_string(), + } + })?; + if !buf.is_empty() { + return Err(MorphEngineApiError::InvalidTransaction { + index, + message: "trailing bytes after tx RLP decoding".to_string(), + }); + } + txs.push(tx); + } + + let logs_bloom = alloy_primitives::Bloom::from_slice(data.logs_bloom.as_ref()); + let shanghai_active = self + .chain_spec + .is_shanghai_active_at_timestamp(data.timestamp); + let cancun_active = self + .chain_spec + .is_cancun_active_at_timestamp(data.timestamp); + let header = MorphHeader { + next_l1_msg_index: data.next_l1_message_index, + inner: Header { + parent_hash: data.parent_hash, + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: data.miner, + state_root: data.state_root, + transactions_root: calculate_transaction_root(&txs), + receipts_root: data.receipts_root, + withdrawals_root: shanghai_active.then_some(EMPTY_WITHDRAWALS), + logs_bloom, + difficulty: Default::default(), + number: data.number, + gas_limit: data.gas_limit, + gas_used: data.gas_used, + timestamp: data.timestamp, + mix_hash: B256::ZERO, + nonce: B64::ZERO, + base_fee_per_gas, + extra_data: Default::default(), + parent_beacon_block_root: None, + blob_gas_used: cancun_active.then_some(0), + excess_blob_gas: cancun_active.then_some(0), + requests_hash: None, + }, + }; + let body = BlockBody { + transactions: txs, + ommers: Default::default(), + withdrawals: None, + }; + let sealed_block = SealedBlock::seal_slow(Block::new(header.clone(), body)); + + if sealed_block.hash() != data.hash { + return Err(MorphEngineApiError::ValidationFailed(format!( + "block hash mismatch: expected {}, computed {}", + data.hash, + sealed_block.hash() + ))); + } + + Ok(( + MorphExecutionData::with_expected_withdraw_trie_root( + Arc::new(sealed_block), + data.withdraw_trie_root, + ), + header, + )) + } + + fn ensure_payload_status_acceptable( + &self, + status: &alloy_rpc_types_engine::PayloadStatus, + context: &'static str, + ) -> EngineApiResult<()> { + match &status.status { + alloy_rpc_types_engine::PayloadStatusEnum::Valid + | alloy_rpc_types_engine::PayloadStatusEnum::Accepted => Ok(()), + alloy_rpc_types_engine::PayloadStatusEnum::Syncing => { + Err(MorphEngineApiError::ExecutionFailed(format!( + "{context} returned SYNCING for payload" + ))) + } + alloy_rpc_types_engine::PayloadStatusEnum::Invalid { validation_error } => { + Err(MorphEngineApiError::ValidationFailed(format!( + "{context} returned INVALID: {validation_error}" + ))) + } + } + } + + const fn engine_api_version() -> EngineApiMessageVersion { + EngineApiMessageVersion::V1 } fn current_head(&self) -> EngineApiResult where - Provider: BlockReader, + Provider: HeaderProvider + BlockNumReader, { - if let Some(head) = *self - .in_memory_head - .read() - .expect("in-memory head lock poisoned") - { + if let Some(head) = self.engine_state_tracker.current_head() { return Ok(head); } @@ -617,14 +723,18 @@ impl RealMorphL2EngineApi { .map_err(|e| MorphEngineApiError::Database(e.to_string()))? .ok_or_else(|| MorphEngineApiError::Internal(format!("header {number} not found")))?; - Ok(InMemoryHead { + let head = InMemoryHead { number, hash: header.hash(), timestamp: header.timestamp(), - }) + }; + self.engine_state_tracker + .record_local_head(head.number, head.hash, head.timestamp); + Ok(head) } } +#[cfg(test)] fn apply_executable_data_overrides( recovered_block: RecoveredBlock, data: &ExecutableL2Data, @@ -668,97 +778,46 @@ fn apply_executable_data_overrides( Ok(RecoveredBlock::new_unhashed(block, senders)) } -// ============================================================================= -// Stub Implementation (for testing/fallback) -// ============================================================================= - -/// Stub implementation of MorphL2EngineApi. -/// -/// This is a temporary placeholder that returns errors for all methods. -/// It allows the node to start and register the RPC methods, but the methods -/// will return errors when called. -#[derive(Debug, Clone)] -pub struct StubMorphL2EngineApi; - -#[async_trait::async_trait] -impl MorphL2EngineApi for StubMorphL2EngineApi { - async fn assemble_l2_block( - &self, - _params: morph_payload_types::AssembleL2BlockParams, - ) -> crate::EngineApiResult { - tracing::warn!(target: "morph::engine", "assemble_l2_block called on stub implementation"); - Err(crate::MorphEngineApiError::Internal( - "L2 Engine API not yet implemented".to_string(), - )) - } - - async fn validate_l2_block( - &self, - _data: morph_payload_types::ExecutableL2Data, - ) -> crate::EngineApiResult { - tracing::warn!(target: "morph::engine", "validate_l2_block called on stub implementation"); - Err(crate::MorphEngineApiError::Internal( - "L2 Engine API not yet implemented".to_string(), - )) - } - - async fn new_l2_block( - &self, - _data: morph_payload_types::ExecutableL2Data, - ) -> crate::EngineApiResult<()> { - tracing::warn!(target: "morph::engine", "new_l2_block called on stub implementation"); - Err(crate::MorphEngineApiError::Internal( - "L2 Engine API not yet implemented".to_string(), - )) - } - - async fn new_safe_l2_block( - &self, - _data: morph_payload_types::SafeL2Data, - ) -> crate::EngineApiResult { - tracing::warn!(target: "morph::engine", "new_safe_l2_block called on stub implementation"); - Err(crate::MorphEngineApiError::Internal( - "L2 Engine API not yet implemented".to_string(), - )) - } -} - -// ============================================================================= -// Legacy Builder (kept for reference, can be removed) -// ============================================================================= - -use crate::MorphL2EngineRpcHandler; -use reth_node_api::{AddOnsContext, FullNodeComponents}; -use reth_node_builder::rpc::EngineApiBuilder; - -/// Builder for the Morph L2 Engine API. -/// -/// Note: This builder is now superseded by the direct registration in MorphAddOns. -/// It's kept for compatibility but not used in practice. -#[derive(Debug, Default, Clone)] -pub struct MorphL2EngineApiBuilder; - -impl EngineApiBuilder for MorphL2EngineApiBuilder { - type EngineApi = MorphL2EngineRpcHandler; - - async fn build_engine_api(self, _ctx: &AddOnsContext<'_, N>) -> eyre::Result { - // Use stub implementation - real implementation is registered in MorphAddOns - Ok(MorphL2EngineRpcHandler::new(StubMorphL2EngineApi)) - } -} - #[cfg(test)] mod tests { use super::*; use alloy_consensus::Header; use alloy_primitives::{Address, Bloom, Bytes}; use morph_primitives::BlockBody; + use reth_node_api::ConsensusEngineEvent; + use reth_primitives_traits::SealedHeader; + use std::time::Duration; fn recovered_with_header(header: MorphHeader) -> RecoveredBlock { let block = Block::new(header, BlockBody::default()); RecoveredBlock::new_unhashed(block, Vec::new()) } + #[test] + fn test_engine_state_tracker_updates_head_on_canonical_chain_commit() { + let tracker = EngineStateTracker::default(); + assert!(tracker.current_head().is_none()); + + let header = MorphHeader { + inner: Header { + number: 42, + timestamp: 1_700_000_042, + ..Default::default() + }, + ..Default::default() + }; + let sealed_header = SealedHeader::seal_slow(header); + tracker.on_consensus_engine_event(&ConsensusEngineEvent::CanonicalChainCommitted( + Box::new(sealed_header.clone()), + Duration::ZERO, + )); + + let current_head = tracker.current_head().expect("head should be updated"); + assert_eq!(current_head.number, sealed_header.number()); + assert_eq!(current_head.hash, sealed_header.hash()); + assert_eq!(current_head.timestamp, sealed_header.timestamp()); + } + #[test] fn test_apply_executable_data_overrides_aligns_hash_with_engine_data() { let source_header: MorphHeader = Header::default().into(); @@ -889,7 +948,7 @@ mod tests { assert_eq!(header.inner.receipts_root, data.receipts_root); assert_eq!( header.inner.base_fee_per_gas, - data.base_fee_per_gas.map(|v| u64::try_from(v).unwrap()) + data.base_fee_per_gas.map(|v| v as u64) ); assert_eq!(header.inner.logs_bloom.as_slice(), data.logs_bloom.as_ref()); assert_eq!(header.next_l1_msg_index, data.next_l1_message_index); diff --git a/crates/engine-api/src/error.rs b/crates/engine-api/src/error.rs index 7d8b77a..c86124b 100644 --- a/crates/engine-api/src/error.rs +++ b/crates/engine-api/src/error.rs @@ -46,15 +46,6 @@ pub enum MorphEngineApiError { #[error("block execution failed: {0}")] ExecutionFailed(String), - /// Withdraw trie root mismatch. - #[error("withdraw trie root mismatch: expected {expected}, got {actual}")] - WithdrawTrieRootMismatch { - /// Expected withdraw trie root. - expected: B256, - /// Actual withdraw trie root. - actual: B256, - }, - /// Database error. #[error("database error: {0}")] Database(String), @@ -75,7 +66,6 @@ impl MorphEngineApiError { Self::BlockBuildError(_) => -32003, Self::ValidationFailed(_) => -32004, Self::ExecutionFailed(_) => -32005, - Self::WithdrawTrieRootMismatch { .. } => -32006, Self::Database(_) => -32010, Self::Internal(_) => -32099, }; diff --git a/crates/engine-api/src/lib.rs b/crates/engine-api/src/lib.rs index 07bbfeb..65d229b 100644 --- a/crates/engine-api/src/lib.rs +++ b/crates/engine-api/src/lib.rs @@ -27,7 +27,7 @@ mod rpc; mod validator; pub use api::MorphL2EngineApi; -pub use builder::{MorphL2EngineApiBuilder, RealMorphL2EngineApi, StubMorphL2EngineApi}; +pub use builder::{EngineStateTracker, RealMorphL2EngineApi}; pub use error::{EngineApiResult, MorphEngineApiError}; pub use rpc::{MorphL2EngineRpcHandler, MorphL2EngineRpcServer, into_rpc_result}; pub use validator::{MorphValidationContext, should_validate_state_root}; diff --git a/crates/engine-api/src/rpc.rs b/crates/engine-api/src/rpc.rs index 7ac35a6..3282e17 100644 --- a/crates/engine-api/src/rpc.rs +++ b/crates/engine-api/src/rpc.rs @@ -4,6 +4,7 @@ //! allowing the sequencer to interact with the execution layer via RPC. use crate::{EngineApiResult, api::MorphL2EngineApi}; +use alloy_primitives::B256; use jsonrpsee::{RpcModule, core::RpcResult, proc_macros::rpc}; use morph_payload_types::{AssembleL2BlockParams, ExecutableL2Data, GenericResponse, SafeL2Data}; use morph_primitives::MorphHeader; @@ -48,6 +49,18 @@ pub trait MorphL2EngineRpc { /// `engine_newSafeL2Block` #[method(name = "newSafeL2Block")] async fn new_safe_l2_block(&self, data: SafeL2Data) -> RpcResult; + + /// Set the safe and finalized block tags. + /// + /// # JSON-RPC Method + /// + /// `engine_setBlockTags` + #[method(name = "setBlockTags")] + async fn set_block_tags( + &self, + safe_block_hash: B256, + finalized_block_hash: B256, + ) -> RpcResult<()>; } /// Implementation of the L2 Engine RPC API. @@ -105,11 +118,11 @@ where } async fn new_l2_block(&self, data: ExecutableL2Data) -> RpcResult<()> { - tracing::info!( + tracing::debug!( target: "morph::engine", block_number = data.number, block_hash = %data.hash, - "importing new L2 block" + "RPC newL2Block called" ); self.inner.new_l2_block(data).await.map_err(|e| { @@ -119,10 +132,10 @@ where } async fn new_safe_l2_block(&self, data: SafeL2Data) -> RpcResult { - tracing::info!( + tracing::debug!( target: "morph::engine", block_number = data.number, - "importing safe L2 block" + "RPC newSafeL2Block called" ); self.inner.new_safe_l2_block(data).await.map_err(|e| { @@ -130,6 +143,27 @@ where e.into() }) } + + async fn set_block_tags( + &self, + safe_block_hash: B256, + finalized_block_hash: B256, + ) -> RpcResult<()> { + tracing::debug!( + target: "morph::engine", + %safe_block_hash, + %finalized_block_hash, + "RPC setBlockTags called" + ); + + self.inner + .set_block_tags(safe_block_hash, finalized_block_hash) + .await + .map_err(|e| { + tracing::error!(target: "morph::engine", error = %e, "failed to set block tags"); + e.into() + }) + } } /// Converts an `EngineApiResult` into a `RpcResult`. diff --git a/crates/evm/src/assemble.rs b/crates/evm/src/assemble.rs index 8510601..94665e7 100644 --- a/crates/evm/src/assemble.rs +++ b/crates/evm/src/assemble.rs @@ -108,7 +108,7 @@ impl BlockAssembler for MorphBlockAssembler { gas_limit: evm_env.block_env.gas_limit(), difficulty: evm_env.block_env.difficulty(), gas_used: *gas_used, - extra_data: Default::default(), + extra_data: execution_ctx.extra_data, parent_beacon_block_root: None, blob_gas_used: None, excess_blob_gas: None, diff --git a/crates/evm/src/config.rs b/crates/evm/src/config.rs index 23ab238..c47f441 100644 --- a/crates/evm/src/config.rs +++ b/crates/evm/src/config.rs @@ -41,7 +41,10 @@ impl ConfigureEvm for MorphEvmConfig { // Morph allows transactions with gas limit > 16777216 (EIP-7825 cap) cfg_env.tx_gas_limit_cap = Some(header.gas_limit()); - let fee_recipient = header.beneficiary(); + let fee_recipient = self + .chain_spec() + .fee_vault_address() + .unwrap_or_else(|| header.beneficiary()); // Morph doesn't support EIP-4844 blob transactions, but when SpecId >= CANCUN, // revm requires `blob_excess_gas_and_price` to be set. We provide a placeholder @@ -87,7 +90,10 @@ impl ConfigureEvm for MorphEvmConfig { // Morph allows transactions with gas limit > 16777216 (EIP-7825 cap) cfg_env.tx_gas_limit_cap = Some(attributes.gas_limit); - let fee_recipient = attributes.suggested_fee_recipient; + let fee_recipient = self + .chain_spec() + .fee_vault_address() + .unwrap_or(attributes.suggested_fee_recipient); // Morph doesn't support EIP-4844 blob transactions, but when SpecId >= CANCUN, // revm requires `blob_excess_gas_and_price` to be set. We provide a placeholder diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index e67fd23..84988a8 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -23,8 +23,10 @@ morph-txpool.workspace = true # Reth dependencies reth-db.workspace = true +reth-chainspec.workspace = true reth-engine-local.workspace = true reth-engine-tree.workspace = true +reth-errors.workspace = true reth-node-api.workspace = true reth-node-builder.workspace = true reth-node-ethereum.workspace = true @@ -36,6 +38,7 @@ reth-rpc-builder.workspace = true reth-rpc-eth-api.workspace = true reth-transaction-pool.workspace = true reth-tracing.workspace = true +reth-trie.workspace = true # Alloy alloy-consensus.workspace = true @@ -47,10 +50,17 @@ alloy-rpc-types-eth.workspace = true # Async eyre.workspace = true clap.workspace = true +dashmap.workspace = true +parking_lot.workspace = true +tokio-stream.workspace = true + +# HTTP (for geth diskRoot RPC cross-validation) +reqwest = { version = "0.12", default-features = false, features = ["json", "blocking", "rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true [dev-dependencies] tokio.workspace = true -reth-chainspec.workspace = true reth-db = { workspace = true, features = ["test-utils"] } reth-node-core.workspace = true reth-tasks.workspace = true diff --git a/crates/node/src/add_ons.rs b/crates/node/src/add_ons.rs index 7dbe801..9536ed9 100644 --- a/crates/node/src/add_ons.rs +++ b/crates/node/src/add_ons.rs @@ -1,6 +1,9 @@ //! Morph node RPC add-ons. -use crate::{MorphNode, validator::MorphEngineValidatorBuilder}; +use crate::{ + MorphNode, + validator::{MorphEngineValidatorBuilder, MorphTreeEngineValidatorBuilder}, +}; use morph_evm::MorphEvmConfig; use morph_primitives::{Block, MorphHeader, MorphReceipt}; use morph_rpc::MorphEthApiBuilder; @@ -8,8 +11,8 @@ use reth_node_api::{AddOnsContext, FullNodeComponents, FullNodeTypes, NodeAddOns use reth_node_builder::{ NodeAdapter, rpc::{ - BasicEngineValidatorBuilder, EngineValidatorAddOn, EngineValidatorBuilder, EthApiBuilder, - NoopEngineApiBuilder, PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, + EngineValidatorAddOn, EngineValidatorBuilder, EthApiBuilder, NoopEngineApiBuilder, + PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, }, }; use reth_provider::{ @@ -18,6 +21,7 @@ use reth_provider::{ use reth_rpc_builder::Identity; use reth_rpc_eth_api::RpcNodeCore; use reth_tracing::tracing; +use tokio_stream::StreamExt; /// Morph node add-ons for RPC and Engine API. /// @@ -30,7 +34,7 @@ pub struct MorphAddOns< N: FullNodeComponents, EthB: EthApiBuilder = MorphEthApiBuilder, PVB = MorphEngineValidatorBuilder, - EVB = BasicEngineValidatorBuilder, + EVB = MorphTreeEngineValidatorBuilder, RpcMiddleware = Identity, > { /// Inner RPC add-ons from reth. @@ -46,12 +50,18 @@ where { /// Creates a new [`MorphAddOns`] with default configuration. pub fn new() -> Self { + Self::with_geth_rpc_url(None) + } + + /// Creates a new [`MorphAddOns`] with an optional geth RPC URL for state root validation. + pub fn with_geth_rpc_url(geth_rpc_url: Option) -> Self { + let pvb = MorphEngineValidatorBuilder::default().with_geth_rpc_url(geth_rpc_url); Self { inner: RpcAddOns::new( MorphEthApiBuilder::default(), - MorphEngineValidatorBuilder, + pvb.clone(), NoopEngineApiBuilder::default(), - BasicEngineValidatorBuilder::default(), + MorphTreeEngineValidatorBuilder::new(pvb), Identity::default(), ), } @@ -92,6 +102,20 @@ where let provider = ctx.node.provider().clone(); let payload_builder = ctx.node.payload_builder_handle().clone(); let chain_spec = ctx.node.provider().chain_spec(); + let beacon_engine_handle = ctx.beacon_engine_handle.clone(); + let engine_events = ctx.engine_events.clone(); + let task_executor = ctx.node.task_executor().clone(); + let engine_state_tracker = + std::sync::Arc::new(morph_engine_api::EngineStateTracker::default()); + + // Keep a local view of canonical head/forkchoice from reth engine events. + let tracker_for_events = engine_state_tracker.clone(); + task_executor.spawn_critical("morph engine state tracker", async move { + let mut listener = engine_events.new_listener(); + while let Some(event) = listener.next().await { + tracker_for_events.on_consensus_engine_event(&event); + } + }); // Use launch_add_ons_with to register custom Engine API self.inner @@ -105,7 +129,13 @@ where // Create the Engine API implementation let engine_api = - morph_engine_api::RealMorphL2EngineApi::new(provider, payload_builder, chain_spec); + morph_engine_api::RealMorphL2EngineApi::new( + provider, + payload_builder, + chain_spec, + beacon_engine_handle, + engine_state_tracker, + ); // Create the RPC handler let handler = morph_engine_api::MorphL2EngineRpcHandler::new(engine_api); diff --git a/crates/node/src/args.rs b/crates/node/src/args.rs index f19e07a..5fe2394 100644 --- a/crates/node/src/args.rs +++ b/crates/node/src/args.rs @@ -33,6 +33,15 @@ pub struct MorphArgs { /// Morph Holesky testnet uses 1000 as the default limit. #[arg(long = "morph.max-tx-per-block", value_name = "COUNT")] pub max_tx_per_block: Option, + + /// Geth RPC URL for cross-validating MPT state root via `morph_diskRoot`. + /// + /// Before MPTFork, reth cannot validate ZK-trie state roots. When this URL + /// is set, reth calls the geth node's `morph_diskRoot` RPC to obtain the + /// MPT state root for each block and compares it with reth's computed root. + /// This catches state divergences that gas_used/receipts_root checks may miss. + #[arg(long = "morph.geth-rpc-url", value_name = "URL")] + pub geth_rpc_url: Option, } impl Default for MorphArgs { @@ -40,6 +49,7 @@ impl Default for MorphArgs { Self { max_tx_payload_bytes: MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES, max_tx_per_block: None, + geth_rpc_url: None, } } } diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 6c16ab8..13eb385 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -32,7 +32,9 @@ pub use components::{ MorphConsensusBuilder, MorphExecutorBuilder, MorphPayloadBuilderBuilder, MorphPoolBuilder, }; pub use node::{MorphNode, MorphPayloadAttributesBuilder}; -pub use validator::{MorphEngineValidator, MorphEngineValidatorBuilder}; +pub use validator::{ + MorphEngineValidator, MorphEngineValidatorBuilder, MorphTreeEngineValidatorBuilder, +}; // Re-export morph-rpc for convenience pub use morph_rpc as rpc; diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index ce382fb..3951114 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -126,7 +126,7 @@ where } fn add_ons(&self) -> Self::AddOns { - MorphAddOns::new() + MorphAddOns::with_geth_rpc_url(self.args.geth_rpc_url.clone()) } } diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index 7c38ef8..a29dde0 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -2,31 +2,133 @@ use crate::MorphNode; use alloy_consensus::BlockHeader; +use alloy_primitives::{B256, keccak256}; +use dashmap::DashMap; +use morph_chainspec::{ + L2_MESSAGE_QUEUE_ADDRESS, L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT, MorphChainSpec, + MorphHardforks, +}; use morph_payload_types::{MorphExecutionData, MorphPayloadTypes}; use morph_primitives::MorphHeader; +use parking_lot::Mutex; +use reth_chainspec::EthChainSpec; +use reth_errors::ConsensusError; use reth_node_api::{ - AddOnsContext, FullNodeComponents, InvalidPayloadAttributesError, NewPayloadError, - PayloadAttributes, PayloadValidator, + AddOnsContext, BlockTy, FullNodeComponents, InvalidPayloadAttributesError, NewPayloadError, + NodeTypes, PayloadAttributes, PayloadTypes, PayloadValidator, StateRootDecisionInput, + StateRootValidator, +}; +use reth_node_builder::{ + invalid_block_hook::InvalidBlockHookExt, + rpc::{BasicEngineValidator, ChangesetCache, EngineValidatorBuilder, PayloadValidatorBuilder}, }; -use reth_node_builder::rpc::PayloadValidatorBuilder; -use reth_primitives_traits::SealedBlock; -use std::sync::Arc; +use reth_primitives_traits::{GotExpected, RecoveredBlock, SealedBlock}; +use reth_provider::ChainSpecProvider; +use reth_tracing::tracing; +use std::{collections::VecDeque, sync::Arc}; /// Builder for Morph engine validator (payload validation). /// /// Creates a validator for validating engine API payloads. -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone)] #[non_exhaustive] -pub struct MorphEngineValidatorBuilder; +pub struct MorphEngineValidatorBuilder { + /// Optional geth RPC URL for cross-validating MPT state root via `morph_diskRoot`. + pub geth_rpc_url: Option, +} + +impl MorphEngineValidatorBuilder { + /// Sets the geth RPC URL for state root cross-validation. + pub fn with_geth_rpc_url(mut self, url: Option) -> Self { + self.geth_rpc_url = url; + self + } +} impl PayloadValidatorBuilder for MorphEngineValidatorBuilder where Node: FullNodeComponents, + Node::Provider: ChainSpecProvider, { type Validator = MorphEngineValidator; - async fn build(self, _ctx: &AddOnsContext<'_, Node>) -> eyre::Result { - Ok(MorphEngineValidator::new()) + async fn build(self, ctx: &AddOnsContext<'_, Node>) -> eyre::Result { + let mut validator = MorphEngineValidator::new(ctx.node.provider().chain_spec()); + if let Some(url) = self.geth_rpc_url { + validator = validator.with_geth_rpc_url(url); + } + Ok(validator) + } +} + +/// Builder for Morph tree engine validator. +/// +/// This wires [`MorphEngineValidator`] into both payload validation and state-root +/// decision/validation hooks. +#[derive(Debug, Clone)] +pub struct MorphTreeEngineValidatorBuilder { + payload_validator_builder: PVB, +} + +impl MorphTreeEngineValidatorBuilder { + /// Creates a new instance with the given payload validator builder. + pub const fn new(payload_validator_builder: PVB) -> Self { + Self { + payload_validator_builder, + } + } +} + +impl Default for MorphTreeEngineValidatorBuilder +where + PVB: Default, +{ + fn default() -> Self { + Self::new(PVB::default()) + } +} + +impl EngineValidatorBuilder for MorphTreeEngineValidatorBuilder +where + Node: FullNodeComponents< + Evm: reth_node_api::ConfigureEngineEvm< + <::Payload as PayloadTypes>::ExecutionData, + >, + >, + PVB: PayloadValidatorBuilder, + PVB::Validator: reth_node_api::PayloadValidator< + ::Payload, + Block = BlockTy, + > + StateRootValidator<::Primitives> + + Clone, +{ + type EngineValidator = + BasicEngineValidator; + + async fn build_tree_validator( + self, + ctx: &AddOnsContext<'_, Node>, + tree_config: reth_node_api::TreeConfig, + changeset_cache: ChangesetCache, + ) -> eyre::Result { + let validator = self.payload_validator_builder.build(ctx).await?; + let data_dir = ctx + .config + .datadir + .clone() + .resolve_datadir(ctx.config.chain.chain()); + let invalid_block_hook = ctx.create_invalid_block_hook(&data_dir).await?; + + Ok(BasicEngineValidator::new( + ctx.node.provider().clone(), + Arc::new(ctx.node.consensus().clone()), + ctx.node.evm_config().clone(), + validator.clone(), + tree_config, + invalid_block_hook, + changeset_cache, + ) + .with_state_root_validator(validator)) } } @@ -34,14 +136,94 @@ where /// /// This validator is used by the engine API to validate incoming payloads. /// For Morph, most validation is deferred to the consensus layer. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] #[non_exhaustive] -pub struct MorphEngineValidator; +pub struct MorphEngineValidator { + chain_spec: Arc, + expected_withdraw_trie_roots: Arc>, + expected_withdraw_trie_root_order: Arc>>, + /// Optional geth RPC URL for cross-validating MPT state root via `morph_diskRoot`. + geth_rpc_url: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WithdrawTrieRootExpectation { + SkipValidation, + Verify(B256), +} impl MorphEngineValidator { + const MAX_EXPECTED_WITHDRAW_TRIE_ROOTS: usize = 4096; + /// Creates a new [`MorphEngineValidator`]. - pub const fn new() -> Self { - Self + pub fn new(chain_spec: Arc) -> Self { + Self { + chain_spec, + expected_withdraw_trie_roots: Arc::new(DashMap::new()), + expected_withdraw_trie_root_order: Arc::new(Mutex::new(VecDeque::new())), + geth_rpc_url: None, + } + } + + /// Sets the geth RPC URL for cross-validating MPT state root. + pub fn with_geth_rpc_url(mut self, url: String) -> Self { + tracing::info!(target: "morph::validator", %url, "Enabled state root cross-validation via geth diskRoot RPC"); + self.geth_rpc_url = Some(url); + self + } + + fn record_withdraw_trie_root_expectation( + &self, + block_hash: B256, + expectation: WithdrawTrieRootExpectation, + ) { + let is_new_entry = self + .expected_withdraw_trie_roots + .insert(block_hash, expectation) + .is_none(); + + if is_new_entry { + let mut order = self.expected_withdraw_trie_root_order.lock(); + order.push_back(block_hash); + + while self.expected_withdraw_trie_roots.len() > Self::MAX_EXPECTED_WITHDRAW_TRIE_ROOTS { + let Some(evicted_hash) = order.pop_front() else { + break; + }; + self.expected_withdraw_trie_roots.remove(&evicted_hash); + } + } + } + + fn take_withdraw_trie_root_expectation( + &self, + block_hash: B256, + ) -> Option { + let removed = self + .expected_withdraw_trie_roots + .remove(&block_hash) + .map(|(_, expected)| expected); + + if removed.is_some() { + self.expected_withdraw_trie_root_order + .lock() + .retain(|hash| *hash != block_hash); + } + + removed + } + + fn updated_withdraw_trie_root_from_hashed_state( + state_updates: &reth_trie::HashedPostState, + ) -> Option { + let hashed_address = keccak256(L2_MESSAGE_QUEUE_ADDRESS); + let hashed_slot = keccak256(B256::from(L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT)); + + state_updates + .storages + .get(&hashed_address) + .and_then(|storage| storage.storage.get(&hashed_slot).copied()) + .map(B256::from) } } @@ -52,7 +234,51 @@ impl PayloadValidator for MorphEngineValidator { &self, payload: MorphExecutionData, ) -> Result, NewPayloadError> { - Ok(Arc::unwrap_or_clone(payload.block)) + let expected_withdraw_trie_root = payload.expected_withdraw_trie_root; + let sealed_block = Arc::unwrap_or_clone(payload.block); + + let expectation = expected_withdraw_trie_root + .map(WithdrawTrieRootExpectation::Verify) + .unwrap_or(WithdrawTrieRootExpectation::SkipValidation); + self.record_withdraw_trie_root_expectation(sealed_block.hash(), expectation); + + Ok(sealed_block) + } + + fn validate_block_post_execution_with_hashed_state( + &self, + state_updates: &reth_trie::HashedPostState, + block: &RecoveredBlock, + ) -> Result<(), ConsensusError> { + let Some(expectation) = self.take_withdraw_trie_root_expectation(block.hash()) else { + return Err(ConsensusError::Other(format!( + "missing withdraw trie root expectation cache entry for block {}", + block.hash() + ))); + }; + let WithdrawTrieRootExpectation::Verify(expected_withdraw_trie_root) = expectation else { + return Ok(()); + }; + + // Only validate if the withdraw trie root slot was actually updated in this block. + // If the slot is absent from hashed_state, the root is unchanged from the parent — + // the consensus layer guarantees the expected value is correct in that case. + // Doing a DB read for the parent state here would be expensive (history_by_block_hash + // + storage lookup) and would occur while holding the execution cache write lock, + // causing lock contention with the next block's cache lookup. + let Some(actual_withdraw_trie_root) = + Self::updated_withdraw_trie_root_from_hashed_state(state_updates) + else { + return Ok(()); + }; + + if actual_withdraw_trie_root != expected_withdraw_trie_root { + return Err(ConsensusError::Other(format!( + "withdraw trie root mismatch: expected {expected_withdraw_trie_root}, got {actual_withdraw_trie_root}" + ))); + } + + Ok(()) } fn validate_payload_attributes_against_header( @@ -67,3 +293,235 @@ impl PayloadValidator for MorphEngineValidator { Ok(()) } } + +impl StateRootValidator for MorphEngineValidator { + fn should_compute_state_root(&self, input: &StateRootDecisionInput) -> bool { + // Long-term behavior: always compute after Jade. + // Temporary behavior: if geth RPC is configured, also compute before Jade + // so we can cross-check against geth's `morph_diskRoot`. + self.chain_spec.is_jade_active_at_timestamp(input.timestamp) || self.geth_rpc_url.is_some() + } + + fn validate_state_root( + &self, + block: &RecoveredBlock, + computed_state_root: B256, + ) -> Result<(), ConsensusError> { + let block_number = block.header().number(); + let jade_active = self + .chain_spec + .is_jade_active_at_timestamp(block.header().timestamp()); + + // Always enforce canonical state-root equality in MPT mode. + if jade_active { + let expected_state_root = block.header().state_root(); + if computed_state_root != expected_state_root { + return Err(ConsensusError::BodyStateRootDiff( + GotExpected { + got: computed_state_root, + expected: expected_state_root, + } + .into(), + )); + } + } + + // Temporary cross-validation path: compare with geth's diskRoot when configured. + let Some(geth_url) = self.geth_rpc_url.as_deref() else { + return Ok(()); + }; + + match fetch_geth_disk_root(geth_url, block_number) { + Ok(disk_root) => { + if computed_state_root == disk_root { + tracing::debug!( + target: "morph::validator", + block_number, + ?computed_state_root, + "State root cross-validation passed" + ); + Ok(()) + } else { + tracing::error!( + target: "morph::validator", + block_number, + ?computed_state_root, + ?disk_root, + "State root cross-validation FAILED" + ); + Err(ConsensusError::BodyStateRootDiff( + GotExpected { + got: computed_state_root, + expected: disk_root, + } + .into(), + )) + } + } + Err(err) => { + tracing::warn!( + target: "morph::validator", + block_number, + %err, + "Failed to fetch diskRoot from geth, skipping state root validation" + ); + Ok(()) + } + } + } +} + +/// Response from geth's `morph_diskRoot` RPC method. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct DiskAndHeaderRoot { + disk_root: B256, +} + +/// JSON-RPC response wrapper. +#[derive(Debug, serde::Deserialize)] +struct JsonRpcResponse { + result: Option, + error: Option, +} + +/// JSON-RPC error object. +#[derive(Debug, serde::Deserialize)] +struct JsonRpcError { + code: i64, + message: String, +} + +impl std::fmt::Display for JsonRpcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "JSON-RPC error {}: {}", self.code, self.message) + } +} + +/// Fetches the MPT state root from a geth node via `morph_diskRoot` RPC. +/// +/// This calls geth's `morph_diskRoot` method with the given block number to obtain +/// the MPT-format state root (`diskRoot`) for cross-validation against reth's +/// computed root. +fn fetch_geth_disk_root(geth_url: &str, block_number: u64) -> Result { + let block_hex = format!("0x{block_number:x}"); + let body = serde_json::json!({ + "jsonrpc": "2.0", + "method": "morph_diskRoot", + "params": [{"blockNumber": block_hex}], + "id": 1 + }); + + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .map_err(|e| format!("failed to create HTTP client: {e}"))?; + + let resp = client + .post(geth_url) + .json(&body) + .send() + .map_err(|e| format!("HTTP request failed: {e}"))?; + + if !resp.status().is_success() { + return Err(format!("HTTP {} from geth", resp.status())); + } + + let rpc_resp: JsonRpcResponse = resp + .json() + .map_err(|e| format!("failed to parse response: {e}"))?; + + if let Some(err) = rpc_resp.error { + return Err(err.to_string()); + } + + rpc_resp + .result + .map(|r| r.disk_root) + .ok_or_else(|| "morph_diskRoot returned null result".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::U256; + use morph_chainspec::MORPH_HOODI; + use reth_trie::{HashedPostState, HashedStorage}; + use std::sync::Arc; + + fn test_chain_spec() -> Arc { + MORPH_HOODI.clone() + } + + #[test] + fn test_extract_updated_withdraw_trie_root_from_hashed_state() { + let expected = B256::from([0x11; 32]); + let hashed_address = keccak256(L2_MESSAGE_QUEUE_ADDRESS); + let hashed_slot = keccak256(B256::from(L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT)); + + let state = HashedPostState::from_hashed_storage( + hashed_address, + HashedStorage::from_iter(false, [(hashed_slot, U256::from_be_bytes(expected.0))]), + ); + + assert_eq!( + MorphEngineValidator::updated_withdraw_trie_root_from_hashed_state(&state), + Some(expected) + ); + } + + #[test] + fn test_extract_updated_withdraw_trie_root_from_hashed_state_missing_slot() { + let state = HashedPostState::default(); + assert_eq!( + MorphEngineValidator::updated_withdraw_trie_root_from_hashed_state(&state), + None + ); + } + + #[test] + fn test_withdraw_trie_root_expectation_cache_evicts_incrementally_not_clear_all() { + let validator = MorphEngineValidator::new(test_chain_spec()); + let key = |n: usize| { + let mut bytes = [0u8; 32]; + bytes[..8].copy_from_slice(&(n as u64).to_be_bytes()); + B256::from(bytes) + }; + + for i in 0..MorphEngineValidator::MAX_EXPECTED_WITHDRAW_TRIE_ROOTS { + validator.record_withdraw_trie_root_expectation( + key(i), + WithdrawTrieRootExpectation::Verify(B256::from([0xaa; 32])), + ); + } + assert_eq!( + validator.expected_withdraw_trie_roots.len(), + MorphEngineValidator::MAX_EXPECTED_WITHDRAW_TRIE_ROOTS + ); + + let oldest = key(0); + let newest = key(MorphEngineValidator::MAX_EXPECTED_WITHDRAW_TRIE_ROOTS); + validator.record_withdraw_trie_root_expectation( + newest, + WithdrawTrieRootExpectation::Verify(B256::from([0xbb; 32])), + ); + + assert_eq!( + validator.expected_withdraw_trie_roots.len(), + MorphEngineValidator::MAX_EXPECTED_WITHDRAW_TRIE_ROOTS + ); + assert!(!validator.expected_withdraw_trie_roots.is_empty()); + assert!( + validator + .expected_withdraw_trie_roots + .get(&newest) + .is_some() + ); + assert!( + validator + .expected_withdraw_trie_roots + .get(&oldest) + .is_none() + ); + } +} diff --git a/crates/payload/builder/src/builder.rs b/crates/payload/builder/src/builder.rs index 4d0e2a2..c84a212 100644 --- a/crates/payload/builder/src/builder.rs +++ b/crates/payload/builder/src/builder.rs @@ -3,9 +3,10 @@ use crate::{MorphBuilderConfig, MorphPayloadBuilderError, config::PayloadBuildingBreaker}; use alloy_consensus::{BlockHeader, Transaction, Typed2718}; use alloy_eips::eip2718::Encodable2718; -use alloy_primitives::{Address, B256, Bytes, U256, address}; +use alloy_primitives::{B256, Bytes, U256}; use alloy_rlp::Encodable; use morph_chainspec::MorphChainSpec; +use morph_chainspec::{L2_MESSAGE_QUEUE_ADDRESS, L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT}; use morph_evm::{MorphEvmConfig, MorphNextBlockEnvAttributes}; use morph_payload_types::{ExecutableL2Data, MorphBuiltPayload, MorphPayloadBuilderAttributes}; use morph_primitives::{MorphHeader, MorphTxEnvelope}; @@ -32,19 +33,6 @@ use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction, Transac use revm::context_interface::Block as RevmBlock; use std::sync::Arc; -// ============================================================================= -// L2 Message Queue Constants -// ============================================================================= - -/// L2 Message Queue contract address. -/// -/// Manages the L1-to-L2 message queue and stores the withdraw trie root. -const L2_MESSAGE_QUEUE_ADDRESS: Address = address!("5300000000000000000000000000000000000001"); - -/// Storage slot for the withdraw trie root (`messageRoot`) in L2MessageQueue contract. -/// This is slot 33, which stores the Merkle root for L2→L1 messages. -const L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT: U256 = U256::from_limbs([33, 0, 0, 0]); - /// Reads the withdraw trie root from the L2MessageQueue contract storage. fn read_withdraw_trie_root(db: &mut DB) -> Result { let value = db.storage( diff --git a/crates/payload/types/src/lib.rs b/crates/payload/types/src/lib.rs index 12d9825..018a67b 100644 --- a/crates/payload/types/src/lib.rs +++ b/crates/payload/types/src/lib.rs @@ -55,12 +55,29 @@ pub struct MorphPayloadTypes; pub struct MorphExecutionData { /// The built block. pub block: Arc>, + /// Optional expected withdraw trie root supplied by custom engine APIs. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub expected_withdraw_trie_root: Option, } impl MorphExecutionData { /// Creates a new `MorphExecutionData` from a sealed block. pub fn new(block: Arc>) -> Self { - Self { block } + Self { + block, + expected_withdraw_trie_root: None, + } + } + + /// Creates a new `MorphExecutionData` with an expected withdraw trie root. + pub fn with_expected_withdraw_trie_root( + block: Arc>, + expected_withdraw_trie_root: B256, + ) -> Self { + Self { + block, + expected_withdraw_trie_root: Some(expected_withdraw_trie_root), + } } } diff --git a/crates/revm/src/precompiles.rs b/crates/revm/src/precompiles.rs index 074f07e..650fac9 100644 --- a/crates/revm/src/precompiles.rs +++ b/crates/revm/src/precompiles.rs @@ -7,16 +7,31 @@ //! //! ```text //! Berlin (base) -//! └── Bernoulli/Curie = Berlin - ripemd160 - blake2f -//! └── Morph203/Viridian = Bernoulli + blake2f + ripemd160 +//! └── Bernoulli/Curie = Berlin with ripemd160/blake2f replaced by disabled stubs +//! └── Morph203/Viridian = Bernoulli with ripemd160/blake2f re-enabled (working) //! └── Emerald = Morph203 + Osaka precompiles //! ``` //! -//! | Hardfork | Base | Added | Disabled | -//! |------------------|-----------|------------------------------------------------|-------------------| -//! | Bernoulli/Curie | Berlin | - | ripemd160/blake2f | -//! | Morph203/Viridian| Bernoulli | blake2f, ripemd160 | - | -//! | Emerald | Morph203 | Osaka (P256verify, BLS12-381, point eval, etc) | - | +//! | Hardfork | Base | Added | Notes | +//! |------------------|-----------|----------------------------------------------------------|-------------------------------| +//! | Bernoulli/Curie | Berlin | - | ripemd160/blake2f as disabled stubs | +//! | Morph203/Viridian| Bernoulli | blake2f, ripemd160 (working) | replaces disabled stubs | +//! | Emerald | Morph203 | Osaka (P256verify, BLS12-381, point eval, etc) | - | +//! +//! ## Why Disabled Stubs? +//! +//! go-ethereum's `PrecompiledContractsBernoulli` includes 0x03 (ripemd160) and 0x09 (blake2f) +//! as disabled stubs (`ripemd160hashDisabled`, `blake2FDisabled`). This has two effects: +//! +//! 1. Both addresses are included in `PrecompiledAddressesBernoulli`, so they get **warmed** +//! via `StateDB.Prepare` (EIP-2929). CALL costs 100 gas (warm) instead of 2600 (cold). +//! +//! 2. When called, go-eth's CALL handler sets `gas = 0` for any non-revert error, consuming +//! all forwarded gas. revm's `PrecompileError` result also causes all forwarded gas to +//! be consumed (parent does not reclaim gas when sub-call is not ok-or-revert). +//! +//! Omitting these stubs causes morph-reth to treat 0x03/0x09 as cold empty accounts (2600 +//! base cost, forwarded gas returned), creating a gas mismatch vs go-ethereum. use alloy_primitives::Address; use morph_chainspec::hardfork::MorphHardfork; @@ -25,7 +40,7 @@ use revm::{ context_interface::ContextTr, handler::{EthPrecompiles, PrecompileProvider}, interpreter::{CallInputs, InterpreterResult}, - precompile::Precompiles, + precompile::{Precompile, PrecompileError, PrecompileId, PrecompileResult, Precompiles}, primitives::{OnceLock, hardfork::SpecId}, }; use std::boxed::Box; @@ -117,29 +132,52 @@ impl Default for MorphPrecompiles { } } +/// Disabled stub for ripemd160 (0x03) in Bernoulli/Curie hardfork. +/// +/// Returns `PrecompileError` to consume all forwarded gas, matching go-ethereum's behavior +/// where a disabled precompile error causes the CALL handler to burn all remaining gas. +fn ripemd160_disabled(_input: &[u8], _gas_limit: u64) -> PrecompileResult { + Err(PrecompileError::Other( + "ripemd160 precompile disabled in Bernoulli/Curie hardfork".into(), + )) +} + +/// Disabled stub for blake2f (0x09) in Bernoulli/Curie hardfork. +/// +/// Returns `PrecompileError` to consume all forwarded gas, matching go-ethereum's behavior +/// where a disabled precompile error causes the CALL handler to burn all remaining gas. +fn blake2f_disabled(_input: &[u8], _gas_limit: u64) -> PrecompileResult { + Err(PrecompileError::Other( + "blake2f precompile disabled in Bernoulli/Curie hardfork".into(), + )) +} + /// Returns precompiles for Bernoulli hardfork. /// -/// Based on Berlin but with ripemd160 and blake2f excluded. -/// Enabled: ecrecover, sha256, identity, modexp, bn256 ops -/// Disabled: ripemd160 (0x03), blake2f (0x09) +/// Based on Berlin with ripemd160 (0x03) and blake2f (0x09) replaced by disabled stubs. +/// All 9 Berlin addresses are present (so they get warmed via EIP-2929), but 0x03/0x09 +/// consume all forwarded gas and return failure when called. /// /// Matches: pub fn bernoulli() -> &'static Precompiles { static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| { - let berlin = Precompiles::berlin(); - - // Create a set with only ripemd160 and blake2f to exclude - let mut to_exclude = Precompiles::default(); - if let Some(ripemd) = berlin.get(&addresses::RIPEMD160) { - to_exclude.extend([ripemd.clone()]); - } - if let Some(blake2f) = berlin.get(&addresses::BLAKE2F) { - to_exclude.extend([blake2f.clone()]); - } + // Start from Berlin (9 precompiles including 0x03 and 0x09). + let mut precompiles = Precompiles::berlin().clone(); + + // Replace ripemd160 (0x03) and blake2f (0x09) with disabled stubs. + // This keeps them in warm_addresses() so EIP-2929 warms them (100 gas instead of + // 2600 cold), matching go-ethereum's PrecompiledContractsBernoulli behavior. + precompiles.extend([ + Precompile::new( + PrecompileId::Ripemd160, + addresses::RIPEMD160, + ripemd160_disabled, + ), + Precompile::new(PrecompileId::Blake2F, addresses::BLAKE2F, blake2f_disabled), + ]); - // Return berlin precompiles minus the excluded ones - berlin.difference(&to_exclude) + precompiles }) } @@ -247,10 +285,12 @@ mod tests { assert!(precompiles.contains(&addresses::MODEXP)); assert!(precompiles.contains(&addresses::BN256_ADD)); - // ripemd160 and blake2f should NOT be present (disabled in Bernoulli) - // Matches Go: PrecompiledContractsBernoulli - assert!(!precompiles.contains(&addresses::RIPEMD160)); - assert!(!precompiles.contains(&addresses::BLAKE2F)); + // ripemd160 (0x03) and blake2f (0x09) ARE present as disabled stubs. + // They must be in the precompile set so they get warmed (EIP-2929: 100 gas warm + // instead of 2600 cold), matching go-ethereum's PrecompiledContractsBernoulli + // which includes &ripemd160hashDisabled{} and &blake2FDisabled{}. + assert!(precompiles.contains(&addresses::RIPEMD160)); + assert!(precompiles.contains(&addresses::BLAKE2F)); } #[test] @@ -263,10 +303,10 @@ mod tests { // Both should have the same precompiles assert_eq!(bernoulli_p.precompiles().len(), curie_p.precompiles().len()); - // Both should have sha256 enabled, ripemd160/blake2f disabled + // Both should have sha256 enabled and 0x03/0x09 as disabled stubs (present in set) assert!(curie_p.contains(&addresses::SHA256)); - assert!(!curie_p.contains(&addresses::RIPEMD160)); - assert!(!curie_p.contains(&addresses::BLAKE2F)); + assert!(curie_p.contains(&addresses::RIPEMD160)); + assert!(curie_p.contains(&addresses::BLAKE2F)); } #[test] @@ -308,8 +348,9 @@ mod tests { let morph203_count = morph203().len(); let emerald_count = emerald().len(); - // Morph203/Viridian should have more than Bernoulli (adds blake2f + ripemd160) - assert!(morph203_count > bernoulli_count); + // Bernoulli and Morph203 have the same number of addresses (9), but + // Bernoulli has 0x03/0x09 as disabled stubs while Morph203 re-enables them. + assert_eq!(morph203_count, bernoulli_count); // Emerald should have more than Morph203 (adds Osaka precompiles) assert!(emerald_count > morph203_count); @@ -324,11 +365,12 @@ mod tests { let viridian_p = MorphPrecompiles::new_with_spec(MorphHardfork::Viridian); let emerald_p = MorphPrecompiles::new_with_spec(MorphHardfork::Emerald); - // Bernoulli and Curie: no ripemd160, no blake2f (same precompile set) - assert!(!bernoulli_p.contains(&addresses::RIPEMD160)); - assert!(!bernoulli_p.contains(&addresses::BLAKE2F)); - assert!(!curie_p.contains(&addresses::RIPEMD160)); - assert!(!curie_p.contains(&addresses::BLAKE2F)); + // Bernoulli and Curie: ripemd160 and blake2f are present as disabled stubs (same precompile set). + // They're in the set to ensure EIP-2929 warming matches go-ethereum. + assert!(bernoulli_p.contains(&addresses::RIPEMD160)); + assert!(bernoulli_p.contains(&addresses::BLAKE2F)); + assert!(curie_p.contains(&addresses::RIPEMD160)); + assert!(curie_p.contains(&addresses::BLAKE2F)); // Morph203 and Viridian: blake2f + ripemd160 enabled, no P256verify (same precompile set) assert!(morph203_p.contains(&addresses::RIPEMD160)); diff --git a/crates/txpool/src/maintain.rs b/crates/txpool/src/maintain.rs index 6f134bb..2c32851 100644 --- a/crates/txpool/src/maintain.rs +++ b/crates/txpool/src/maintain.rs @@ -46,52 +46,16 @@ struct SenderBudget { token_balances: HashMap, } -/// Applies cumulative sender-budget checks and consumes budget on success. +/// Applies cumulative sender-budget check for the ETH-fee path and consumes budget on success. /// -/// Returns `true` if the transaction can be afforded under the current rolling budget. -#[allow(clippy::too_many_arguments)] -fn consume_sender_budget( +/// Returns `true` if the transaction can be afforded under the current rolling ETH budget. +fn consume_eth_budget( budget: &mut SenderBudget, - uses_token_fee: bool, tx_value: U256, gas_limit: u64, max_fee_per_gas: u128, l1_data_fee: U256, - token_id: Option, - fee_limit: Option, - required_token_amount: U256, - state_token_balance: Option, ) -> bool { - if uses_token_fee { - let (token_id, fee_limit) = match (token_id, fee_limit) { - (Some(token_id), Some(fee_limit)) => (token_id, fee_limit), - _ => return false, - }; - - let token_budget = budget - .token_balances - .entry(token_id) - .or_insert(state_token_balance.unwrap_or(U256::ZERO)); - - // Match REVM semantics with rolling sender budget: - // - fee_limit == 0 => use remaining token budget - // - fee_limit > remaining => cap by remaining token budget - let effective_limit = if fee_limit.is_zero() || fee_limit > *token_budget { - *token_budget - } else { - fee_limit - }; - - if effective_limit < required_token_amount || tx_value > budget.eth_balance { - return false; - } - - *token_budget = (*token_budget).saturating_sub(required_token_amount); - budget.eth_balance = budget.eth_balance.saturating_sub(tx_value); - return true; - } - - // ETH-fee path: consume full tx ETH cost (value + gas fee + l1_data_fee). let gas_fee = U256::from(gas_limit).saturating_mul(U256::from(max_fee_per_gas)); let total_eth_cost = gas_fee.saturating_add(l1_data_fee).saturating_add(tx_value); if total_eth_cost > budget.eth_balance { @@ -101,6 +65,45 @@ fn consume_sender_budget( true } +/// Applies cumulative sender-budget check for the token-fee path and consumes budget on success. +/// +/// Returns `true` if the transaction can be afforded under the current rolling token/ETH budget. +fn consume_token_budget( + budget: &mut SenderBudget, + tx_value: U256, + token_id: Option, + fee_limit: Option, + required_token_amount: U256, + state_token_balance: Option, +) -> bool { + let (token_id, fee_limit) = match (token_id, fee_limit) { + (Some(token_id), Some(fee_limit)) => (token_id, fee_limit), + _ => return false, + }; + + let token_budget = budget + .token_balances + .entry(token_id) + .or_insert(state_token_balance.unwrap_or(U256::ZERO)); + + // Match REVM semantics with rolling sender budget: + // - fee_limit == 0 => use remaining token budget + // - fee_limit > remaining => cap by remaining token budget + let effective_limit = if fee_limit.is_zero() || fee_limit > *token_budget { + *token_budget + } else { + fee_limit + }; + + if effective_limit < required_token_amount || tx_value > budget.eth_balance { + return false; + } + + *token_budget = (*token_budget).saturating_sub(required_token_amount); + budget.eth_balance = budget.eth_balance.saturating_sub(tx_value); + true +} + /// Maintains the Morph transaction pool by revalidating MorphTx transactions. /// /// This task runs continuously and: @@ -263,18 +266,25 @@ where let token_id = fields.as_ref().map(|f| f.fee_token_id); let fee_limit = fields.as_ref().map(|f| f.fee_limit); - if !consume_sender_budget( - &mut budget, - validation.uses_token_fee, - consensus_tx.value(), - consensus_tx.gas_limit(), - consensus_tx.max_fee_per_gas(), - l1_data_fee, - token_id, - fee_limit, - validation.required_token_amount, - state_token_balance, - ) { + let affordable = if validation.uses_token_fee { + consume_token_budget( + &mut budget, + consensus_tx.value(), + token_id, + fee_limit, + validation.required_token_amount, + state_token_balance, + ) + } else { + consume_eth_budget( + &mut budget, + consensus_tx.value(), + consensus_tx.gas_limit(), + consensus_tx.max_fee_per_gas(), + l1_data_fee, + ) + }; + if !affordable { tracing::debug!( target: "morph_txpool::maintain", tx_hash = ?tx.hash(), @@ -316,34 +326,12 @@ mod tests { token_balances: HashMap::new(), }; - let first = consume_sender_budget( - &mut budget, - false, - U256::from(20u64), - 10, - 3, - U256::from(5u64), - None, - None, - U256::ZERO, - None, - ); + let first = consume_eth_budget(&mut budget, U256::from(20u64), 10, 3, U256::from(5u64)); assert!(first); // total cost = value(20) + gas(30) + l1(5) = 55 assert_eq!(budget.eth_balance, U256::from(45u64)); - let second = consume_sender_budget( - &mut budget, - false, - U256::from(20u64), - 10, - 3, - U256::from(5u64), - None, - None, - U256::ZERO, - None, - ); + let second = consume_eth_budget(&mut budget, U256::from(20u64), 10, 3, U256::from(5u64)); assert!(!second); assert_eq!(budget.eth_balance, U256::from(45u64)); } @@ -355,12 +343,8 @@ mod tests { token_balances: HashMap::new(), }; - let first = consume_sender_budget( + let first = consume_token_budget( &mut budget, - true, - U256::ZERO, - 1, - 1, U256::ZERO, Some(7), Some(U256::ZERO), // fee_limit=0 => use full remaining budget @@ -373,12 +357,8 @@ mod tests { Some(U256::from(40u64)) ); - let second = consume_sender_budget( + let second = consume_token_budget( &mut budget, - true, - U256::ZERO, - 1, - 1, U256::ZERO, Some(7), Some(U256::ZERO), @@ -400,12 +380,8 @@ mod tests { }; // fee_limit caps the payment below required amount => reject - let limited = consume_sender_budget( + let limited = consume_token_budget( &mut budget, - true, - U256::ZERO, - 1, - 1, U256::ZERO, Some(9), Some(U256::from(30u64)), @@ -415,13 +391,9 @@ mod tests { assert!(!limited); // Enough token, but ETH value exceeds remaining ETH budget => reject - let eth_value_fail = consume_sender_budget( + let eth_value_fail = consume_token_budget( &mut budget, - true, U256::from(6u64), - 1, - 1, - U256::ZERO, Some(9), Some(U256::from(100u64)), U256::from(10u64), @@ -438,13 +410,9 @@ mod tests { }; // Tx1: token-fee path, consumes token only for fee and ETH for value. - let tx1 = consume_sender_budget( + let tx1 = consume_token_budget( &mut budget, - true, U256::from(10u64), // value in ETH - 1, - 1, - U256::ZERO, Some(3), Some(U256::ZERO), // unlimited by tx field => bounded by remaining token budget U256::from(70u64), @@ -458,29 +426,20 @@ mod tests { ); // Tx2: ETH-fee path, consumes full ETH cost. - let tx2 = consume_sender_budget( + let tx2 = consume_eth_budget( &mut budget, - false, U256::from(20u64), // value 5, // gas_limit 4, // max_fee_per_gas => gas fee = 20 U256::from(10u64), // l1 fee - None, - None, - U256::ZERO, - None, ); assert!(tx2); // total eth cost = 20(value) + 20(gas) + 10(l1) = 50 assert_eq!(budget.eth_balance, U256::from(40u64)); // Tx3: token-fee path should now fail because remaining token budget is only 30. - let tx3 = consume_sender_budget( + let tx3 = consume_token_budget( &mut budget, - true, - U256::ZERO, - 1, - 1, U256::ZERO, Some(3), Some(U256::ZERO), diff --git a/local-test/.gitignore b/local-test/.gitignore new file mode 100644 index 0000000..e75bc96 --- /dev/null +++ b/local-test/.gitignore @@ -0,0 +1,18 @@ +# Data directories +reth-data/ +geth-data/ +node-data/ + +# Runtime files +jwt-secret.txt +*.pid +*.log + +# Log rotation directories (numeric names like 2818/) +[0-9]*/ + +# Downloaded artifacts +mainnet-data.zip +mainnet-data/ +.config-extract/ +config-prep.*/ diff --git a/local-test/common.sh b/local-test/common.sh new file mode 100755 index 0000000..2ab2f36 --- /dev/null +++ b/local-test/common.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Morphnode configuration (binary is in ../morph/node, data is in local-test) +: "${MORPHNODE_BIN:=../morph/node/build/bin/morphnode}" +: "${NODE_HOME:=./local-test/node-data}" +: "${JWT_SECRET:=./local-test/jwt-secret.txt}" +: "${NODE_LOG_FILE:=./local-test/node.log}" +: "${DOWNLOAD_CONFIG_IF_MISSING:=1}" +: "${MAINNET_CONFIG_ZIP_URL:=https://raw.githubusercontent.com/morph-l2/run-morph-node/main/mainnet/data.zip}" +: "${CONFIG_ZIP_PATH:=./local-test/mainnet-data.zip}" +: "${KEEP_CONFIG_ARTIFACTS:=0}" +: "${AUTO_RESET_ON_WRONG_BLOCK:=0}" + +# Morph Geth configuration +: "${GETH_BIN:=../morph/go-ethereum/build/bin/geth}" +: "${GETH_DATA_DIR:=./local-test/geth-data}" +: "${GETH_LOG_FILE:=./local-test/geth.log}" + +# Morph-Reth configuration +: "${RETH_BIN:=./target/release/morph-reth}" +: "${RETH_DATA_DIR:=./local-test/reth-data}" +: "${RETH_LOG_FILE:=./local-test/reth.log}" +: "${RETH_HTTP_ADDR:=0.0.0.0}" +: "${RETH_HTTP_PORT:=8545}" +: "${RETH_AUTHRPC_ADDR:=127.0.0.1}" +: "${RETH_AUTHRPC_PORT:=8551}" +: "${RETH_BOOTNODES:=}" +: "${MORPH_MAX_TX_PAYLOAD_BYTES:=122880}" +: "${MORPH_MAX_TX_PER_BLOCK:=}" +: "${MORPH_GETH_RPC_URL:=http://localhost:8546}" + +check_binary() { + local bin_path="$1" + local build_hint="$2" + if [[ ! -x "${bin_path}" ]]; then + echo "Missing executable: ${bin_path}" + echo "Build hint: ${build_hint}" + return 1 + fi +} + +cleanup_runtime_logs() { + rm -f "${NODE_LOG_FILE}" "${RETH_LOG_FILE}" + rm -rf "$(dirname "${RETH_LOG_FILE}")"/{[0-9]*,*.log*} +} + +# pm2 helper functions +pm2_check() { + if ! command -v pm2 &> /dev/null; then + echo "ERROR: pm2 is not installed" + echo "Install with: npm install -g pm2" + return 1 + fi +} + +pm2_is_running() { + local name="$1" + pm2 describe "${name}" &>/dev/null && \ + [[ "$(pm2 jlist 2>/dev/null | jq -r ".[] | select(.name==\"${name}\") | .pm2_env.status")" == "online" ]] +} + +pm2_stop() { + local name="$1" + if pm2 describe "${name}" &>/dev/null; then + pm2 stop "${name}" 2>/dev/null || true + pm2 delete "${name}" 2>/dev/null || true + echo "${name}: stopped" + else + echo "${name}: not running" + fi +} + +rel_path() { + local path="$1" + echo "${path#./}" +} diff --git a/local-test/geth-start.sh b/local-test/geth-start.sh new file mode 100755 index 0000000..5bb65f4 --- /dev/null +++ b/local-test/geth-start.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +echo "Starting morph-geth..." + +# Check prerequisites +pm2_check +check_binary "${GETH_BIN}" "cd ../morph/go-ethereum && make geth" + +# Check if already running +if pm2_is_running "morph-geth"; then + echo "morph-geth already running" + pm2 describe morph-geth + exit 0 +fi + +# Ensure data directory exists +mkdir -p "${GETH_DATA_DIR}" +mkdir -p "$(dirname "${GETH_LOG_FILE}")" + +# Start morph-geth with pm2 +pm2 start "${GETH_BIN}" --name morph-geth -- \ + --morph \ + --datadir "${GETH_DATA_DIR}" \ + --gcmode archive \ + --syncmode full \ + --http \ + --http.addr "${RETH_HTTP_ADDR}" \ + --http.port "${RETH_HTTP_PORT}" \ + --http.corsdomain "*" \ + --http.vhosts "*" \ + --http.api "web3,eth,debug,txpool,net,morph,engine" \ + --authrpc.addr "${RETH_AUTHRPC_ADDR}" \ + --authrpc.port "${RETH_AUTHRPC_PORT}" \ + --authrpc.vhosts "*" \ + --authrpc.jwtsecret "${JWT_SECRET}" \ + --nodiscover \ + --maxpeers 0 \ + --verbosity 3 \ + --log.filename "${GETH_LOG_FILE}" + +echo "Logs: pm2 logs morph-geth" diff --git a/local-test/geth-stop.sh b/local-test/geth-stop.sh new file mode 100755 index 0000000..72df6ab --- /dev/null +++ b/local-test/geth-stop.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +pm2_stop "morph-geth" diff --git a/local-test/node-start.sh b/local-test/node-start.sh new file mode 100755 index 0000000..d950781 --- /dev/null +++ b/local-test/node-start.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +echo "Starting morphnode..." + +# Check prerequisites +pm2_check +check_binary "${MORPHNODE_BIN}" "cd ../morph/node && make build" + +# Check if already running +if pm2_is_running "morph-node"; then + echo "morphnode already running" + pm2 describe morph-node + exit 0 +fi + +# Ensure log directory exists +mkdir -p "$(dirname "${NODE_LOG_FILE}")" + +# Start morphnode with pm2 +pm2 start "${MORPHNODE_BIN}" --name morph-node -- \ + --home "${NODE_HOME}" \ + --l2.jwt-secret "${JWT_SECRET}" \ + --l2.eth "http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" \ + --l2.engine "http://${RETH_AUTHRPC_ADDR}:${RETH_AUTHRPC_PORT}" \ + --log.filename "${NODE_LOG_FILE}" + +echo "Logs: pm2 logs morph-node" diff --git a/local-test/node-stop.sh b/local-test/node-stop.sh new file mode 100755 index 0000000..6b15ad4 --- /dev/null +++ b/local-test/node-stop.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +stop_by_pid_file "morphnode" "${NODE_PID_FILE}" diff --git a/local-test/prepare.sh b/local-test/prepare.sh new file mode 100755 index 0000000..5928d3d --- /dev/null +++ b/local-test/prepare.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +check_binary "${RETH_BIN}" "cargo build --release --bin morph-reth" +check_binary "${MORPHNODE_BIN}" "run: cd node && make build" + +mkdir -p "${RETH_DATA_DIR}" +mkdir -p "${NODE_HOME}" +mkdir -p "${NODE_HOME}/config" +mkdir -p "${NODE_HOME}/data" +mkdir -p "$(dirname "${RETH_LOG_FILE}")" +mkdir -p "$(dirname "${NODE_LOG_FILE}")" + +if [[ ! -f "${JWT_SECRET}" ]]; then + openssl rand -hex 32 > "${JWT_SECRET}" + chmod 600 "${JWT_SECRET}" +fi + +if [[ ! -f "${NODE_HOME}/config/config.toml" || ! -f "${NODE_HOME}/config/genesis.json" || ! -f "${NODE_HOME}/data/priv_validator_state.json" ]]; then + if [[ "${DOWNLOAD_CONFIG_IF_MISSING}" == "1" ]]; then + if ! command -v curl >/dev/null 2>&1; then + echo "curl is required to download Morph config bundle." + exit 1 + fi + if ! command -v unzip >/dev/null 2>&1; then + echo "unzip is required to extract Morph config bundle." + exit 1 + fi + + # Clean legacy extraction directories from previous runs. + rm -rf "./local-test/.config-extract" "./local-test/mainnet-data" + mkdir -p "$(dirname "${CONFIG_ZIP_PATH}")" + + temp_extract_dir="$(mktemp -d "${SCRIPT_DIR}/config-prep.XXXXXX")" + cleanup_temp() { + if [[ "${KEEP_CONFIG_ARTIFACTS}" != "1" ]]; then + rm -rf "${temp_extract_dir}" + rm -f "${CONFIG_ZIP_PATH}" + fi + } + trap cleanup_temp EXIT + + echo "Downloading mainnet config bundle..." + curl -fL "${MAINNET_CONFIG_ZIP_URL}" -o "${CONFIG_ZIP_PATH}" + unzip -oq "${CONFIG_ZIP_PATH}" -d "${temp_extract_dir}" + + bundle_root="" + if [[ -f "${temp_extract_dir}/data/node-data/config/config.toml" && -f "${temp_extract_dir}/data/node-data/config/genesis.json" ]]; then + bundle_root="${temp_extract_dir}/data" + elif [[ -f "${temp_extract_dir}/mainnet-data/node-data/config/config.toml" && -f "${temp_extract_dir}/mainnet-data/node-data/config/genesis.json" ]]; then + bundle_root="${temp_extract_dir}/mainnet-data" + elif [[ -f "${temp_extract_dir}/node-data/config/config.toml" && -f "${temp_extract_dir}/node-data/config/genesis.json" ]]; then + bundle_root="${temp_extract_dir}" + fi + + if [[ -z "${bundle_root}" ]]; then + echo "Downloaded zip does not contain expected node-data config files." + echo "Checked bundle roots: ${temp_extract_dir}/data, ${temp_extract_dir}/mainnet-data, ${temp_extract_dir}" + exit 1 + fi + + cp -f "${bundle_root}/node-data/config/config.toml" "${NODE_HOME}/config/config.toml" + cp -f "${bundle_root}/node-data/config/genesis.json" "${NODE_HOME}/config/genesis.json" + if [[ -f "${bundle_root}/node-data/config/addrbook.json" ]]; then + cp -f "${bundle_root}/node-data/config/addrbook.json" "${NODE_HOME}/config/addrbook.json" + fi + if [[ -f "${bundle_root}/node-data/config/node_key.json" && ! -f "${NODE_HOME}/config/node_key.json" ]]; then + cp -f "${bundle_root}/node-data/config/node_key.json" "${NODE_HOME}/config/node_key.json" + fi + if [[ -f "${bundle_root}/node-data/config/priv_validator_key.json" && ! -f "${NODE_HOME}/config/priv_validator_key.json" ]]; then + cp -f "${bundle_root}/node-data/config/priv_validator_key.json" "${NODE_HOME}/config/priv_validator_key.json" + fi + if [[ -f "${bundle_root}/node-data/data/priv_validator_state.json" ]]; then + cp -f "${bundle_root}/node-data/data/priv_validator_state.json" "${NODE_HOME}/data/priv_validator_state.json" + fi + echo "Config prepared at ${NODE_HOME} from ${MAINNET_CONFIG_ZIP_URL}" + else + echo "Warning: node-data is incomplete under ${NODE_HOME}." + echo "Set DOWNLOAD_CONFIG_IF_MISSING=1 or prepare config files manually." + fi +fi + +# Tendermint needs this state file. Some published bundles do not include it. +if [[ ! -f "${NODE_HOME}/data/priv_validator_state.json" ]]; then + cat > "${NODE_HOME}/data/priv_validator_state.json" <<'EOF' +{"height":"0","round":0,"step":0} +EOF +fi + +# If the previous run failed with replay "wrong block number", suggest or trigger a clean reset. +if [[ -f "${NODE_LOG_FILE}" ]] && grep -q "wrong block number" "${NODE_LOG_FILE}"; then + echo + echo "Detected historical replay failure in ${NODE_LOG_FILE}: wrong block number" + if [[ "${AUTO_RESET_ON_WRONG_BLOCK}" == "1" ]]; then + echo "AUTO_RESET_ON_WRONG_BLOCK=1, resetting local sync state..." + "${SCRIPT_DIR}/reset-sync-state.sh" --yes + else + echo "If replay fails again, run: ./local-test/reset-sync-state.sh --yes" + fi +fi + +echo "Preparation finished." +echo "RETH_DATA_DIR=$(rel_path "${RETH_DATA_DIR}")" +echo "NODE_HOME=$(rel_path "${NODE_HOME}")" +echo "JWT_SECRET=$(rel_path "${JWT_SECRET}")" diff --git a/local-test/reset.sh b/local-test/reset.sh new file mode 100755 index 0000000..2eaf2d7 --- /dev/null +++ b/local-test/reset.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +assume_yes=0 +if [[ "${1:-}" == "--yes" ]]; then + assume_yes=1 +fi + +echo "==========================================" +echo "Reset local sync state (morph-reth + node)" +echo "==========================================" +echo +echo "This will remove:" +echo " - ${RETH_DATA_DIR}/db" +echo " - ${RETH_DATA_DIR}/static_files" +echo " - ${GETH_DATA_DIR}/geth" +echo " - ${NODE_HOME}/data" +echo +echo "This keeps:" +echo " - ${NODE_HOME}/config (genesis/keys)" +echo " - ${GETH_DATA_DIR}/keystore" +echo " - log files" +echo + +if [[ ${assume_yes} -ne 1 ]]; then + read -r -p "Continue? [y/N] " confirm + if [[ "${confirm}" != "y" && "${confirm}" != "Y" ]]; then + echo "Cancelled." + exit 0 + fi +fi + +"${SCRIPT_DIR}/stop-all.sh" || true +pm2_stop "morph-geth" 2>/dev/null || true + +rm -rf "${RETH_DATA_DIR}/db" "${RETH_DATA_DIR}/static_files" "${GETH_DATA_DIR}/geth" "${NODE_HOME}/data" +mkdir -p "${RETH_DATA_DIR}" "${GETH_DATA_DIR}" "${NODE_HOME}/data" + +cat > "${NODE_HOME}/data/priv_validator_state.json" <<'EOF' +{"height":"0","round":0,"step":0} +EOF +cleanup_runtime_logs + +echo +echo "Reset finished." +echo "Next steps:" +echo " 1) $(rel_path "${SCRIPT_DIR}")/prepare.sh" +echo " 2) $(rel_path "${SCRIPT_DIR}")/start-all.sh" diff --git a/local-test/reth-start.sh b/local-test/reth-start.sh new file mode 100755 index 0000000..186543e --- /dev/null +++ b/local-test/reth-start.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +echo "Starting morph-reth..." + +# Check prerequisites +pm2_check +check_binary "${RETH_BIN}" "cargo build --release --bin morph-reth" + +# Check if already running +if pm2_is_running "morph-reth"; then + echo "morph-reth already running" + pm2 describe morph-reth + exit 0 +fi + +# Ensure data directory exists +mkdir -p "${RETH_DATA_DIR}" +mkdir -p "$(dirname "${RETH_LOG_FILE}")" + +# Build command arguments +args=( + node + --chain mainnet + --datadir "${RETH_DATA_DIR}" + --http + --http.addr "${RETH_HTTP_ADDR}" + --http.port "${RETH_HTTP_PORT}" + --http.api "web3,debug,eth,txpool,net,trace" + --authrpc.addr "${RETH_AUTHRPC_ADDR}" + --authrpc.port "${RETH_AUTHRPC_PORT}" + --authrpc.jwtsecret "${JWT_SECRET}" + --log.file.directory "$(dirname "${RETH_LOG_FILE}")" + --log.file.filter info + --morph.max-tx-payload-bytes "${MORPH_MAX_TX_PAYLOAD_BYTES}" + --nat none + --engine.legacy-state-root +) + +# Add optional max-tx-per-block if configured +if [[ -n "${MORPH_MAX_TX_PER_BLOCK}" ]]; then + args+=(--morph.max-tx-per-block "${MORPH_MAX_TX_PER_BLOCK}") +fi + +# Add optional geth RPC URL for state root cross-validation +if [[ -n "${MORPH_GETH_RPC_URL}" ]]; then + args+=(--morph.geth-rpc-url "${MORPH_GETH_RPC_URL}") +fi + +# Add bootnodes if configured +if [[ -n "${RETH_BOOTNODES}" ]]; then + args+=(--bootnodes "${RETH_BOOTNODES}") +fi + +# Start morph-reth with pm2 +pm2 start "${RETH_BIN}" --name morph-reth -- "${args[@]}" + +echo "Logs: pm2 logs morph-reth" diff --git a/local-test/reth-stop.sh b/local-test/reth-stop.sh new file mode 100755 index 0000000..c1e4563 --- /dev/null +++ b/local-test/reth-stop.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +stop_by_pid_file "morph-reth" "${RETH_PID_FILE}" diff --git a/local-test/start-all.sh b/local-test/start-all.sh new file mode 100755 index 0000000..b7e50d6 --- /dev/null +++ b/local-test/start-all.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +echo "==========================================" +echo "Starting Morph full node (pm2)" +echo "==========================================" + +# Step 1: Check pm2 +echo "[1/4] Checking pm2..." +pm2_check + +# Step 2: Prepare configuration +echo "[2/4] Preparing configuration..." +"${SCRIPT_DIR}/prepare.sh" + +# Step 3: Start morph-reth +echo "[3/4] Starting morph-reth..." +"${SCRIPT_DIR}/reth-start.sh" + +# Wait for RPC to be ready +echo "Waiting for RPC..." +max_retries=60 +retry_count=0 +while [[ ${retry_count} -lt ${max_retries} ]]; do + if curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \ + "http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" >/dev/null 2>&1; then + echo "RPC ready" + break + fi + + retry_count=$((retry_count + 1)) + if [[ $((retry_count % 10)) -eq 0 ]]; then + echo "Still waiting... (${retry_count}/${max_retries})" + fi + sleep 1 +done + +if [[ ${retry_count} -eq ${max_retries} ]]; then + echo "ERROR: RPC did not become ready after ${max_retries} seconds" + echo "Check logs: pm2 logs morph-reth" + exit 1 +fi + +# Step 4: Start morphnode +echo "[4/4] Starting morphnode..." +"${SCRIPT_DIR}/node-start.sh" + +echo +echo "Full node started" +echo "RPC: http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" +echo +echo "Useful commands:" +echo " pm2 list - view process status" +echo " pm2 logs - view all logs" +echo " pm2 logs morph-reth - view morph-reth logs" +echo " pm2 logs morph-node - view morphnode logs" +echo " pm2 monit - real-time monitoring" +echo " pm2 save - save process list for restart" +echo +echo "Check status: $(rel_path "${SCRIPT_DIR}")/status.sh" diff --git a/local-test/status.sh b/local-test/status.sh new file mode 100755 index 0000000..8b0e865 --- /dev/null +++ b/local-test/status.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +echo "==========================================" +echo "Morph Node Status (with morph-reth)" +echo "==========================================" +echo + +# Process status via pm2 +echo "--- Process Status (pm2) ---" +pm2 list --no-color 2>/dev/null | grep -E "morph-reth|morph-node|name" || echo "No pm2 processes found" +echo + +# morph-reth RPC status +echo "--- morph-reth RPC ---" + +# Chain ID +echo -n "Chain ID: " +chain_id=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \ + "http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" 2>/dev/null | jq -r '.result // "error"') +if [[ "${chain_id}" != "error" && "${chain_id}" != "null" ]]; then + # Convert hex to decimal + chain_id_dec=$((chain_id)) + echo "${chain_id} (${chain_id_dec})" +else + echo "unavailable" +fi + +# Block number +echo -n "Block Number: " +block_num=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + "http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" 2>/dev/null | jq -r '.result // "error"') +if [[ "${block_num}" != "error" && "${block_num}" != "null" ]]; then + block_num_dec=$((block_num)) + echo "${block_num} (${block_num_dec})" +else + echo "unavailable" +fi + +# Peer count +echo -n "Peer Count: " +peer_count=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"net_peerCount","params":[],"id":1}' \ + "http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" 2>/dev/null | jq -r '.result // "error"') +if [[ "${peer_count}" != "error" && "${peer_count}" != "null" ]]; then + peer_count_dec=$((peer_count)) + echo "${peer_count} (${peer_count_dec})" +else + echo "unavailable" +fi + +# Syncing status +echo -n "Syncing: " +syncing=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \ + "http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" 2>/dev/null | jq -r '.result // "error"') +if [[ "${syncing}" == "false" ]]; then + echo "false (synced)" +elif [[ "${syncing}" != "error" && "${syncing}" != "null" ]]; then + echo "true (in progress)" +else + echo "unavailable" +fi + +echo + +# morphnode status +echo "--- morphnode Status ---" +morphnode_status=$(curl -s "http://127.0.0.1:26657/status" 2>/dev/null) +if [[ -n "${morphnode_status}" ]]; then + echo -n "Latest Block Height: " + echo "${morphnode_status}" | jq -r '.result.sync_info.latest_block_height // "unknown"' + echo -n "Latest Block Time: " + echo "${morphnode_status}" | jq -r '.result.sync_info.latest_block_time // "unknown"' + echo -n "Catching Up: " + echo "${morphnode_status}" | jq -r '.result.sync_info.catching_up // "unknown"' +else + echo "morphnode RPC not available" +fi + +echo + +# morphnode net_info +echo "--- morphnode Network ---" +morphnode_netinfo=$(curl -s "http://127.0.0.1:26657/net_info" 2>/dev/null) +if [[ -n "${morphnode_netinfo}" ]]; then + echo -n "Peers: " + echo "${morphnode_netinfo}" | jq -r '.result.n_peers // "unknown"' +else + echo "morphnode RPC not available" +fi + +echo +echo "==========================================" +echo "Logs:" +echo " - pm2 logs morph-reth" +echo " - pm2 logs morph-node" +echo " - pm2 monit (real-time monitoring)" +echo "==========================================" diff --git a/local-test/stop-all.sh b/local-test/stop-all.sh new file mode 100755 index 0000000..33b768f --- /dev/null +++ b/local-test/stop-all.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +# Stop in reverse order: morphnode first, then morph-reth +pm2_stop "morph-node" +pm2_stop "morph-reth" + +echo "All services stopped" diff --git a/local-test/sync-test.sh b/local-test/sync-test.sh new file mode 100755 index 0000000..51640e1 --- /dev/null +++ b/local-test/sync-test.sh @@ -0,0 +1,335 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +# ─── Configuration ──────────────────────────────────────────────────────────── +: "${TEST_DURATION:=120}" # seconds to run each node +: "${RPC_WAIT_TIMEOUT:=60}" # seconds to wait for RPC readiness +: "${SAMPLE_INTERVAL:=10}" # seconds between BPS samples +: "${SKIP_GETH:=0}" # set to 1 to skip geth test +: "${SKIP_RETH:=0}" # set to 1 to skip reth test +: "${MAINNET_TIP:=21100000}" # approximate current mainnet tip for ETA calc + +# ─── Helpers ────────────────────────────────────────────────────────────────── + +get_block_number() { + local result + result=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + "http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" 2>/dev/null | jq -r '.result // ""') + if [[ -n "${result}" && "${result}" != "null" ]]; then + printf "%d" "${result}" + else + echo "0" + fi +} + +wait_for_rpc() { + local name="$1" + local retries=0 + echo -n " Waiting for ${name} RPC..." + while [[ ${retries} -lt ${RPC_WAIT_TIMEOUT} ]]; do + if curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \ + "http://${RETH_HTTP_ADDR}:${RETH_HTTP_PORT}" >/dev/null 2>&1; then + echo " ready" + return 0 + fi + retries=$((retries + 1)) + sleep 1 + done + echo " TIMEOUT" + return 1 +} + +# Collect BPS samples over the test duration by polling eth_blockNumber. +# Outputs: start_block end_block elapsed_seconds avg_bps peak_bps +run_bps_sampling() { + local name="$1" + local duration="$2" + local interval="${SAMPLE_INTERVAL}" + + local start_block end_block prev_block + local elapsed=0 sample_count=0 + local total_bps=0 peak_bps=0 + + start_block=$(get_block_number) + prev_block=${start_block} + + echo " Sampling BPS for ${name} (${duration}s, every ${interval}s)..." + + while [[ ${elapsed} -lt ${duration} ]]; do + sleep "${interval}" + elapsed=$((elapsed + interval)) + + local current_block + current_block=$(get_block_number) + local delta=$((current_block - prev_block)) + local bps + bps=$(echo "scale=2; ${delta} / ${interval}" | bc) + + sample_count=$((sample_count + 1)) + total_bps=$(echo "${total_bps} + ${bps}" | bc) + + # Track peak + if [[ $(echo "${bps} > ${peak_bps}" | bc -l) -eq 1 ]]; then + peak_bps=${bps} + fi + + printf " [%3ds] block=%d delta=+%d bps=%.2f\n" "${elapsed}" "${current_block}" "${delta}" "${bps}" + prev_block=${current_block} + done + + end_block=$(get_block_number) + local total_blocks=$((end_block - start_block)) + local avg_bps + avg_bps=$(echo "scale=2; ${total_blocks} / ${duration}" | bc) + + echo " ${name} sampling complete: ${start_block} -> ${end_block} (+${total_blocks} blocks)" + + # Export results via global vars (bash doesn't have return values for multiple) + RESULT_START_BLOCK=${start_block} + RESULT_END_BLOCK=${end_block} + RESULT_TOTAL_BLOCKS=${total_blocks} + RESULT_AVG_BPS=${avg_bps} + RESULT_PEAK_BPS=${peak_bps} +} + +# Full reset: stop everything, clean EL + node data, re-prepare config. +full_reset() { + echo " Resetting all data..." + pm2_stop "morph-geth" 2>/dev/null || true + pm2_stop "morph-reth" 2>/dev/null || true + pm2_stop "morph-node" 2>/dev/null || true + + rm -rf "${RETH_DATA_DIR}/db" "${RETH_DATA_DIR}/static_files" + rm -rf "${GETH_DATA_DIR}/geth" + rm -rf "${NODE_HOME}/data" + mkdir -p "${RETH_DATA_DIR}" "${GETH_DATA_DIR}" "${NODE_HOME}/data" + + cat > "${NODE_HOME}/data/priv_validator_state.json" <<'EOF' +{"height":"0","round":0,"step":0} +EOF + + cleanup_runtime_logs + echo " Reset complete" +} + +stop_all() { + pm2_stop "morph-geth" 2>/dev/null || true + pm2_stop "morph-reth" 2>/dev/null || true + pm2_stop "morph-node" 2>/dev/null || true +} + +format_duration() { + local total_seconds=$1 + local days=$((total_seconds / 86400)) + local hours=$(( (total_seconds % 86400) / 3600 )) + local minutes=$(( (total_seconds % 3600) / 60 )) + if [[ ${days} -gt 0 ]]; then + printf "%dd %dh %dm" "${days}" "${hours}" "${minutes}" + elif [[ ${hours} -gt 0 ]]; then + printf "%dh %dm" "${hours}" "${minutes}" + else + printf "%dm" "${minutes}" + fi +} + +# ─── Main ───────────────────────────────────────────────────────────────────── + +echo "==========================================" +echo " Morph Sync Speed Test: Geth vs Reth" +echo "==========================================" +echo " Duration per node: ${TEST_DURATION}s" +echo " Sample interval: ${SAMPLE_INTERVAL}s" +echo " Mainnet tip (est): ${MAINNET_TIP}" +echo "==========================================" +echo + +# Check prerequisites +pm2_check +check_binary "${MORPHNODE_BIN}" "cd ../morph/node && make build" + +# Results storage +GETH_AVG_BPS=0 +GETH_PEAK_BPS=0 +GETH_START=0 +GETH_END=0 +GETH_TOTAL=0 + +RETH_AVG_BPS=0 +RETH_PEAK_BPS=0 +RETH_START=0 +RETH_END=0 +RETH_TOTAL=0 + +# ─── Phase 1: Test Geth ────────────────────────────────────────────────────── + +if [[ "${SKIP_GETH}" != "1" ]]; then + check_binary "${GETH_BIN}" "cd ../morph/go-ethereum && make geth" + + echo "=== Phase 1: Testing Geth ===" + echo + + # Reset + full_reset + + # Prepare config (need jwt-secret and node config) + "${SCRIPT_DIR}/prepare.sh" 2>/dev/null + + # Start geth + echo " Starting morph-geth..." + "${SCRIPT_DIR}/geth-start.sh" + + # Wait for geth RPC + wait_for_rpc "geth" + + # Start morphnode + echo " Starting morphnode..." + "${SCRIPT_DIR}/node-start.sh" + + # Brief warmup to let morphnode establish connection + echo " Warming up (10s)..." + sleep 10 + + # Run BPS sampling + run_bps_sampling "geth" "${TEST_DURATION}" + GETH_AVG_BPS=${RESULT_AVG_BPS} + GETH_PEAK_BPS=${RESULT_PEAK_BPS} + GETH_START=${RESULT_START_BLOCK} + GETH_END=${RESULT_END_BLOCK} + GETH_TOTAL=${RESULT_TOTAL_BLOCKS} + + # Collect morphnode BPS logs + echo + echo " morphnode Block Sync Rate samples (geth):" + grep "Block Sync Rate" "${NODE_LOG_FILE}" 2>/dev/null | tail -5 | while read -r line; do + echo " ${line}" + done + + # Stop everything + echo + echo " Stopping geth test..." + stop_all + + echo + echo "=== Geth test complete ===" + echo +else + echo "=== Skipping Geth test (SKIP_GETH=1) ===" + echo +fi + +# ─── Phase 2: Test Reth ────────────────────────────────────────────────────── + +if [[ "${SKIP_RETH}" != "1" ]]; then + check_binary "${RETH_BIN}" "cargo build --release --bin morph-reth" + + echo "=== Phase 2: Testing Reth ===" + echo + + # Reset + full_reset + + # Prepare config + "${SCRIPT_DIR}/prepare.sh" 2>/dev/null + + # Start reth + echo " Starting morph-reth..." + "${SCRIPT_DIR}/reth-start.sh" + + # Wait for reth RPC + wait_for_rpc "reth" + + # Start morphnode + echo " Starting morphnode..." + "${SCRIPT_DIR}/node-start.sh" + + # Brief warmup + echo " Warming up (10s)..." + sleep 10 + + # Run BPS sampling + run_bps_sampling "reth" "${TEST_DURATION}" + RETH_AVG_BPS=${RESULT_AVG_BPS} + RETH_PEAK_BPS=${RESULT_PEAK_BPS} + RETH_START=${RESULT_START_BLOCK} + RETH_END=${RESULT_END_BLOCK} + RETH_TOTAL=${RESULT_TOTAL_BLOCKS} + + # Collect morphnode BPS logs + echo + echo " morphnode Block Sync Rate samples (reth):" + grep "Block Sync Rate" "${NODE_LOG_FILE}" 2>/dev/null | tail -5 | while read -r line; do + echo " ${line}" + done + + # Stop everything + echo + echo " Stopping reth test..." + stop_all + + echo + echo "=== Reth test complete ===" + echo +else + echo "=== Skipping Reth test (SKIP_RETH=1) ===" + echo +fi + +# ─── Results ────────────────────────────────────────────────────────────────── + +echo "==========================================" +echo " RESULTS" +echo "==========================================" +echo + +printf "%-20s %12s %12s\n" "" "Geth" "Reth" +printf "%-20s %12s %12s\n" "---" "---" "---" +printf "%-20s %12d %12d\n" "Start Block" "${GETH_START}" "${RETH_START}" +printf "%-20s %12d %12d\n" "End Block" "${GETH_END}" "${RETH_END}" +printf "%-20s %12d %12d\n" "Total Blocks" "${GETH_TOTAL}" "${RETH_TOTAL}" +printf "%-20s %12s %12s\n" "Avg BPS" "${GETH_AVG_BPS}" "${RETH_AVG_BPS}" +printf "%-20s %12s %12s\n" "Peak BPS" "${GETH_PEAK_BPS}" "${RETH_PEAK_BPS}" + +# ETA calculation +echo +echo "--- Estimated Full Sync Time (to block ${MAINNET_TIP}) ---" +if [[ $(echo "${GETH_AVG_BPS} > 0" | bc -l) -eq 1 ]]; then + geth_eta_seconds=$(echo "scale=0; ${MAINNET_TIP} / ${GETH_AVG_BPS}" | bc) + printf "Geth: %s (at %.2f bps)\n" "$(format_duration "${geth_eta_seconds}")" "${GETH_AVG_BPS}" +else + echo "Geth: N/A (no data)" +fi + +if [[ $(echo "${RETH_AVG_BPS} > 0" | bc -l) -eq 1 ]]; then + reth_eta_seconds=$(echo "scale=0; ${MAINNET_TIP} / ${RETH_AVG_BPS}" | bc) + printf "Reth: %s (at %.2f bps)\n" "$(format_duration "${reth_eta_seconds}")" "${RETH_AVG_BPS}" +else + echo "Reth: N/A (no data)" +fi + +# Winner +echo +if [[ $(echo "${GETH_AVG_BPS} > 0 && ${RETH_AVG_BPS} > 0" | bc -l) -eq 1 ]]; then + if [[ $(echo "${RETH_AVG_BPS} > ${GETH_AVG_BPS}" | bc -l) -eq 1 ]]; then + speedup=$(echo "scale=2; ${RETH_AVG_BPS} / ${GETH_AVG_BPS}" | bc) + echo "Winner: Reth (${speedup}x faster)" + elif [[ $(echo "${GETH_AVG_BPS} > ${RETH_AVG_BPS}" | bc -l) -eq 1 ]]; then + speedup=$(echo "scale=2; ${GETH_AVG_BPS} / ${RETH_AVG_BPS}" | bc) + echo "Winner: Geth (${speedup}x faster)" + else + echo "Result: Tie" + fi +fi + +echo +echo "==========================================" +echo " Test complete" +echo "=========================================="