diff --git a/Cargo.lock b/Cargo.lock index cbaa765..80ebb31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4934,7 +4934,6 @@ dependencies = [ "alloy-eips", "alloy-evm", "alloy-primitives", - "alloy-rlp", "alloy-sol-types", "auto_impl", "derive_more", @@ -6337,7 +6336,7 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "reth-basic-payload-builder" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6361,7 +6360,7 @@ dependencies = [ [[package]] name = "reth-chain-state" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6392,7 +6391,7 @@ dependencies = [ [[package]] name = "reth-chainspec" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-chains", "alloy-consensus", @@ -6412,7 +6411,7 @@ dependencies = [ [[package]] name = "reth-cli" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-genesis", "clap", @@ -6426,7 +6425,7 @@ dependencies = [ [[package]] name = "reth-cli-commands" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-chains", "alloy-consensus", @@ -6504,7 +6503,7 @@ dependencies = [ [[package]] name = "reth-cli-runner" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "reth-tasks", "tokio", @@ -6514,7 +6513,7 @@ dependencies = [ [[package]] name = "reth-cli-util" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-primitives", @@ -6531,7 +6530,7 @@ dependencies = [ [[package]] name = "reth-codecs" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6551,7 +6550,7 @@ dependencies = [ [[package]] name = "reth-codecs-derive" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "proc-macro2", "quote", @@ -6561,7 +6560,7 @@ dependencies = [ [[package]] name = "reth-config" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "eyre", "humantime-serde", @@ -6577,7 +6576,7 @@ dependencies = [ [[package]] name = "reth-consensus" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -6590,7 +6589,7 @@ dependencies = [ [[package]] name = "reth-consensus-common" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6602,7 +6601,7 @@ dependencies = [ [[package]] name = "reth-consensus-debug-client" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6628,7 +6627,7 @@ dependencies = [ [[package]] name = "reth-db" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "derive_more", @@ -6654,7 +6653,7 @@ dependencies = [ [[package]] name = "reth-db-api" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -6682,7 +6681,7 @@ dependencies = [ [[package]] name = "reth-db-common" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -6712,7 +6711,7 @@ dependencies = [ [[package]] name = "reth-db-models" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-primitives", @@ -6727,7 +6726,7 @@ dependencies = [ [[package]] name = "reth-discv4" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -6752,7 +6751,7 @@ dependencies = [ [[package]] name = "reth-discv5" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -6776,7 +6775,7 @@ dependencies = [ [[package]] name = "reth-dns-discovery" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "data-encoding", @@ -6800,7 +6799,7 @@ dependencies = [ [[package]] name = "reth-downloaders" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6831,7 +6830,7 @@ dependencies = [ [[package]] name = "reth-ecies" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "aes", "alloy-primitives", @@ -6859,7 +6858,7 @@ dependencies = [ [[package]] name = "reth-engine-local" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -6882,7 +6881,7 @@ dependencies = [ [[package]] name = "reth-engine-primitives" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6907,7 +6906,7 @@ dependencies = [ [[package]] name = "reth-engine-service" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "futures", "pin-project", @@ -6930,7 +6929,7 @@ dependencies = [ [[package]] name = "reth-engine-tree" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eip7928", @@ -6984,7 +6983,7 @@ dependencies = [ [[package]] name = "reth-engine-util" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -7012,7 +7011,7 @@ dependencies = [ [[package]] name = "reth-era" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7027,7 +7026,7 @@ dependencies = [ [[package]] name = "reth-era-downloader" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "bytes", @@ -7043,7 +7042,7 @@ dependencies = [ [[package]] name = "reth-era-utils" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7065,7 +7064,7 @@ dependencies = [ [[package]] name = "reth-errors" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "reth-consensus", "reth-execution-errors", @@ -7076,7 +7075,7 @@ dependencies = [ [[package]] name = "reth-eth-wire" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-chains", "alloy-primitives", @@ -7104,7 +7103,7 @@ dependencies = [ [[package]] name = "reth-eth-wire-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-chains", "alloy-consensus", @@ -7125,7 +7124,7 @@ dependencies = [ [[package]] name = "reth-ethereum-cli" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "clap", "eyre", @@ -7147,7 +7146,7 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7163,7 +7162,7 @@ dependencies = [ [[package]] name = "reth-ethereum-engine-primitives" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-primitives", @@ -7181,7 +7180,7 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -7194,7 +7193,7 @@ dependencies = [ [[package]] name = "reth-ethereum-payload-builder" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7223,7 +7222,7 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7243,7 +7242,7 @@ dependencies = [ [[package]] name = "reth-etl" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "rayon", "reth-db-api", @@ -7253,7 +7252,7 @@ dependencies = [ [[package]] name = "reth-evm" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7277,7 +7276,7 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7298,7 +7297,7 @@ dependencies = [ [[package]] name = "reth-execution-errors" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-evm", "alloy-primitives", @@ -7311,7 +7310,7 @@ dependencies = [ [[package]] name = "reth-execution-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7329,7 +7328,7 @@ dependencies = [ [[package]] name = "reth-exex" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7367,7 +7366,7 @@ dependencies = [ [[package]] name = "reth-exex-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-primitives", @@ -7381,7 +7380,7 @@ dependencies = [ [[package]] name = "reth-fs-util" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "serde", "serde_json", @@ -7391,7 +7390,7 @@ dependencies = [ [[package]] name = "reth-invalid-block-hooks" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7419,7 +7418,7 @@ dependencies = [ [[package]] name = "reth-ipc" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "bytes", "futures", @@ -7439,7 +7438,7 @@ dependencies = [ [[package]] name = "reth-libmdbx" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "bitflags 2.10.0", "byteorder", @@ -7455,7 +7454,7 @@ dependencies = [ [[package]] name = "reth-mdbx-sys" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "bindgen 0.71.1", "cc", @@ -7464,7 +7463,7 @@ dependencies = [ [[package]] name = "reth-metrics" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "futures", "metrics", @@ -7476,7 +7475,7 @@ dependencies = [ [[package]] name = "reth-net-banlist" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "ipnet", @@ -7485,7 +7484,7 @@ dependencies = [ [[package]] name = "reth-net-nat" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "futures-util", "if-addrs", @@ -7499,7 +7498,7 @@ dependencies = [ [[package]] name = "reth-network" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7555,7 +7554,7 @@ dependencies = [ [[package]] name = "reth-network-api" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7580,7 +7579,7 @@ dependencies = [ [[package]] name = "reth-network-p2p" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7602,7 +7601,7 @@ dependencies = [ [[package]] name = "reth-network-peers" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -7617,7 +7616,7 @@ dependencies = [ [[package]] name = "reth-network-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eip2124", "humantime-serde", @@ -7631,7 +7630,7 @@ dependencies = [ [[package]] name = "reth-nippy-jar" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "anyhow", "bincode", @@ -7648,7 +7647,7 @@ dependencies = [ [[package]] name = "reth-node-api" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-rpc-types-engine", "eyre", @@ -7672,7 +7671,7 @@ dependencies = [ [[package]] name = "reth-node-builder" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7741,7 +7740,7 @@ dependencies = [ [[package]] name = "reth-node-core" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7797,7 +7796,7 @@ dependencies = [ [[package]] name = "reth-node-ethereum" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-network", @@ -7835,7 +7834,7 @@ dependencies = [ [[package]] name = "reth-node-ethstats" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7859,7 +7858,7 @@ dependencies = [ [[package]] name = "reth-node-events" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7883,7 +7882,7 @@ dependencies = [ [[package]] name = "reth-node-metrics" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "bytes", "eyre", @@ -7906,7 +7905,7 @@ dependencies = [ [[package]] name = "reth-node-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "reth-chainspec", "reth-db-api", @@ -7918,7 +7917,7 @@ dependencies = [ [[package]] name = "reth-optimism-primitives" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7933,7 +7932,7 @@ dependencies = [ [[package]] name = "reth-payload-builder" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7954,7 +7953,7 @@ dependencies = [ [[package]] name = "reth-payload-builder-primitives" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "pin-project", "reth-payload-primitives", @@ -7966,7 +7965,7 @@ dependencies = [ [[package]] name = "reth-payload-primitives" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7989,7 +7988,7 @@ dependencies = [ [[package]] name = "reth-payload-util" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7999,7 +7998,7 @@ dependencies = [ [[package]] name = "reth-payload-validator" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -8009,7 +8008,7 @@ dependencies = [ [[package]] name = "reth-primitives-traits" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8042,7 +8041,7 @@ dependencies = [ [[package]] name = "reth-provider" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8085,7 +8084,7 @@ dependencies = [ [[package]] name = "reth-prune" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8113,7 +8112,7 @@ dependencies = [ [[package]] name = "reth-prune-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "arbitrary", @@ -8128,7 +8127,7 @@ dependencies = [ [[package]] name = "reth-revm" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "reth-primitives-traits", @@ -8141,7 +8140,7 @@ dependencies = [ [[package]] name = "reth-rpc" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -8223,7 +8222,7 @@ dependencies = [ [[package]] name = "reth-rpc-api" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eip7928", "alloy-eips", @@ -8253,7 +8252,7 @@ dependencies = [ [[package]] name = "reth-rpc-builder" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-network", "alloy-provider", @@ -8294,7 +8293,7 @@ dependencies = [ [[package]] name = "reth-rpc-convert" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-evm", @@ -8315,7 +8314,7 @@ dependencies = [ [[package]] name = "reth-rpc-engine-api" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8345,7 +8344,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-api" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -8389,7 +8388,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8437,7 +8436,7 @@ dependencies = [ [[package]] name = "reth-rpc-layer" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-rpc-types-engine", "http", @@ -8451,7 +8450,7 @@ dependencies = [ [[package]] name = "reth-rpc-server-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8467,7 +8466,7 @@ dependencies = [ [[package]] name = "reth-stages" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8512,7 +8511,7 @@ dependencies = [ [[package]] name = "reth-stages-api" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8539,7 +8538,7 @@ dependencies = [ [[package]] name = "reth-stages-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "arbitrary", @@ -8553,7 +8552,7 @@ dependencies = [ [[package]] name = "reth-static-file" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "parking_lot", @@ -8573,7 +8572,7 @@ dependencies = [ [[package]] name = "reth-static-file-types" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "clap", @@ -8586,7 +8585,7 @@ dependencies = [ [[package]] name = "reth-storage-api" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8610,7 +8609,7 @@ dependencies = [ [[package]] name = "reth-storage-errors" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8627,7 +8626,7 @@ dependencies = [ [[package]] name = "reth-tasks" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "auto_impl", "dyn-clone", @@ -8645,7 +8644,7 @@ dependencies = [ [[package]] name = "reth-tokio-util" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "tokio", "tokio-stream", @@ -8655,7 +8654,7 @@ dependencies = [ [[package]] name = "reth-tracing" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "clap", "eyre", @@ -8672,7 +8671,7 @@ dependencies = [ [[package]] name = "reth-tracing-otlp" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "clap", "eyre", @@ -8689,7 +8688,7 @@ dependencies = [ [[package]] name = "reth-transaction-pool" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8729,7 +8728,7 @@ dependencies = [ [[package]] name = "reth-trie" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8755,7 +8754,7 @@ dependencies = [ [[package]] name = "reth-trie-common" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8782,7 +8781,7 @@ dependencies = [ [[package]] name = "reth-trie-db" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "metrics", @@ -8802,7 +8801,7 @@ dependencies = [ [[package]] name = "reth-trie-parallel" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8827,7 +8826,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8846,7 +8845,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse-parallel" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8864,7 +8863,7 @@ dependencies = [ [[package]] name = "reth-zstd-compressors" version = "1.10.2" -source = "git+https://github.com/morph-l2/reth?rev=8057627ac9f169a0da4de44b616006a9e30382c1#8057627ac9f169a0da4de44b616006a9e30382c1" +source = "git+https://github.com/morph-l2/reth?rev=f13757781d2f6b0c1ec1c3e38f3ac32004bff24e#f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" dependencies = [ "zstd", ] @@ -9883,6 +9882,20 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "state-root-check" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "clap", + "eyre", + "morph-chainspec", + "morph-node", + "reth-provider", + "reth-storage-api", + "reth-trie", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index b49f43c..c5807ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ publish = false resolver = "3" members = [ "bin/morph-reth", + "bin/state-root-check", "crates/chainspec", "crates/consensus", "crates/engine-api", @@ -54,59 +55,60 @@ 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/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-basic-payload-builder = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-chain-state = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-chainspec = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-cli = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-cli-commands = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-cli-util = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-codecs = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-codecs-derive = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-consensus = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-consensus-common = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-db = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-db-api = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-e2e-test-utils = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-engine-local = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-engine-primitives = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-engine-tree = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-errors = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-eth-wire-types = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-ethereum = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-ethereum-cli = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-ethereum-consensus = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-ethereum-engine-primitives = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-ethereum-primitives = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e", default-features = false } +reth-evm = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-evm-ethereum = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-execution-types = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-metrics = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-network-peers = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-node-api = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-node-builder = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-node-core = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-node-ethereum = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-node-metrics = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-payload-builder = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-payload-primitives = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-payload-util = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-primitives-traits = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e", default-features = false } +reth-provider = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-rpc = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-rpc-api = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-rpc-builder = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-rpc-convert = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-rpc-eth-api = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-rpc-eth-types = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-rpc-server-types = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-storage-api = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-tasks = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-tracing = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-trie = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-trie-db = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-transaction-pool = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e" } +reth-zstd-compressors = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e", default-features = false } -reth-revm = { git = "https://github.com/morph-l2/reth", rev = "8057627ac9f169a0da4de44b616006a9e30382c1", features = [ +reth-revm = { git = "https://github.com/morph-l2/reth", rev = "f13757781d2f6b0c1ec1c3e38f3ac32004bff24e", features = [ "std", "optional-checks", ] } diff --git a/bin/morph-reth/src/main.rs b/bin/morph-reth/src/main.rs index 3033721..6be14ee 100644 --- a/bin/morph-reth/src/main.rs +++ b/bin/morph-reth/src/main.rs @@ -18,7 +18,9 @@ fn main() { // Install signal handler for segmentation faults sigsegv_handler::install(); - // Enable backtraces by default + // Enable backtraces by default. + // SAFETY: Called at process startup before any other threads are spawned, + // so there are no concurrent readers of the environment. if std::env::var_os("RUST_BACKTRACE").is_none() { unsafe { std::env::set_var("RUST_BACKTRACE", "1") }; } diff --git a/bin/state-root-check/Cargo.toml b/bin/state-root-check/Cargo.toml new file mode 100644 index 0000000..a84d786 --- /dev/null +++ b/bin/state-root-check/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "state-root-check" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +alloy-primitives.workspace = true +clap.workspace = true +eyre.workspace = true +morph-chainspec.workspace = true +morph-node.workspace = true +reth-provider.workspace = true +reth-storage-api.workspace = true +reth-trie.workspace = true + +[[bin]] +name = "state-root-check" +path = "src/main.rs" diff --git a/bin/state-root-check/src/lib.rs b/bin/state-root-check/src/lib.rs new file mode 100644 index 0000000..a675a8e --- /dev/null +++ b/bin/state-root-check/src/lib.rs @@ -0,0 +1,76 @@ +use alloy_primitives::B256; +use eyre::{Result, ensure}; + +#[cfg(test)] +mod tests { + use super::find_first_mismatch; + use eyre::Result; + + #[test] + fn returns_first_divergent_block_for_monotonic_range() -> Result<()> { + let mismatch_from = 103_u64; + + let got = find_first_mismatch(100, 105, |block| Ok(block < mismatch_from))?; + + assert_eq!(got, Some(mismatch_from)); + Ok(()) + } + + #[test] + fn returns_none_when_everything_matches() -> Result<()> { + let got = find_first_mismatch(100, 105, |_block| Ok(true))?; + + assert_eq!(got, None); + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockRootComparison { + pub block_number: u64, + pub reth_root: B256, + pub geth_disk_root: B256, +} + +impl BlockRootComparison { + pub fn is_match(&self) -> bool { + self.reth_root == self.geth_disk_root + } +} + +pub fn find_first_mismatch(from: u64, to: u64, mut matches: F) -> Result> +where + F: FnMut(u64) -> Result, +{ + ensure!( + from <= to, + "invalid range: from-block {from} is greater than to-block {to}" + ); + + if !matches(from)? { + return Ok(Some(from)); + } + + if matches(to)? { + return Ok(None); + } + + let mut low = from.saturating_add(1); + let mut high = to; + let mut first = to; + + while low <= high { + let mid = low + (high - low) / 2; + if matches(mid)? { + low = mid.saturating_add(1); + } else { + first = mid; + if mid == 0 { + break; + } + high = mid - 1; + } + } + + Ok(Some(first)) +} diff --git a/bin/state-root-check/src/main.rs b/bin/state-root-check/src/main.rs new file mode 100644 index 0000000..1878112 --- /dev/null +++ b/bin/state-root-check/src/main.rs @@ -0,0 +1,217 @@ +use alloy_primitives::B256; +use clap::{Parser, ValueEnum}; +use eyre::{Context, ContextCompat, Result, ensure}; +use morph_chainspec::{MORPH_HOODI, MORPH_MAINNET, MorphChainSpec}; +use morph_node::{MorphNode, validator::fetch_geth_disk_root}; +use reth_provider::{ + ProviderFactory, + providers::{ProviderNodeTypes, ReadOnlyConfig}, +}; +use reth_storage_api::{BlockNumReader, StateRootProvider}; +use reth_trie::HashedPostState; +use state_root_check::{BlockRootComparison, find_first_mismatch}; +use std::{collections::HashMap, path::PathBuf, sync::Arc}; + +#[derive(Debug, Clone, Copy, Default, ValueEnum)] +enum ChainArg { + Morph, + #[default] + #[value(name = "morph-hoodi")] + MorphHoodi, +} + +impl ChainArg { + fn chain_spec(self) -> Arc { + match self { + Self::Morph => MORPH_MAINNET.clone(), + Self::MorphHoodi => MORPH_HOODI.clone(), + } + } +} + +#[derive(Debug, Parser)] +#[command(name = "state-root-check")] +#[command(about = "Compare local reth MPT roots with geth morph_diskRoot")] +struct Args { + #[arg(long)] + datadir: PathBuf, + #[arg(long, default_value = "morph-hoodi")] + chain: ChainArg, + #[arg(long)] + geth_rpc_url: Option, + #[arg(long, conflicts_with_all = ["fresh_root_at", "bisect"])] + block: Option, + #[arg(long, conflicts_with_all = ["block", "bisect"])] + fresh_root_at: Option, + #[arg(long, requires_all = ["from_block", "to_block"], conflicts_with_all = ["block", "fresh_root_at"])] + bisect: bool, + #[arg(long, requires = "bisect")] + from_block: Option, + #[arg(long, requires = "bisect")] + to_block: Option, +} + +enum Mode { + LocalRoot { + block: u64, + }, + Compare { + block: u64, + geth_rpc_url: String, + }, + Bisect { + from: u64, + to: u64, + geth_rpc_url: String, + }, +} + +fn main() -> Result<()> { + let args = Args::parse(); + ensure!( + args.datadir.exists(), + "datadir does not exist: {}", + args.datadir.display() + ); + + let factory = MorphNode::provider_factory_builder() + .open_read_only( + args.chain.chain_spec(), + ReadOnlyConfig::from_datadir(&args.datadir), + ) + .with_context(|| format!("failed to open datadir {}", args.datadir.display()))?; + + let latest_block = factory.best_block_number()?; + let mode = resolve_mode(&args, latest_block)?; + + match mode { + Mode::LocalRoot { block } => { + let root = compute_local_state_root(&factory, block)?; + println!("fresh_root_at #{block}: {root:#x}"); + } + Mode::Compare { + block, + geth_rpc_url, + } => { + let comparison = compare_block_roots(&factory, &geth_rpc_url, block)?; + print_comparison(&comparison); + } + Mode::Bisect { + from, + to, + geth_rpc_url, + } => { + let mut cache = HashMap::new(); + let first = find_first_mismatch(from, to, |block| { + if let Some(is_match) = cache.get(&block) { + return Ok(*is_match); + } + + let comparison = compare_block_roots(&factory, &geth_rpc_url, block)?; + let is_match = comparison.is_match(); + print_probe(&comparison); + cache.insert(block, is_match); + Ok(is_match) + })?; + + match first { + Some(block) => println!("first_mismatch: {block}"), + None => println!("all blocks matched in range [{from}, {to}]"), + } + } + } + + Ok(()) +} + +fn resolve_mode(args: &Args, latest_block: u64) -> Result { + if let Some(block) = args.fresh_root_at { + return Ok(Mode::LocalRoot { block }); + } + + if args.bisect { + let from = args + .from_block + .context("--from-block is required when --bisect is set")?; + let to = args + .to_block + .context("--to-block is required when --bisect is set")?; + ensure!( + from <= to, + "--from-block must be less than or equal to --to-block" + ); + return Ok(Mode::Bisect { + from, + to, + geth_rpc_url: args + .geth_rpc_url + .clone() + .context("--geth-rpc-url is required for --bisect")?, + }); + } + + let block = args.block.unwrap_or(latest_block); + if let Some(geth_rpc_url) = args.geth_rpc_url.clone() { + Ok(Mode::Compare { + block, + geth_rpc_url, + }) + } else { + Ok(Mode::LocalRoot { block }) + } +} + +fn compute_local_state_root(factory: &ProviderFactory, block: u64) -> Result +where + N: ProviderNodeTypes, +{ + let provider = factory + .history_by_block_number(block) + .with_context(|| format!("failed to open historical state at block {block}"))?; + let root = provider + .state_root(HashedPostState::default()) + .with_context(|| format!("failed to compute local state root at block {block}"))?; + Ok(root) +} + +fn compare_block_roots( + factory: &ProviderFactory, + geth_rpc_url: &str, + block: u64, +) -> Result +where + N: ProviderNodeTypes, +{ + let reth_root = compute_local_state_root(factory, block)?; + let geth_disk_root = + fetch_geth_disk_root(geth_rpc_url, block).map_err(|err| eyre::eyre!(err))?; + Ok(BlockRootComparison { + block_number: block, + reth_root, + geth_disk_root, + }) +} + +fn print_comparison(comparison: &BlockRootComparison) { + let status = if comparison.is_match() { + "MATCH" + } else { + "MISMATCH" + }; + println!("block #{}", comparison.block_number); + println!("reth_root: {:#x}", comparison.reth_root); + println!("geth_disk_root: {:#x}", comparison.geth_disk_root); + println!("status: {status}"); +} + +fn print_probe(comparison: &BlockRootComparison) { + let status = if comparison.is_match() { + "MATCH" + } else { + "MISMATCH" + }; + println!( + "probe #{} => {} reth={:#x} geth={:#x}", + comparison.block_number, status, comparison.reth_root, comparison.geth_disk_root + ); +} diff --git a/crates/consensus/src/validation.rs b/crates/consensus/src/validation.rs index a885930..5c8b914 100644 --- a/crates/consensus/src/validation.rs +++ b/crates/consensus/src/validation.rs @@ -14,7 +14,7 @@ //! - Coinbase must be zero when FeeVault is enabled //! - Timestamp cannot be in the future //! - Gas limit must be within bounds -//! - Base fee must be set after Curie hardfork +//! - Base fee must always be set (EIP-1559 is always active) //! //! ## L1 Message Rules //! @@ -38,8 +38,11 @@ use crate::MorphConsensusError; use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH, TxReceipt}; use alloy_evm::block::BlockExecutionResult; use alloy_primitives::{B256, Bloom}; -use morph_chainspec::MorphChainSpec; -use morph_primitives::{Block, BlockBody, MorphHeader, MorphReceipt, MorphTxEnvelope}; +use morph_chainspec::{MorphChainSpec, MorphHardforks}; +use morph_primitives::{ + Block, BlockBody, MorphHeader, MorphReceipt, MorphTxEnvelope, + transaction::morph_transaction::MORPH_TX_VERSION_1, +}; use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom}; use reth_consensus_common::validation::{ validate_against_parent_hash_number, validate_body_against_header, @@ -74,6 +77,25 @@ const GAS_LIMIT_BOUND_DIVISOR: u64 = 1024; /// /// Validates Morph L2 blocks according to the L2 consensus rules. /// See module-level documentation for detailed validation rules. +/// +/// # L1 Message Validation Architecture +/// +/// L1 message ordering requires both body data (transactions) and parent header data. +/// Since reth's `Consensus` trait methods provide these separately — `validate_block_pre_execution` +/// has the block body but not the parent header, while `validate_header_against_parent` has +/// both headers but not the body — the validation is split into two independent checks: +/// +/// 1. **Internal consistency** (`validate_block_pre_execution`): L1 messages are at the block +/// start, have sequential queue indices, and are consistent with `header.next_l1_msg_index`. +/// 2. **Cross-block monotonicity** (`validate_header_against_parent`): `header.next_l1_msg_index` +/// is monotonically non-decreasing relative to the parent. +/// +/// These two methods have no ordering dependency and share no mutable state. The strict +/// cross-block equality check (`header.next == parent.next + l1_count`) requires simultaneous +/// access to both parent header and block body, which reth's trait API does not provide in +/// any single method. In Morph's single-sequencer model, the remaining gap (queue index +/// skipping) is prevented by the trusted sequencer and verified by the L1 message queue +/// contract. #[derive(Debug, Clone)] pub struct MorphConsensus { /// Chain specification containing hardfork information and chain config. @@ -82,7 +104,7 @@ pub struct MorphConsensus { impl MorphConsensus { /// Creates a new [`MorphConsensus`] instance. - pub const fn new(chain_spec: Arc) -> Self { + pub fn new(chain_spec: Arc) -> Self { Self { chain_spec } } @@ -109,7 +131,7 @@ impl HeaderValidator for MorphConsensus { /// 6. **Timestamp**: Must not be in the future /// 7. **Gas Limit**: Must be <= MAX_GAS_LIMIT /// 8. **Gas Used**: Must be <= gas limit - /// 9. **Base Fee**: Must be set after Curie hardfork and <= 10 Gwei + /// 9. **Base Fee**: Must always be set (EIP-1559 is always active) and <= 10 Gwei fn validate_header(&self, header: &SealedHeader) -> Result<(), ConsensusError> { // Extra data must be empty (Morph L2 specific - stricter than max length) if !header.extra_data().is_empty() { @@ -198,12 +220,28 @@ impl HeaderValidator for MorphConsensus { // Validate parent hash and block number validate_against_parent_hash_number(header.header(), parent)?; - // Validate timestamp against parent - validate_against_parent_timestamp(header.header(), parent.header())?; + // Validate timestamp against parent (pre-Emerald: strict >, post-Emerald: >=) + let is_emerald = self + .chain_spec + .is_emerald_active_at_timestamp(header.timestamp()); + validate_against_parent_timestamp(header.header(), parent.header(), is_emerald)?; // Validate gas limit change validate_against_parent_gas_limit(header.header(), parent.header())?; + // Cross-block L1 message index monotonicity: next_l1_msg_index must never + // decrease across blocks. This is the header-only half of L1 message + // validation; the body-level half is in validate_block_pre_execution. + if header.next_l1_msg_index < parent.next_l1_msg_index { + return Err(ConsensusError::Other( + MorphConsensusError::InvalidNextL1MessageIndex { + expected: parent.next_l1_msg_index, + actual: header.next_l1_msg_index, + } + .to_string(), + )); + } + Ok(()) } } @@ -266,9 +304,22 @@ impl Consensus for MorphConsensus { )); } - // Validate L1 messages ordering - let txs: Vec<_> = block.body().transactions().collect(); - validate_l1_messages(&txs)?; + // Validate MorphTx version and field constraints. + // Matches go-ethereum's BlockValidator.ValidateBody() → ValidateMorphTxVersion(). + let is_jade = self + .chain_spec + .is_jade_active_at_timestamp(block.header().timestamp()); + validate_morph_txs(&block.body().transactions, is_jade)?; + + // Validate L1 messages ordering and internal consistency with header. + // This is the body-level half of L1 validation; it verifies that the L1 + // messages within this block are internally consistent with the header's + // next_l1_msg_index. The cross-block monotonicity check (ensuring + // next_l1_msg_index >= parent's value) is in validate_header_against_parent. + validate_l1_messages_in_block( + &block.body().transactions, + block.header().next_l1_msg_index, + )?; Ok(()) } @@ -349,22 +400,28 @@ impl FullConsensus for MorphConsensus { } } -/// Validates that the header's timestamp is not before the parent's timestamp. +/// Validates that the header's timestamp is valid relative to the parent's timestamp. /// -/// # Errors +/// # Hardfork Behavior /// -/// Returns [`ConsensusError::TimestampIsInPast`] if the header's timestamp -/// is less than the parent's timestamp. +/// - **Pre-Emerald**: Timestamp must be strictly greater than parent's timestamp. +/// - **Post-Emerald**: Timestamp must be greater than or equal to parent's timestamp. /// -/// # Note +/// This matches go-ethereum's `consensus/l2/consensus.go:155-157`. /// -/// Equal timestamps are allowed - only strictly less than is rejected. +/// # Errors +/// +/// Returns [`ConsensusError::TimestampIsInPast`] if the header's timestamp +/// violates the hardfork-specific constraint. #[inline] fn validate_against_parent_timestamp( header: &H, parent: &H, + is_emerald: bool, ) -> Result<(), ConsensusError> { - if header.timestamp() < parent.timestamp() { + if header.timestamp() < parent.timestamp() + || (header.timestamp() == parent.timestamp() && !is_emerald) + { return Err(ConsensusError::TimestampIsInPast { parent_timestamp: parent.timestamp(), timestamp: header.timestamp(), @@ -392,7 +449,7 @@ fn validate_against_parent_gas_limit( ) -> Result<(), ConsensusError> { let diff = header.gas_limit().abs_diff(parent.gas_limit()); let limit = parent.gas_limit() / GAS_LIMIT_BOUND_DIVISOR; - if diff > limit { + if diff >= limit { return if header.gas_limit() > parent.gas_limit() { Err(ConsensusError::GasLimitInvalidIncrease { parent_gas_limit: parent.gas_limit(), @@ -419,30 +476,36 @@ fn validate_against_parent_gas_limit( // L1 Message Validation // ============================================================================ -/// Validates L1 message ordering in a block's transactions. +/// Validates L1 message ordering and internal consistency within a single block. /// -/// L1 messages are special transactions that originate from L1 (deposits, etc.). -/// They must follow strict ordering rules to ensure deterministic block execution. +/// This is a **stateless** validation that uses only the block's own transactions +/// and header — it does not require the parent header or any shared mutable state. /// -/// # Rules +/// # Checks Performed /// /// 1. **Position**: All L1 messages must appear at the beginning of the block. /// Once a regular (L2) transaction appears, no more L1 messages are allowed. /// /// 2. **Sequential Queue Index**: L1 messages must have strictly sequential -/// `queue_index` values. If the first L1 message has `queue_index = N`, -/// the next must have `queue_index = N+1`, and so on. +/// `queue_index` values (each = previous + 1). /// -/// # Errors +/// 3. **Header Consistency**: If L1 messages are present, +/// `header.next_l1_msg_index` must be >= `last_queue_index + 1`. It may be +/// strictly greater because Morph allows L1 messages to be "skipped" — the +/// sequencer can advance past queue indices not included in the block body. +/// +/// # Cross-Block Validation /// -/// - [`MorphConsensusError::MalformedL1Message`] if an L1 message is missing its queue_index -/// - [`MorphConsensusError::L1MessagesNotInOrder`] if queue indices are not sequential -/// - [`MorphConsensusError::InvalidL1MessageOrder`] if L1 message appears after L2 transaction +/// The cross-block check (ensuring `next_l1_msg_index >= parent.next_l1_msg_index`) +/// is performed separately in `validate_header_against_parent`, which has access to +/// the parent header. See the [`MorphConsensus`] doc comment for the full architecture. /// /// # Example (Valid) /// /// ```text -/// [L1Msg(queue=0), L1Msg(queue=1), L1Msg(queue=2), RegularTx, RegularTx] +/// [L1Msg(queue=5), L1Msg(queue=6), L1Msg(queue=7), RegularTx] +/// // header.next_l1_msg_index = 8 ✓ (exact match) +/// // header.next_l1_msg_index = 10 ✓ (skipped queue indices 8, 9) /// ``` /// /// # Example (Invalid - L1 after L2) @@ -451,41 +514,127 @@ fn validate_against_parent_gas_limit( /// [L1Msg(queue=0), RegularTx, L1Msg(queue=1)] // ❌ L1 after L2 /// ``` #[inline] -fn validate_l1_messages(txs: &[&MorphTxEnvelope]) -> Result<(), ConsensusError> { - // Find the starting queue index from the first L1 message - let mut queue_index = txs - .iter() - .find(|tx| tx.is_l1_msg()) - .and_then(|tx| tx.queue_index()) - .unwrap_or_default(); - +fn validate_l1_messages_in_block( + txs: &[MorphTxEnvelope], + header_next_l1_msg_index: u64, +) -> Result<(), ConsensusError> { + let mut l1_msg_count = 0u64; let mut saw_l2_transaction = false; + let mut prev_queue_index: Option = None; for tx in txs { - // Check queue index is strictly sequential if tx.is_l1_msg() { + // Check L1 messages are only at the start of the block (before any L2 tx) + if saw_l2_transaction { + return Err(ConsensusError::Other( + MorphConsensusError::InvalidL1MessageOrder.to_string(), + )); + } + let tx_queue_index = tx.queue_index().ok_or_else(|| { ConsensusError::Other(MorphConsensusError::MalformedL1Message.to_string()) })?; - if tx_queue_index != queue_index { - return Err(ConsensusError::Other( - MorphConsensusError::L1MessagesNotInOrder { - expected: queue_index, - actual: tx_queue_index, - } - .to_string(), - )); + + // Check queue indices are strictly sequential (each = previous + 1). + // Use checked_add to prevent overflow at u64::MAX. + if let Some(prev) = prev_queue_index { + let expected = prev.checked_add(1).ok_or_else(|| { + ConsensusError::Other( + MorphConsensusError::L1MessagesNotInOrder { + expected: u64::MAX, + actual: tx_queue_index, + } + .to_string(), + ) + })?; + if tx_queue_index != expected { + return Err(ConsensusError::Other( + MorphConsensusError::L1MessagesNotInOrder { + expected, + actual: tx_queue_index, + } + .to_string(), + )); + } } - queue_index = tx_queue_index + 1; + + prev_queue_index = Some(tx_queue_index); + l1_msg_count += 1; + } else { + saw_l2_transaction = true; + } + } + + // Validate header consistency: header.next_l1_msg_index must be at least + // last_queue_index + 1 (cannot go backwards relative to included messages). + // It may be strictly greater because Morph allows L1 messages to be + // "skipped" — the sequencer can advance past queue indices that are not + // included in the block body (e.g., messages that failed on L1 relay). + // go-eth's NumL1MessagesProcessed() comment: "This count includes both + // skipped and included messages." + // For blocks with no L1 messages, this check is skipped — the cross-block + // monotonicity check in validate_header_against_parent handles that case. + if l1_msg_count > 0 { + let last_queue_index = prev_queue_index.ok_or_else(|| { + ConsensusError::Other( + "internal error: l1_msg_count > 0 but prev_queue_index is None".to_string(), + ) + })?; + let min_expected = last_queue_index.checked_add(1).ok_or_else(|| { + ConsensusError::Other( + MorphConsensusError::InvalidNextL1MessageIndex { + expected: u64::MAX, + actual: header_next_l1_msg_index, + } + .to_string(), + ) + })?; + if header_next_l1_msg_index < min_expected { + return Err(ConsensusError::Other( + MorphConsensusError::InvalidNextL1MessageIndex { + expected: min_expected, + actual: header_next_l1_msg_index, + } + .to_string(), + )); + } + } + + Ok(()) +} + +/// Validates all MorphTx (0x7F) transactions in a block. +/// +/// Performs two checks per MorphTx: +/// 1. **Hardfork gate**: rejects V1 transactions before the Jade fork is active +/// 2. **Field validation**: delegates to [`TxMorph::validate()`] for version-specific +/// field constraints, memo length, and gas price ordering +/// +/// See [`TxMorph::validate()`] for the detailed per-version rules. +fn validate_morph_txs(txs: &[MorphTxEnvelope], is_jade: bool) -> Result<(), ConsensusError> { + for tx in txs { + let morph_tx = match tx { + MorphTxEnvelope::Morph(signed) => signed.tx(), + _ => continue, + }; + + // Reject MorphTx V1 before Jade fork (hardfork-gated, consensus-only check). + if !is_jade && morph_tx.version == MORPH_TX_VERSION_1 { + return Err(ConsensusError::Other( + MorphConsensusError::InvalidBody( + "MorphTx version 1 is not yet active (jade fork not reached)".into(), + ) + .to_string(), + )); } - // Check L1 messages are only at the start of the block - if tx.is_l1_msg() && saw_l2_transaction { + // Reuse primitive-layer validation (version, fee_token_id, reference, + // memo length, fee_limit constraints, gas price ordering). + if let Err(reason) = morph_tx.validate() { return Err(ConsensusError::Other( - MorphConsensusError::InvalidL1MessageOrder.to_string(), + MorphConsensusError::InvalidBody(reason.to_string()).to_string(), )); } - saw_l2_transaction = !tx.is_l1_msg(); } Ok(()) @@ -615,25 +764,26 @@ mod tests { } #[test] - fn test_validate_l1_messages_valid() { + fn test_validate_l1_messages_in_block_valid() { let txs = [ create_l1_msg_tx(0), create_l1_msg_tx(1), create_regular_tx(), ]; - let txs_refs: Vec<_> = txs.iter().collect(); - assert!(validate_l1_messages(&txs_refs).is_ok()); + + // L1 msgs: 0, 1 → last+1=2==header_next + assert!(validate_l1_messages_in_block(&txs, 2).is_ok()); } #[test] - fn test_validate_l1_messages_after_regular() { + fn test_validate_l1_messages_in_block_after_regular() { let txs = [ create_l1_msg_tx(0), create_regular_tx(), create_l1_msg_tx(1), ]; - let txs_refs: Vec<_> = txs.iter().collect(); - assert!(validate_l1_messages(&txs_refs).is_err()); + + assert!(validate_l1_messages_in_block(&txs, 2).is_err()); } #[test] @@ -753,40 +903,47 @@ mod tests { // ======================================================================== #[test] - fn test_validate_l1_messages_empty_block() { + fn test_validate_l1_messages_in_block_empty_block() { let txs: [MorphTxEnvelope; 0] = []; - let txs_refs: Vec<_> = txs.iter().collect(); - assert!(validate_l1_messages(&txs_refs).is_ok()); + + // Empty block: no L1 messages → internal check always passes. + // Any header_next value is accepted because the cross-block + // monotonicity check is in validate_header_against_parent. + assert!(validate_l1_messages_in_block(&txs, 0).is_ok()); + assert!(validate_l1_messages_in_block(&txs, 5).is_ok()); + assert!(validate_l1_messages_in_block(&txs, 100).is_ok()); } #[test] - fn test_validate_l1_messages_only_l1_messages() { + fn test_validate_l1_messages_in_block_only_l1_messages() { let txs = [ create_l1_msg_tx(0), create_l1_msg_tx(1), create_l1_msg_tx(2), ]; - let txs_refs: Vec<_> = txs.iter().collect(); - assert!(validate_l1_messages(&txs_refs).is_ok()); + + // last=2, 2+1=3==header_next + assert!(validate_l1_messages_in_block(&txs, 3).is_ok()); } #[test] - fn test_validate_l1_messages_only_regular_txs() { + fn test_validate_l1_messages_in_block_only_regular_txs() { let txs = [ create_regular_tx(), create_regular_tx(), create_regular_tx(), ]; - let txs_refs: Vec<_> = txs.iter().collect(); - assert!(validate_l1_messages(&txs_refs).is_ok()); + + // No L1 messages → internal check passes (header_next not checked) + assert!(validate_l1_messages_in_block(&txs, 0).is_ok()); } #[test] - fn test_validate_l1_messages_skipped_index() { - // Skip index 1: 0, 2 + fn test_validate_l1_messages_in_block_skipped_index() { + // Block has 0 then 2 (skipping 1) — caught by sequential check let txs = [create_l1_msg_tx(0), create_l1_msg_tx(2)]; - let txs_refs: Vec<_> = txs.iter().collect(); - let result = validate_l1_messages(&txs_refs); + + let result = validate_l1_messages_in_block(&txs, 3); assert!(result.is_err()); let err_str = result.unwrap_err().to_string(); assert!(err_str.contains("expected 1")); @@ -794,23 +951,24 @@ mod tests { } #[test] - fn test_validate_l1_messages_non_zero_start_index() { - // Starting from index 100 is valid + fn test_validate_l1_messages_in_block_non_zero_start_index() { + // Block starts L1 messages at queue_index 100 let txs = [ create_l1_msg_tx(100), create_l1_msg_tx(101), create_regular_tx(), ]; - let txs_refs: Vec<_> = txs.iter().collect(); - assert!(validate_l1_messages(&txs_refs).is_ok()); + + // last=101, 101+1=102==header_next + assert!(validate_l1_messages_in_block(&txs, 102).is_ok()); } #[test] - fn test_validate_l1_messages_duplicate_index() { - // Duplicate index: 0, 0 + fn test_validate_l1_messages_in_block_duplicate_index() { + // Duplicate index: 0, 0 — caught by sequential check (prev=0, expected 1, got 0) let txs = [create_l1_msg_tx(0), create_l1_msg_tx(0)]; - let txs_refs: Vec<_> = txs.iter().collect(); - let result = validate_l1_messages(&txs_refs); + + let result = validate_l1_messages_in_block(&txs, 1); assert!(result.is_err()); let err_str = result.unwrap_err().to_string(); assert!(err_str.contains("expected 1")); @@ -818,16 +976,53 @@ mod tests { } #[test] - fn test_validate_l1_messages_out_of_order() { - // Reversed order: 1, 0 + fn test_validate_l1_messages_in_block_out_of_order() { + // Block has 1 then 0 — caught by sequential check (prev=1, expected 2, got 0) let txs = [create_l1_msg_tx(1), create_l1_msg_tx(0)]; - let txs_refs: Vec<_> = txs.iter().collect(); - let result = validate_l1_messages(&txs_refs); + + let result = validate_l1_messages_in_block(&txs, 2); assert!(result.is_err()); } #[test] - fn test_validate_l1_messages_multiple_l1_after_regular() { + fn test_validate_l1_messages_in_block_next_index_too_low() { + // Valid sequential L1 messages (0, 1, 2) but header.next_l1_msg_index < last+1 + let txs = [ + create_l1_msg_tx(0), + create_l1_msg_tx(1), + create_l1_msg_tx(2), + create_regular_tx(), + ]; + + // Header says 2 but minimum is 3 (last=2, 2+1=3) — INVALID + let result = validate_l1_messages_in_block(&txs, 2); + assert!(result.is_err()); + let err_str = result.unwrap_err().to_string(); + assert!(err_str.contains("expected 3")); + assert!(err_str.contains("got 2")); + } + + #[test] + fn test_validate_l1_messages_in_block_skipped_messages_allowed() { + // L1 messages 0, 1, 2 but header says next=5 (messages 3, 4 were skipped). + // This is valid — Morph allows the sequencer to skip L1 messages. + let txs = [ + create_l1_msg_tx(0), + create_l1_msg_tx(1), + create_l1_msg_tx(2), + create_regular_tx(), + ]; + + // header_next=5 > last+1=3 — valid (2 messages skipped) + assert!(validate_l1_messages_in_block(&txs, 5).is_ok()); + // header_next=3 == last+1=3 — valid (no messages skipped) + assert!(validate_l1_messages_in_block(&txs, 3).is_ok()); + // header_next=100 > last+1=3 — valid (many messages skipped) + assert!(validate_l1_messages_in_block(&txs, 100).is_ok()); + } + + #[test] + fn test_validate_l1_messages_in_block_multiple_l1_after_regular() { // Multiple L1 messages after regular tx let txs = [ create_l1_msg_tx(0), @@ -835,8 +1030,8 @@ mod tests { create_l1_msg_tx(1), create_l1_msg_tx(2), ]; - let txs_refs: Vec<_> = txs.iter().collect(); - assert!(validate_l1_messages(&txs_refs).is_err()); + + assert!(validate_l1_messages_in_block(&txs, 3).is_err()); } // ======================================================================== @@ -994,6 +1189,50 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_validate_header_against_parent_l1_msg_index_monotonicity() { + let chain_spec = create_test_chainspec(); + let consensus = MorphConsensus::new(chain_spec); + + // Parent has next_l1_msg_index = 10 + let mut parent = create_valid_morph_header(1000, 30_000_000, 100); + parent.next_l1_msg_index = 10; + let parent_sealed = SealedHeader::seal_slow(parent); + + // Child with next_l1_msg_index = 15 (increased, valid) + let mut child = create_valid_morph_header(1001, 30_000_000, 101); + child.inner.parent_hash = parent_sealed.hash(); + child.next_l1_msg_index = 15; + let child_sealed = SealedHeader::seal_slow(child); + assert!( + consensus + .validate_header_against_parent(&child_sealed, &parent_sealed) + .is_ok() + ); + + // Child with next_l1_msg_index = 10 (unchanged, valid — no L1 msgs in block) + let mut child_same = create_valid_morph_header(1001, 30_000_000, 101); + child_same.inner.parent_hash = parent_sealed.hash(); + child_same.next_l1_msg_index = 10; + let child_same_sealed = SealedHeader::seal_slow(child_same); + assert!( + consensus + .validate_header_against_parent(&child_same_sealed, &parent_sealed) + .is_ok() + ); + + // Child with next_l1_msg_index = 5 (decreased, INVALID) + let mut child_dec = create_valid_morph_header(1001, 30_000_000, 101); + child_dec.inner.parent_hash = parent_sealed.hash(); + child_dec.next_l1_msg_index = 5; + let child_dec_sealed = SealedHeader::seal_slow(child_dec); + let result = consensus.validate_header_against_parent(&child_dec_sealed, &parent_sealed); + assert!(result.is_err()); + let err_str = result.unwrap_err().to_string(); + assert!(err_str.contains("expected 10")); + assert!(err_str.contains("got 5")); + } + #[test] fn test_validate_header_against_parent_timestamp_less_than_parent() { let chain_spec = create_test_chainspec(); @@ -1087,13 +1326,30 @@ mod tests { let parent = create_valid_morph_header(1000, parent_gas_limit, 100); let parent_sealed = SealedHeader::seal_slow(parent); - // Increase by exactly the allowed amount (valid) - let mut child = create_valid_morph_header(1001, parent_gas_limit + max_change, 101); - child.inner.parent_hash = parent_sealed.hash(); - let child_sealed = SealedHeader::seal_slow(child); + // Increase by exactly the boundary (diff == limit) should be REJECTED, + // matching go-ethereum's `diff >= limit` check. + let mut child_at_boundary = + create_valid_morph_header(1001, parent_gas_limit + max_change, 101); + child_at_boundary.inner.parent_hash = parent_sealed.hash(); + let child_sealed = SealedHeader::seal_slow(child_at_boundary); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); - assert!(result.is_ok()); + assert!( + matches!(result, Err(ConsensusError::GasLimitInvalidIncrease { .. })), + "gas limit change exactly at boundary should be rejected" + ); + + // Increase by one less than the boundary should be ACCEPTED + let mut child_within = + create_valid_morph_header(1001, parent_gas_limit + max_change - 1, 101); + child_within.inner.parent_hash = parent_sealed.hash(); + let child_sealed = SealedHeader::seal_slow(child_within); + + let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); + assert!( + result.is_ok(), + "gas limit change within boundary should be accepted" + ); } #[test] @@ -1226,17 +1482,32 @@ mod tests { let parent = create_valid_header(1000, 30_000_000, 100); let child = create_valid_header(1001, 30_000_000, 101); - let result = validate_against_parent_timestamp(&child, &parent); - assert!(result.is_ok()); + // Both pre-Emerald and post-Emerald: strictly greater is always ok + assert!(validate_against_parent_timestamp(&child, &parent, false).is_ok()); + assert!(validate_against_parent_timestamp(&child, &parent, true).is_ok()); } #[test] - fn test_validate_against_parent_timestamp_equal() { + fn test_validate_against_parent_timestamp_equal_pre_emerald() { let parent = create_valid_header(1000, 30_000_000, 100); let child = create_valid_header(1000, 30_000_000, 101); // Same timestamp - let result = validate_against_parent_timestamp(&child, &parent); - assert!(result.is_ok()); // Equal timestamp is allowed + // Pre-Emerald: equal timestamp is rejected + let result = validate_against_parent_timestamp(&child, &parent, false); + assert!(matches!( + result, + Err(ConsensusError::TimestampIsInPast { .. }) + )); + } + + #[test] + fn test_validate_against_parent_timestamp_equal_post_emerald() { + let parent = create_valid_header(1000, 30_000_000, 100); + let child = create_valid_header(1000, 30_000_000, 101); // Same timestamp + + // Post-Emerald: equal timestamp is allowed + let result = validate_against_parent_timestamp(&child, &parent, true); + assert!(result.is_ok()); } #[test] @@ -1244,9 +1515,13 @@ mod tests { let parent = create_valid_header(1000, 30_000_000, 100); let child = create_valid_header(999, 30_000_000, 101); // Earlier timestamp - let result = validate_against_parent_timestamp(&child, &parent); + // Both pre-Emerald and post-Emerald: strictly less is always rejected assert!(matches!( - result, + validate_against_parent_timestamp(&child, &parent, false), + Err(ConsensusError::TimestampIsInPast { .. }) + )); + assert!(matches!( + validate_against_parent_timestamp(&child, &parent, true), Err(ConsensusError::TimestampIsInPast { .. }) )); } diff --git a/crates/engine-api/src/builder.rs b/crates/engine-api/src/builder.rs index 8b85082..74a588e 100644 --- a/crates/engine-api/src/builder.rs +++ b/crates/engine-api/src/builder.rs @@ -60,13 +60,34 @@ struct InMemoryHead { timestamp: u64, } +/// Allow FCU tag fallback to head only while the imported block is clearly historical. +/// +/// Once imported blocks are close to wall-clock time, we stop synthesizing safe/finalized and +/// wait for Morph node's real `set_block_tags` updates instead. +const FCU_TAG_FALLBACK_MAX_AGE_SECS: u64 = 60; + /// 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. +/// +/// Also caches L1-based safe/finalized block hashes from `set_block_tags` so that +/// the FCU can pass them to the engine tree, keeping both memory cleanup and +/// RPC-visible tags consistent. #[derive(Debug, Default)] pub struct EngineStateTracker { head: RwLock>, + /// Last L1-based safe/finalized hashes from `set_block_tags`. + /// `None` means `set_block_tags` has not yet provided a value (e.g. during + /// historical sync when all batches are already finalized on L1). + block_tags: RwLock, +} + +/// Cached L1-based block tag hashes from `set_block_tags`. +#[derive(Debug, Default, Clone, Copy)] +struct BlockTagCache { + safe_hash: Option, + finalized_hash: Option, } impl EngineStateTracker { @@ -94,6 +115,27 @@ impl EngineStateTracker { fn current_head(&self) -> Option { *self.head.read() } + + /// Caches L1-based block tag hashes from a successful `set_block_tags` call. + pub fn record_block_tags(&self, safe_hash: Option, finalized_hash: Option) { + let mut tags = self.block_tags.write(); + if let Some(h) = safe_hash { + tags.safe_hash = Some(h); + } + if let Some(h) = finalized_hash { + tags.finalized_hash = Some(h); + } + } + + /// Returns the last L1-based finalized hash, or `None` if not yet set. + fn l1_finalized_hash(&self) -> Option { + self.block_tags.read().finalized_hash + } + + /// Returns the last L1-based safe hash, or `None` if not yet set. + fn l1_safe_hash(&self) -> Option { + self.block_tags.read().safe_hash + } } impl RealMorphL2EngineApi { @@ -170,6 +212,8 @@ where ); // 1. Enforce canonical continuity against the current head. + // Matching go-ethereum: returns error (not GenericResponse{false}) for + // discontinuous block number or parent hash mismatch. let current_head = self.current_head()?; if data.number != current_head.number + 1 { tracing::warn!( @@ -178,7 +222,10 @@ where actual = data.number, "cannot validate block with discontinuous block number" ); - return Ok(GenericResponse { success: false }); + return Err(MorphEngineApiError::DiscontinuousBlockNumber { + expected: current_head.number + 1, + actual: data.number, + }); } if data.parent_hash != current_head.hash { @@ -188,7 +235,10 @@ where actual = %data.parent_hash, "parent hash mismatch" ); - return Ok(GenericResponse { success: false }); + return Err(MorphEngineApiError::WrongParentHash { + expected: current_head.hash, + actual: data.parent_hash, + }); } // 2. Convert and forward to reth engine tree (`newPayload` path). @@ -302,19 +352,21 @@ where }); } - let imported_header = self.import_l2_block_via_engine(data).await?; + let block_hash = data.hash; + let block_number = data.number; + self.import_l2_block_via_engine(data).await?; tracing::debug!( target: "morph::engine", - block_hash = %imported_header.hash_slow(), - block_number = imported_header.number(), + block_hash = %block_hash, + block_number, "L2 block accepted via engine tree" ); Ok(()) } - async fn new_safe_l2_block(&self, data: SafeL2Data) -> EngineApiResult { + async fn new_safe_l2_block(&self, mut data: SafeL2Data) -> EngineApiResult { tracing::debug!( target: "morph::engine", block_number = data.number, @@ -334,29 +386,40 @@ where // 2. Assemble the block from SafeL2Data inputs. let assemble_params = AssembleL2BlockParams { number: data.number, - transactions: data.transactions.clone(), + // Move transactions out of data to avoid cloning the full Vec. + transactions: std::mem::take(&mut data.transactions), timestamp: Some(data.timestamp), }; let built_payload = self .build_l2_payload(assemble_params, Some(data.gas_limit), data.base_fee_per_gas) .await?; - let executable_data = built_payload.executable_data.clone(); + let executable_data = built_payload.executable_data; + // Save hash before moving executable_data into the import call. + let block_hash = executable_data.hash; // 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 - .import_l2_block_via_engine(executable_data.clone()) - .await?; - - // 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 { + let header = self.import_l2_block_via_engine(executable_data).await?; + + // Update safe block tag and seed finalized for memory cleanup. + // + // Validator / derivation mode does not run BlockTagService, so + // set_block_tags is never called externally. Without a cached + // finalized hash the FCU falls back to B256::ZERO once blocks are + // near wall-clock time, disabling changeset-cache eviction. + // + // Passing block_hash as finalized here seeds the tracker so the + // engine tree can keep evicting. Once validators adopt + // BlockTagService the L1-derived finalized value will naturally + // supersede this hint. + // + // Best-effort: block import already succeeded, so don't fail the + // whole call if only the tag update encounters an issue. + if let Err(e) = self.set_block_tags(block_hash, block_hash).await { tracing::warn!( target: "morph::engine", - block_hash = %executable_data.hash, + block_hash = %block_hash, error = %e, "failed to update safe tag after block import; tag can be set later via setBlockTags" ); @@ -364,7 +427,7 @@ where tracing::debug!( target: "morph::engine", - block_hash = %header.hash_slow(), + block_hash = %block_hash, "safe L2 block imported successfully" ); @@ -414,6 +477,22 @@ where ); } + // Cache the L1-based hashes so subsequent FCU calls use them instead of + // falling back to head. This keeps engine-tree finalization and + // RPC-visible tags aligned with the actual L1 finalization status. + self.engine_state_tracker.record_block_tags( + if safe_block_hash != B256::ZERO { + Some(safe_block_hash) + } else { + None + }, + if finalized_block_hash != B256::ZERO { + Some(finalized_block_hash) + } else { + None + }, + ); + Ok(()) } } @@ -475,7 +554,7 @@ impl RealMorphL2EngineApi { withdrawals: Some(Vec::new()), parent_beacon_block_root: None, }, - transactions: Some(params.transactions.clone()), + transactions: Some(params.transactions), gas_limit: gas_limit_override, base_fee_per_gas: base_fee_override, }; @@ -536,12 +615,44 @@ impl RealMorphL2EngineApi { 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. + // Morph uses Tendermint consensus with instant finality — every committed + // block is final and no reorgs are possible. + // + // The safe/finalized hashes passed here serve two purposes in reth's engine + // tree: (1) driving changeset-cache eviction and sidechain pruning (memory + // management), and (2) setting the RPC-visible "safe"/"finalized" block tags. + // + // When BlockTagService has provided L1-based tags via set_block_tags, we + // forward those so the engine tree and RPC layer stay consistent with the + // actual L1 finalization status. + // + // During deep historical sync, BlockTagService may be unable to provide + // tags for already-finalized batches. In that case we temporarily fall back + // to head so the engine tree can continue evicting old changesets. + // + // Once imported blocks are close to wall-clock time, we stop synthesizing + // safe/finalized and wait for real L1-derived tags to avoid falsely + // advertising live blocks as finalized in the catch-up window. + let now_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let finalized_hash = resolve_fcu_block_tag_hash( + self.engine_state_tracker.l1_finalized_hash(), + data.hash, + data.timestamp, + now_timestamp, + ); + let safe_hash = resolve_fcu_block_tag_hash( + self.engine_state_tracker.l1_safe_hash(), + data.hash, + data.timestamp, + now_timestamp, + ); let forkchoice = alloy_rpc_types_engine::ForkchoiceState { head_block_hash: data.hash, - safe_block_hash: B256::ZERO, - finalized_block_hash: B256::ZERO, + safe_block_hash: safe_hash, + finalized_block_hash: finalized_hash, }; self.provider.on_forkchoice_update_received(&forkchoice); @@ -560,12 +671,11 @@ impl RealMorphL2EngineApi { // 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.engine_state_tracker + .record_local_head(data.number, data.hash, data.timestamp); 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, @@ -630,12 +740,19 @@ impl RealMorphL2EngineApi { let cancun_active = self .chain_spec .is_cancun_active_at_timestamp(data.timestamp); + // Override coinbase to empty address when FeeVault is enabled, + // matching go-ethereum's executableDataToBlock (l2_api.go:292-293). + let beneficiary = if self.chain_spec.is_fee_vault_enabled() { + Address::ZERO + } else { + data.miner + }; 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, + beneficiary, state_root: data.state_root, transactions_root: calculate_transaction_root(&txs), receipts_root: data.receipts_root, @@ -661,15 +778,20 @@ impl RealMorphL2EngineApi { ommers: Default::default(), withdrawals: None, }; - let sealed_block = SealedBlock::seal_slow(Block::new(header.clone(), body)); - if sealed_block.hash() != data.hash { + // Compute header hash once and verify against expected hash before + // constructing the sealed block. This avoids the clone + re-hash that + // seal_slow would perform, saving one keccak256 + one MorphHeader clone + // per block import. + let computed_hash = header.hash_slow(); + if computed_hash != data.hash { return Err(MorphEngineApiError::ValidationFailed(format!( "block hash mismatch: expected {}, computed {}", - data.hash, - sealed_block.hash() + data.hash, computed_hash ))); } + let sealed_block = + SealedBlock::new_unchecked(Block::new(header.clone(), body), computed_hash); Ok(( MorphExecutionData::with_expected_withdraw_trie_root( @@ -778,6 +900,21 @@ fn apply_executable_data_overrides( Ok(RecoveredBlock::new_unhashed(block, senders)) } +fn resolve_fcu_block_tag_hash( + l1_tag_hash: Option, + head_hash: B256, + block_timestamp: u64, + now_timestamp: u64, +) -> B256 { + match l1_tag_hash { + Some(hash) => hash, + None if now_timestamp.saturating_sub(block_timestamp) > FCU_TAG_FALLBACK_MAX_AGE_SECS => { + head_hash + } + None => B256::ZERO, + } +} + #[cfg(test)] mod tests { use super::*; @@ -818,6 +955,34 @@ mod tests { assert_eq!(current_head.timestamp, sealed_header.timestamp()); } + #[test] + fn test_resolve_fcu_block_tag_hash_uses_l1_tag_when_available() { + let l1_tag = B256::from([0x11; 32]); + let head = B256::from([0x22; 32]); + + let resolved = resolve_fcu_block_tag_hash(Some(l1_tag), head, 1_700_000_000, 1_700_000_030); + + assert_eq!(resolved, l1_tag); + } + + #[test] + fn test_resolve_fcu_block_tag_hash_falls_back_to_head_for_historical_blocks() { + let head = B256::from([0x33; 32]); + + let resolved = resolve_fcu_block_tag_hash(None, head, 1_700_000_000, 1_700_000_000 + 300); + + assert_eq!(resolved, head); + } + + #[test] + fn test_resolve_fcu_block_tag_hash_returns_zero_near_live_without_l1_tag() { + let head = B256::from([0x44; 32]); + + let resolved = resolve_fcu_block_tag_hash(None, head, 1_700_000_000, 1_700_000_000 + 5); + + assert_eq!(resolved, B256::ZERO); + } + #[test] fn test_apply_executable_data_overrides_aligns_hash_with_engine_data() { let source_header: MorphHeader = Header::default().into(); diff --git a/crates/engine-api/src/rpc.rs b/crates/engine-api/src/rpc.rs index 3282e17..3f43b12 100644 --- a/crates/engine-api/src/rpc.rs +++ b/crates/engine-api/src/rpc.rs @@ -95,51 +95,51 @@ where &self, params: AssembleL2BlockParams, ) -> RpcResult { - tracing::debug!(target: "morph::engine", block_number = params.number, "assembling L2 block"); + tracing::debug!(target: "rpc::engine", block_number = params.number, "assembling L2 block"); self.inner.assemble_l2_block(params).await.map_err(|e| { - tracing::error!(target: "morph::engine", error = %e, "failed to assemble L2 block"); + tracing::error!(target: "rpc::engine", error = %e, "failed to assemble L2 block"); e.into() }) } async fn validate_l2_block(&self, data: ExecutableL2Data) -> RpcResult { tracing::debug!( - target: "morph::engine", + target: "rpc::engine", block_number = data.number, block_hash = %data.hash, "validating L2 block" ); self.inner.validate_l2_block(data).await.map_err(|e| { - tracing::error!(target: "morph::engine", error = %e, "failed to validate L2 block"); + tracing::error!(target: "rpc::engine", error = %e, "failed to validate L2 block"); e.into() }) } async fn new_l2_block(&self, data: ExecutableL2Data) -> RpcResult<()> { tracing::debug!( - target: "morph::engine", + target: "rpc::engine", block_number = data.number, block_hash = %data.hash, "RPC newL2Block called" ); self.inner.new_l2_block(data).await.map_err(|e| { - tracing::error!(target: "morph::engine", error = %e, "failed to import L2 block"); + tracing::error!(target: "rpc::engine", error = %e, "failed to import L2 block"); e.into() }) } async fn new_safe_l2_block(&self, data: SafeL2Data) -> RpcResult { tracing::debug!( - target: "morph::engine", + target: "rpc::engine", block_number = data.number, "RPC newSafeL2Block called" ); self.inner.new_safe_l2_block(data).await.map_err(|e| { - tracing::error!(target: "morph::engine", error = %e, "failed to import safe L2 block"); + tracing::error!(target: "rpc::engine", error = %e, "failed to import safe L2 block"); e.into() }) } @@ -150,7 +150,7 @@ where finalized_block_hash: B256, ) -> RpcResult<()> { tracing::debug!( - target: "morph::engine", + target: "rpc::engine", %safe_block_hash, %finalized_block_hash, "RPC setBlockTags called" @@ -160,7 +160,7 @@ where .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"); + tracing::error!(target: "rpc::engine", error = %e, "failed to set block tags"); e.into() }) } diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index 110c1d9..69a97c9 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -22,17 +22,14 @@ use alloy_evm::{ Database, Evm, block::{BlockExecutionError, BlockExecutionResult, BlockExecutor, ExecutableTx, OnStateHook}, }; -use alloy_primitives::U256; +use alloy_primitives::{Address, U256}; use curie::apply_curie_hard_fork; use morph_chainspec::{MorphChainSpec, MorphHardfork, MorphHardforks}; use morph_primitives::{MorphReceipt, MorphTxEnvelope}; -use morph_revm::{ - L1_GAS_PRICE_ORACLE_ADDRESS, L1BlockInfo, MorphHaltReason, TokenFeeInfo, evm::MorphContext, -}; +use morph_revm::{L1_GAS_PRICE_ORACLE_ADDRESS, MorphHaltReason, TokenFeeInfo, evm::MorphContext}; use reth_chainspec::EthereumHardforks; use reth_revm::{DatabaseCommit, Inspector, State, context::result::ResultAndState}; use revm::context::Block; -use std::marker::PhantomData; /// Block executor for Morph L2 blocks. /// @@ -71,8 +68,9 @@ pub(crate) struct MorphBlockExecutor<'a, DB: Database, I> { receipts: Vec, /// Total gas used by executed transactions gas_used: u64, - /// Phantom data for inspector type - _phantom: PhantomData, + /// Cached hardfork for this block (constant across all transactions). + /// Set in `apply_pre_execution_changes`, reused in `commit_transaction`. + hardfork: MorphHardfork, } impl<'a, DB, I> MorphBlockExecutor<'a, DB, I> @@ -97,52 +95,19 @@ where receipt_builder, receipts: Vec::new(), gas_used: 0, - _phantom: PhantomData, + hardfork: MorphHardfork::default(), } } - /// Calculate the L1 data fee for a transaction. - /// - /// The L1 fee compensates for the cost of posting transaction data to Ethereum L1. - /// This is a key component of L2 transaction costs on Morph. - /// - /// # Calculation Steps - /// 1. Check if transaction is an L1 message (which don't pay L1 fees) - /// 2. Get RLP-encoded transaction bytes - /// 3. Fetch L1 block info from L1 Gas Price Oracle contract - /// 4. Calculate fee based on transaction size and L1 gas price + /// Returns the L1 data fee for the most recently executed transaction. /// - /// # Arguments - /// * `tx` - The transaction to calculate L1 fee for - /// * `hardfork` - The current Morph hardfork (affects fee calculation formula) - /// - /// # Returns - /// - `Ok(U256::ZERO)` for L1 message transactions - /// - `Ok(fee)` for regular transactions, where fee = f(tx_size, l1_gas_price, hardfork) - /// - `Err` if L1 block info cannot be fetched - /// - /// # Errors - /// Returns error if the L1 Gas Price Oracle contract state cannot be read. - fn calculate_l1_fee( - &mut self, - tx: &MorphTxEnvelope, - hardfork: MorphHardfork, - ) -> Result { - // L1 message transactions don't pay L1 fees - if tx.is_l1_msg() { - return Ok(U256::ZERO); - } - - // Get the RLP-encoded transaction bytes - let rlp_bytes = tx.rlp(); - - // Fetch L1 block info from the L1 Gas Price Oracle contract - let l1_block_info = L1BlockInfo::try_fetch(self.evm.db_mut(), hardfork).map_err(|e| { - BlockExecutionError::msg(format!("Failed to fetch L1 block info: {e:?}")) - })?; - - // Calculate L1 data fee - Ok(l1_block_info.calculate_tx_l1_cost(rlp_bytes.as_ref(), hardfork)) + /// Reads from the handler's per-transaction cache (set during + /// `validate_and_deduct_eth_fee` / `validate_and_deduct_token_fee`), + /// avoiding re-encoding the full transaction RLP. + /// For L1 messages (which skip handler fee logic) the cache is ZERO. + #[inline] + fn cached_l1_fee(&self) -> U256 { + self.evm.cached_l1_data_fee() } /// Extract MorphTx-specific fields for MorphTx (0x7F) transactions. @@ -160,6 +125,7 @@ where /// /// # Arguments /// * `tx` - The transaction to extract fields from + /// * `sender` - Transaction sender (used for token registry balance queries) /// * `hardfork` - The current Morph hardfork (affects token registry behavior) /// /// # Returns @@ -170,11 +136,11 @@ where /// # Errors /// Returns error if: /// - MorphTx is missing `fee_token_id` or `fee_limit` - /// - Transaction sender cannot be extracted /// - L2TokenRegistry contract cannot be queried fn get_morph_tx_fields( &mut self, tx: &MorphTxEnvelope, + sender: Address, hardfork: MorphHardfork, ) -> Result, BlockExecutionError> { // Only MorphTx transactions have these fields @@ -192,20 +158,33 @@ where // Extract version, reference, and memo from the transaction let version = tx.version().unwrap_or(0); let reference = tx.reference(); - let memo = tx.memo(); - - // Fetch token fee info from L2TokenRegistry contract - // Note: We use the transaction sender as the caller address - // This is needed to check token balance when validating MorphTx - let sender = tx - .signer_unchecked() - .map_err(|_| BlockExecutionError::msg("Failed to extract signer from MorphTx"))?; + let memo = tx.memo().cloned(); + + // For fee_token_id==0 (ETH fee MorphTx, V1 only), no token registry lookup needed. + // Still preserve version/reference/memo in the receipt. + if fee_token_id == 0 { + return Ok(Some(MorphReceiptTxFields { + version, + fee_token_id: 0, + fee_rate: U256::ZERO, + token_scale: U256::ZERO, + fee_limit, + reference, + memo, + })); + } - let token_info = - TokenFeeInfo::load_for_caller(self.evm.db_mut(), fee_token_id, sender, hardfork) - .map_err(|e| { - BlockExecutionError::msg(format!("Failed to fetch token fee info: {e:?}")) - })?; + // Reuse cached token fee info from handler validation to avoid redundant DB reads. + // Falls back to DB read if cache is empty (e.g., in test scenarios). + let token_info = match self.evm.cached_token_fee_info() { + Some(info) => Some(info), + None => { + TokenFeeInfo::load_for_caller(self.evm.db_mut(), fee_token_id, sender, hardfork) + .map_err(|e| { + BlockExecutionError::msg(format!("Failed to fetch token fee info: {e:?}")) + })? + } + }; Ok(token_info.map(|info| MorphReceiptTxFields { version, @@ -251,13 +230,22 @@ where let state_clear_flag = self.spec.is_spurious_dragon_active_at_block(block_number); self.evm.db_mut().set_state_clear_flag(state_clear_flag); - // 2. Load L1 gas oracle contract into cache + // 2. Load L1 gas oracle contract into cache so that subsequent per-tx + // L1BlockInfo reads in the handler are fast (avoid cold DB hits). + // NOTE: We do NOT cache L1BlockInfo here because the oracle can be + // updated by a regular transaction (from the external gas-oracle service) + // within the same block. The handler reads it per-tx instead. let _ = self .evm .db_mut() .load_cache_account(L1_GAS_PRICE_ORACLE_ADDRESS) .map_err(BlockExecutionError::other)?; + let hardfork = self + .spec + .morph_hardfork_at(block_number, self.evm.block().timestamp.to::()); + self.hardfork = hardfork; + // 3. Apply Curie hardfork at the transition block // Only executes once at the exact block where Curie activates if self @@ -333,6 +321,7 @@ where /// /// # Errors /// Returns error if L1 fee calculation or token fee info extraction fails. + #[inline] fn commit_transaction( &mut self, output: ResultAndState, @@ -340,28 +329,30 @@ where ) -> Result { let ResultAndState { result, state } = output; - // Determine hardfork once and reuse for both L1 fee and token fee calculations - let block_number: u64 = self.evm.block().number.to(); - let timestamp: u64 = self.evm.block().timestamp.to(); - let hardfork = self.spec.morph_hardfork_at(block_number, timestamp); - - // Calculate L1 fee for the transaction - let l1_fee = self.calculate_l1_fee(tx.tx(), hardfork)?; + // Read L1 fee from handler cache (set during validate_and_deduct_*). + let l1_fee = self.cached_l1_fee(); - // Get MorphTx-specific fields for MorphTx transactions - let morph_tx_fields = self.get_morph_tx_fields(tx.tx(), hardfork)?; + // Get MorphTx-specific fields for MorphTx transactions. + // Uses the hardfork cached in apply_pre_execution_changes (constant per block). + let morph_tx_fields = self.get_morph_tx_fields(tx.tx(), *tx.signer(), self.hardfork)?; // Update cumulative gas used let gas_used = result.gas_used(); self.gas_used += gas_used; - // Build receipt + // Build receipt. + // Fee Transfer logs are cached separately by the handler (pre_fee_logs / + // post_fee_logs) so they survive main tx revert. + let pre_fee_logs = self.evm.take_pre_fee_logs(); + let post_fee_logs = self.evm.take_post_fee_logs(); let ctx: MorphReceiptBuilderCtx<'_, Self::Evm> = MorphReceiptBuilderCtx { tx: tx.tx(), result, cumulative_gas_used: self.gas_used, l1_fee, morph_tx_fields, + pre_fee_logs, + post_fee_logs, }; self.receipts.push(self.receipt_builder.build_receipt(ctx)); diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs index eb7f411..2b5f52c 100644 --- a/crates/evm/src/block/receipt.rs +++ b/crates/evm/src/block/receipt.rs @@ -28,7 +28,7 @@ use alloy_consensus::Receipt; use alloy_consensus::transaction::TxHashRef; use alloy_evm::Evm; -use alloy_primitives::{B256, Bytes, U256}; +use alloy_primitives::{B256, Bytes, Log, U256}; use morph_primitives::{MorphReceipt, MorphTransactionReceipt, MorphTxEnvelope, MorphTxType}; use revm::context::result::ExecutionResult; use tracing::warn; @@ -44,7 +44,9 @@ use tracing::warn; /// - `result`: EVM execution result (success/failure, logs, gas used) /// - `cumulative_gas_used`: Running total of gas used in the block /// - `l1_fee`: Pre-calculated L1 data fee for this transaction -/// - `token_fee_info`: Token fee details for MorphTx transactions +/// - `morph_tx_fields`: MorphTx-specific fields (token fee info, version, reference, memo) +/// - `pre_fee_logs`: Transfer event logs from token fee deduction (survives tx revert) +/// - `post_fee_logs`: Transfer event logs from token fee reimbursement #[derive(Debug)] pub(crate) struct MorphReceiptBuilderCtx<'a, E: Evm> { /// The executed transaction @@ -57,6 +59,11 @@ pub(crate) struct MorphReceiptBuilderCtx<'a, E: Evm> { pub l1_fee: U256, /// MorphTx-specific fields (token fee info, version, reference, memo) pub morph_tx_fields: Option, + /// Transfer event logs from token fee deduction (before main tx execution). + /// Managed separately from the handler pipeline to survive main tx revert. + pub pre_fee_logs: Vec, + /// Transfer event logs from token fee reimbursement (after main tx execution). + pub post_fee_logs: Vec, } /// MorphTx (0x7F) specific fields for receipts. @@ -148,12 +155,26 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { cumulative_gas_used, l1_fee, morph_tx_fields, + pre_fee_logs, + post_fee_logs, } = ctx; + // Assemble logs in chronological order matching go-ethereum: + // [deduct Transfer] + [main tx logs] + [refund Transfer] + // Fee logs are cached separately from the journal so they survive + // main tx revert (revm's ExecutionResult::Revert carries no logs). + let is_success = result.is_success(); + let main_logs = result.into_logs(); + let mut logs = + Vec::with_capacity(pre_fee_logs.len() + main_logs.len() + post_fee_logs.len()); + logs.extend(pre_fee_logs); + logs.extend(main_logs); + logs.extend(post_fee_logs); + let inner = Receipt { - status: result.is_success().into(), + status: is_success.into(), cumulative_gas_used, - logs: result.into_logs(), + logs, }; // Create the appropriate receipt variant based on transaction type @@ -197,7 +218,7 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { )) } else { warn!( - target: "morph::receipt", + target: "morph::evm", tx_hash = ?tx.tx_hash(), "MorphTx missing token fee fields - receipt will not include fee token info. \ This may indicate an unregistered/inactive token or a bug." diff --git a/crates/evm/src/evm.rs b/crates/evm/src/evm.rs index 4a873ef..4c64400 100644 --- a/crates/evm/src/evm.rs +++ b/crates/evm/src/evm.rs @@ -7,11 +7,9 @@ use alloy_evm::{ inspector::NoOpInspector, }, }; -use alloy_primitives::{Address, Bytes, Log}; +use alloy_primitives::{Address, Bytes}; use morph_chainspec::hardfork::MorphHardfork; -use morph_revm::{ - MorphHaltReason, MorphInvalidTransaction, MorphPrecompiles, MorphTxEnv, evm::MorphContext, -}; +use morph_revm::{MorphHaltReason, MorphInvalidTransaction, MorphTxEnv, evm::MorphContext}; use reth_revm::MainContext; use std::ops::{Deref, DerefMut}; @@ -66,19 +64,21 @@ pub struct MorphEvm { impl MorphEvm { /// Create a new [`MorphEvm`] instance. pub fn new(db: DB, input: EvmEnv) -> Self { - let spec = input.cfg_env.spec; let ctx = Context::mainnet() .with_db(db) .with_block(input.block_env) .with_cfg(input.cfg_env) - .with_tx(Default::default()); + .with_tx(Default::default()) + .with_chain(morph_revm::MorphTxRuntime::default()); - // Create precompiles for the hardfork and wrap in PrecompilesMap - let morph_precompiles = MorphPrecompiles::new_with_spec(spec); - let precompiles_map = PrecompilesMap::from_static(morph_precompiles.precompiles()); + // Build the inner MorphEvm which creates precompiles once. + // Derive the PrecompilesMap from the inner's precompiles to avoid + // a second MorphPrecompiles::new_with_spec call. + let inner = morph_revm::MorphEvm::new(ctx, NoOpInspector {}); + let precompiles_map = PrecompilesMap::from_static(inner.precompiles.precompiles()); Self { - inner: morph_revm::MorphEvm::new(ctx, NoOpInspector {}), + inner, precompiles_map, inspect: false, } @@ -105,16 +105,34 @@ impl MorphEvm { } } - /// Takes the inner EVM's revert logs. - /// - /// This is used as a work around to allow logs to be - /// included for reverting transactions. + /// Returns the cached token fee info from the handler's validation phase. /// - /// TODO: remove once revm supports emitting logs for reverted transactions + /// Avoids redundant DB reads when the block executor needs token fee + /// parameters (e.g., for receipt construction). + #[inline] + pub fn cached_token_fee_info(&self) -> Option { + self.inner.cached_token_fee_info() + } + + /// Returns the L1 data fee cached during handler validation. /// - /// - pub fn take_revert_logs(&mut self) -> Vec { - std::mem::take(&mut self.inner.logs) + /// Avoids re-encoding the full transaction RLP in the block executor's + /// receipt-building path. + #[inline] + pub fn cached_l1_data_fee(&self) -> alloy_primitives::U256 { + self.inner.cached_l1_data_fee() + } + + /// Takes the cached pre-execution fee logs (token fee deduction Transfer events). + #[inline] + pub fn take_pre_fee_logs(&mut self) -> Vec { + self.inner.take_pre_fee_logs() + } + + /// Takes the cached post-execution fee logs (token fee reimbursement Transfer events). + #[inline] + pub fn take_post_fee_logs(&mut self) -> Vec { + self.inner.take_post_fee_logs() } } diff --git a/crates/node/src/add_ons.rs b/crates/node/src/add_ons.rs index 9536ed9..9f5b6f8 100644 --- a/crates/node/src/add_ons.rs +++ b/crates/node/src/add_ons.rs @@ -50,12 +50,7 @@ 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); + let pvb = MorphEngineValidatorBuilder::default(); Self { inner: RpcAddOns::new( MorphEthApiBuilder::default(), diff --git a/crates/node/src/args.rs b/crates/node/src/args.rs index 5fe2394..f19e07a 100644 --- a/crates/node/src/args.rs +++ b/crates/node/src/args.rs @@ -33,15 +33,6 @@ 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 { @@ -49,7 +40,6 @@ 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/components/pool.rs b/crates/node/src/components/pool.rs index c892af1..8aad6a0 100644 --- a/crates/node/src/components/pool.rs +++ b/crates/node/src/components/pool.rs @@ -42,6 +42,12 @@ where .with_additional_tasks(ctx.config().txpool.additional_validation_tasks) // Register MorphTx (0x7F) type for ERC20 gas payment .with_custom_tx_type(morph_primitives::MORPH_TX_TYPE_ID) + // Disable the inner EthTransactionValidator's balance check. + // MorphTx (fee_token_id > 0) users may have zero ETH but pay gas in ERC20 tokens. + // Without this, the inner validator rejects them before reaching MorphTransactionValidator's + // token fee validation. The MorphTransactionValidator already performs its own balance + // checks for all tx types (including L1 data fee), so this is safe. + .disable_balance_check() // Note: L1Message (0x7E) is NOT registered - it will be rejected by // EthTransactionValidator as TxTypeNotSupported, which is correct since // L1 messages should only be included by the sequencer during block building diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index 3951114..e74a7ac 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -37,6 +37,7 @@ use reth_payload_primitives::PayloadAttributesBuilder; use reth_primitives_traits::SealedHeader; use reth_provider::{ BlockWriter, CanonChainTracker, DBProvider, DatabaseProviderFactory, EthStorage, + providers::ProviderFactoryBuilder, }; use std::sync::Arc; @@ -60,6 +61,11 @@ impl MorphNode { Self { args } } + /// Instantiates a [`ProviderFactoryBuilder`] for a Morph node. + pub fn provider_factory_builder() -> ProviderFactoryBuilder { + ProviderFactoryBuilder::default() + } + /// Returns a [`ComponentsBuilder`] configured for a Morph node. pub fn components( payload_builder_config: MorphBuilderConfig, @@ -126,7 +132,7 @@ where } fn add_ons(&self) -> Self::AddOns { - MorphAddOns::with_geth_rpc_url(self.args.geth_rpc_url.clone()) + MorphAddOns::new() } } diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index a29dde0..9fdba00 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -15,8 +15,7 @@ use reth_chainspec::EthChainSpec; use reth_errors::ConsensusError; use reth_node_api::{ AddOnsContext, BlockTy, FullNodeComponents, InvalidPayloadAttributesError, NewPayloadError, - NodeTypes, PayloadAttributes, PayloadTypes, PayloadValidator, StateRootDecisionInput, - StateRootValidator, + NodeTypes, PayloadAttributes, PayloadTypes, PayloadValidator, StateRootValidator, }; use reth_node_builder::{ invalid_block_hook::InvalidBlockHookExt, @@ -24,7 +23,6 @@ use reth_node_builder::{ }; 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). @@ -32,18 +30,7 @@ use std::{collections::VecDeque, sync::Arc}; /// Creates a validator for validating engine API payloads. #[derive(Debug, Default, Clone)] #[non_exhaustive] -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 - } -} +pub struct MorphEngineValidatorBuilder; impl PayloadValidatorBuilder for MorphEngineValidatorBuilder where @@ -53,11 +40,7 @@ where type Validator = MorphEngineValidator; 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) + Ok(MorphEngineValidator::new(ctx.node.provider().chain_spec())) } } @@ -142,8 +125,6 @@ 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)] @@ -161,17 +142,9 @@ impl MorphEngineValidator { 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, @@ -295,24 +268,16 @@ impl PayloadValidator for MorphEngineValidator { } 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. + // Enforce canonical state-root equality in MPT mode (post-Jade). if jade_active { let expected_state_root = block.header().state_root(); if computed_state_root != expected_state_root { @@ -326,48 +291,7 @@ impl StateRootValidator for MorphEngineValida } } - // 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(()) - } - } + Ok(()) } } @@ -403,7 +327,7 @@ impl std::fmt::Display for JsonRpcError { /// 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 { +pub 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", diff --git a/crates/payload/builder/src/builder.rs b/crates/payload/builder/src/builder.rs index c84a212..f1357c0 100644 --- a/crates/payload/builder/src/builder.rs +++ b/crates/payload/builder/src/builder.rs @@ -288,9 +288,10 @@ impl MorphPayloadBuilderCtx { ) -> Result, PayloadBuilderError> { let block_gas_limit = builder.evm().block().gas_limit(); let base_fee = builder.evm().block().basefee(); - let mut executed_txs: Vec = Vec::new(); + let l1_tx_count = self.attributes().transactions.len(); + let mut executed_txs: Vec = Vec::with_capacity(l1_tx_count); // Track gas spent by each transaction for error reporting - let mut gas_spent_by_transactions: Vec = Vec::new(); + let mut gas_spent_by_transactions: Vec = Vec::with_capacity(l1_tx_count); for (tx_idx, tx_with_encoded) in self.attributes().transactions.iter().enumerate() { // The transaction is already recovered in `try_new` via `try_into_recovered()`. @@ -516,7 +517,7 @@ impl MorphPayloadBuilderCtx { info.total_fees += U256::from(effective_tip) * U256::from(gas_used); // Store the transaction bytes for ExecutableL2Data - let mut tx_bytes = Vec::new(); + let mut tx_bytes = Vec::with_capacity(tx.encode_2718_len()); tx.encode_2718(&mut tx_bytes); executed_txs.push(Bytes::from(tx_bytes)); } diff --git a/crates/payload/builder/src/error.rs b/crates/payload/builder/src/error.rs index ac1d14e..3c726ab 100644 --- a/crates/payload/builder/src/error.rs +++ b/crates/payload/builder/src/error.rs @@ -1,6 +1,5 @@ //! Morph payload builder error types. -use alloy_primitives::B256; use reth_evm::execute::ProviderError; use reth_revm::db::bal::EvmDatabaseError; @@ -37,28 +36,10 @@ pub enum MorphPayloadBuilderError { #[error("failed to decode transaction: {0}")] TransactionDecodeError(#[from] alloy_rlp::Error), - /// Invalid L1 message queue index. - #[error("invalid L1 message queue index: expected {expected}, got {actual}")] - InvalidL1MessageQueueIndex { - /// Expected queue index. - expected: u64, - /// Actual queue index. - actual: u64, - }, - /// L1 message appears after regular transaction. #[error("L1 message appears after regular transaction")] L1MessageAfterRegularTx, - /// Invalid transaction hash. - #[error("invalid transaction hash: expected {expected}, got {actual}")] - InvalidTransactionHash { - /// Expected hash. - expected: B256, - /// Actual hash. - actual: B256, - }, - /// Database error when reading contract storage. #[error("database error: {0}")] Database(#[from] EvmDatabaseError), diff --git a/crates/primitives/src/receipt/envelope.rs b/crates/primitives/src/receipt/envelope.rs index a1bd41d..7f5c3d8 100644 --- a/crates/primitives/src/receipt/envelope.rs +++ b/crates/primitives/src/receipt/envelope.rs @@ -82,7 +82,7 @@ impl MorphReceiptEnvelope { /// Returns the success status of the receipt's transaction. pub const fn status(&self) -> bool { - self.as_receipt().unwrap().status.coerce_status() + self.as_receipt().status.coerce_status() } /// Return true if the transaction was successful. @@ -92,12 +92,12 @@ impl MorphReceiptEnvelope { /// Returns the cumulative gas used at this receipt. pub const fn cumulative_gas_used(&self) -> u64 { - self.as_receipt().unwrap().cumulative_gas_used + self.as_receipt().cumulative_gas_used } /// Return the receipt logs. pub fn logs(&self) -> &[T] { - &self.as_receipt().unwrap().logs + &self.as_receipt().logs } /// Return the receipt's bloom. @@ -128,16 +128,15 @@ impl MorphReceiptEnvelope { } } - /// Return the inner receipt. Currently this is infallible, however, future - /// receipt types may be added. - pub const fn as_receipt(&self) -> Option<&Receipt> { + /// Return the inner receipt. + pub const fn as_receipt(&self) -> &Receipt { match self { Self::Legacy(t) | Self::Eip2930(t) | Self::Eip1559(t) | Self::Eip7702(t) | Self::L1Message(t) - | Self::Morph(t) => Some(&t.receipt), + | Self::Morph(t) => &t.receipt, } } } @@ -172,11 +171,11 @@ where type Log = T; fn status_or_post_state(&self) -> Eip658Value { - self.as_receipt().unwrap().status + self.as_receipt().status } fn status(&self) -> bool { - self.as_receipt().unwrap().status.coerce_status() + self.as_receipt().status.coerce_status() } fn bloom(&self) -> Bloom { @@ -188,11 +187,11 @@ where } fn cumulative_gas_used(&self) -> u64 { - self.as_receipt().unwrap().cumulative_gas_used + self.as_receipt().cumulative_gas_used } fn logs(&self) -> &[T] { - &self.as_receipt().unwrap().logs + &self.as_receipt().logs } } diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index c514f75..6c874be 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -100,9 +100,9 @@ impl MorphTxEnvelope { } /// Returns the memo for MorphTx, or `None` for other transaction types. - pub fn memo(&self) -> Option { + pub fn memo(&self) -> Option<&alloy_primitives::Bytes> { match self { - Self::Morph(tx) => tx.tx().memo.clone(), + Self::Morph(tx) => tx.tx().memo.as_ref(), _ => None, } } @@ -135,15 +135,8 @@ impl MorphTxEnvelope { /// Encode the transaction according to [EIP-2718] rules. First a 1-byte /// type flag in the range 0x0-0x7f, then the body of the transaction. pub fn rlp(&self) -> Bytes { - let mut bytes = BytesMut::new(); - match self { - Self::Legacy(tx) => tx.encode_2718(&mut bytes), - Self::Eip2930(tx) => tx.encode_2718(&mut bytes), - Self::Eip1559(tx) => tx.encode_2718(&mut bytes), - Self::Eip7702(tx) => tx.encode_2718(&mut bytes), - Self::L1Msg(tx) => tx.encode_2718(&mut bytes), - Self::Morph(tx) => tx.encode_2718(&mut bytes), - } + let mut bytes = BytesMut::with_capacity(self.encode_2718_len()); + self.encode_2718(&mut bytes); Bytes(bytes.freeze()) } } @@ -378,6 +371,14 @@ mod codec { // For backwards compatibility purposes only 2 bits of the type are encoded in the identifier // parameter. In the case of a [`COMPACT_EXTENDED_IDENTIFIER_FLAG`], the full transaction type // is read from the buffer as a single byte. + /// Decodes `MorphTxType` from its compact (database) representation. + /// + /// # Panics + /// + /// Panics on unknown identifiers. The `Compact` trait is infallible by design + /// (reth convention) — compact encoding is only used for internal DB storage, + /// never for untrusted network data (which goes through RLP and returns `Result`). + /// An unknown identifier here indicates database corruption, which is unrecoverable. fn from_compact(mut buf: &[u8], identifier: usize) -> (Self, &[u8]) { use bytes::Buf; ( @@ -391,10 +392,16 @@ mod codec { EIP7702_TX_TYPE_ID => Self::Eip7702, crate::transaction::L1_TX_TYPE_ID => Self::L1Msg, crate::transaction::MORPH_TX_TYPE_ID => Self::Morph, - _ => panic!("Unsupported TxType identifier: {extended_identifier}"), + _ => panic!( + "Unsupported MorphTxType compact identifier: {extended_identifier} \ + (database corruption — compact encoding is only used for DB storage)" + ), } } - _ => panic!("Unknown identifier for TxType: {identifier}"), + _ => panic!( + "Unknown MorphTxType compact identifier: {identifier} \ + (database corruption — compact encoding is only used for DB storage)" + ), }, buf, ) diff --git a/crates/primitives/src/transaction/morph_transaction.rs b/crates/primitives/src/transaction/morph_transaction.rs index d9eb415..c1e767f 100644 --- a/crates/primitives/src/transaction/morph_transaction.rs +++ b/crates/primitives/src/transaction/morph_transaction.rs @@ -74,8 +74,9 @@ pub struct TxMorph { /// A scalar value equal to the maximum amount of gas that should be used /// in executing this transaction. This is paid up-front, before any /// computation is done and may not be increased later. + /// Matches go-ethereum's `AltFeeTx.Gas` (uint64). #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] - pub gas_limit: u128, + pub gas_limit: u64, /// A scalar value equal to the maximum amount of gas that should be used /// in executing this transaction. This is paid up-front, before any @@ -240,7 +241,7 @@ impl TxMorph { pub fn size(&self) -> usize { mem::size_of::() + // chain_id mem::size_of::() + // nonce - mem::size_of::() + // gas_limit + mem::size_of::() + // gas_limit mem::size_of::() + // max_fee_per_gas mem::size_of::() + // max_priority_fee_per_gas self.to.size() + // to @@ -594,7 +595,7 @@ impl Transaction for TxMorph { } fn gas_limit(&self) -> u64 { - self.gas_limit as u64 + self.gas_limit } fn gas_price(&self) -> Option { @@ -866,73 +867,103 @@ impl reth_primitives_traits::InMemorySize for TxMorph { } #[cfg(feature = "reth-codec")] -impl reth_codecs::Compact for TxMorph { - fn to_compact(&self, buf: &mut B) -> usize - where - B: BufMut + AsMut<[u8]>, - { - let mut len = 0; - len += self.chain_id.to_compact(buf); - len += self.nonce.to_compact(buf); - len += self.gas_limit.to_compact(buf); - len += self.max_fee_per_gas.to_compact(buf); - len += self.max_priority_fee_per_gas.to_compact(buf); - len += self.to.to_compact(buf); - len += self.value.to_compact(buf); - len += self.access_list.to_compact(buf); - len += (self.version as u64).to_compact(buf); - len += (self.fee_token_id as u64).to_compact(buf); - len += self.fee_limit.to_compact(buf); - len += self.reference.to_compact(buf); - // Memo is Option, convert to Bytes for Compact - let memo_bytes = self.memo.clone().unwrap_or_default(); - len += memo_bytes.to_compact(buf); - len += self.input.to_compact(buf); - len - } +mod compact_txmorph { + use super::*; + use alloy_eips::eip2930::AccessList; + use alloy_primitives::{Bytes, ChainId, TxKind, U256}; + use reth_codecs::Compact; - fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { - let (chain_id, buf) = ChainId::from_compact(buf, len); - let (nonce, buf) = u64::from_compact(buf, len); - let (gas_limit, buf) = u128::from_compact(buf, len); - let (max_fee_per_gas, buf) = u128::from_compact(buf, len); - let (max_priority_fee_per_gas, buf) = u128::from_compact(buf, len); - let (to, buf) = TxKind::from_compact(buf, len); - let (value, buf) = U256::from_compact(buf, len); - let (access_list, buf) = AccessList::from_compact(buf, len); - let (version, buf) = u64::from_compact(buf, len); - let (fee_token_id, buf) = u64::from_compact(buf, len); - let (fee_limit, buf) = U256::from_compact(buf, len); - let (reference, buf) = Option::::from_compact(buf, len); - let (memo_bytes, buf) = Bytes::from_compact(buf, len); - let (input, buf) = Bytes::from_compact(buf, len); - - // Convert Bytes to Option (empty = None) - let memo = if memo_bytes.is_empty() { - None - } else { - Some(memo_bytes) - }; + /// Helper struct for deriving `Compact` instead of manually managing bitfields. + /// + /// Follows the same pattern as reth's `TxEip1559` compact helper + /// (see `reth-codecs/src/alloy/transaction/eip1559.rs`). + /// + /// - `version` and `fee_token_id` are stored as `u64` because `u8`/`u16` don't + /// implement `Compact` in reth_codecs. The conversion is lossless. + /// - `memo` and `input` are packed into a single `Bytes` field (`data`) because + /// the derive macro only allows one `Bytes` field and it must be last. + /// Format: `[memo_len: u8][memo_bytes][input_bytes]`. + #[derive(Debug, Clone, PartialEq, Eq, Hash, Compact)] + #[reth_codecs(crate = "reth_codecs")] + struct TxMorphCompact { + chain_id: ChainId, + nonce: u64, + gas_limit: u64, + max_fee_per_gas: u128, + max_priority_fee_per_gas: u128, + to: TxKind, + value: U256, + access_list: AccessList, + /// Stored as u64 for Compact compatibility (u8 doesn't implement Compact) + version: u64, + /// Stored as u64 for Compact compatibility (u16 doesn't implement Compact) + fee_token_id: u64, + fee_limit: U256, + reference: Option, + /// Packed: `[memo_len: u8][memo_bytes][input_bytes]` (must be last) + data: Bytes, + } + + impl Compact for TxMorph { + fn to_compact(&self, buf: &mut B) -> usize + where + B: bytes::BufMut + AsMut<[u8]>, + { + // Pack memo + input into a single Bytes field + let memo_slice = self.memo.as_deref().map_or(&[] as &[u8], |v| v); + let mut data = Vec::with_capacity(1 + memo_slice.len() + self.input.len()); + data.push(memo_slice.len() as u8); // memo max 64 bytes, fits in u8 + data.extend_from_slice(memo_slice); + data.extend_from_slice(&self.input); + + let helper = TxMorphCompact { + chain_id: self.chain_id, + nonce: self.nonce, + gas_limit: self.gas_limit, + max_fee_per_gas: self.max_fee_per_gas, + max_priority_fee_per_gas: self.max_priority_fee_per_gas, + to: self.to, + value: self.value, + access_list: self.access_list.clone(), + version: u64::from(self.version), + fee_token_id: u64::from(self.fee_token_id), + fee_limit: self.fee_limit, + reference: self.reference, + data: data.into(), + }; + helper.to_compact(buf) + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + let (helper, remaining) = TxMorphCompact::from_compact(buf, len); - ( - Self { - chain_id, - nonce, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - to, - value, - access_list, - version: version as u8, - fee_token_id: fee_token_id as u16, - fee_limit, - reference, + // Unpack memo + input from the combined data field + let memo_len = helper.data[0] as usize; + let memo = if memo_len == 0 { + None + } else { + Some(Bytes::copy_from_slice(&helper.data[1..1 + memo_len])) + }; + let input = Bytes::copy_from_slice(&helper.data[1 + memo_len..]); + + let tx = Self { + chain_id: helper.chain_id, + nonce: helper.nonce, + gas_limit: helper.gas_limit, + max_fee_per_gas: helper.max_fee_per_gas, + max_priority_fee_per_gas: helper.max_priority_fee_per_gas, + to: helper.to, + value: helper.value, + access_list: helper.access_list, + version: helper.version as u8, + fee_token_id: helper.fee_token_id as u16, + fee_limit: helper.fee_limit, + reference: helper.reference, memo, input, - }, - buf, - ) + }; + (tx, remaining) + } } } @@ -1877,7 +1908,7 @@ mod tests { 0u64.encode(&mut inner_buf); // nonce 0u128.encode(&mut inner_buf); // max_priority_fee_per_gas 1000u128.encode(&mut inner_buf); // max_fee_per_gas - 21000u128.encode(&mut inner_buf); // gas_limit + 21000u64.encode(&mut inner_buf); // gas_limit alloy_primitives::TxKind::Create.encode(&mut inner_buf); // to U256::ZERO.encode(&mut inner_buf); // value Bytes::new().encode(&mut inner_buf); // input @@ -2070,4 +2101,64 @@ mod tests { assert_eq!(decoded_signed.tx().memo, None); assert!(decoded_signed.tx().is_v0()); } + + #[cfg(feature = "reth-codec")] + #[test] + fn test_compact_roundtrip_v1_with_memo() { + use reth_codecs::Compact; + + let tx = TxMorph { + chain_id: 2818, + nonce: 42, + gas_limit: 21_000, + max_fee_per_gas: 100_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::from(1_000_000_000_000_000_000u128), + access_list: AccessList::default(), + version: 1, + fee_token_id: 7, + fee_limit: U256::from(999u64), + reference: Some(B256::from([0xab; 32])), + memo: Some(Bytes::from(vec![0xca, 0xfe, 0xba, 0xbe])), + input: Bytes::from(vec![0x12, 0x34, 0x56]), + }; + + let mut buf = Vec::new(); + tx.to_compact(&mut buf); + let (decoded, remaining) = TxMorph::from_compact(&buf, buf.len()); + + assert!(remaining.is_empty()); + assert_eq!(tx, decoded); + } + + #[cfg(feature = "reth-codec")] + #[test] + fn test_compact_roundtrip_v0_no_memo() { + use reth_codecs::Compact; + + let tx = TxMorph { + chain_id: 2818, + nonce: 0, + gas_limit: 100_000, + max_fee_per_gas: 50_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + to: TxKind::Create, + value: U256::ZERO, + access_list: AccessList::default(), + version: 0, + fee_token_id: 1, + fee_limit: U256::from(500u64), + reference: None, + memo: None, + input: Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), + }; + + let mut buf = Vec::new(); + tx.to_compact(&mut buf); + let (decoded, remaining) = TxMorph::from_compact(&buf, buf.len()); + + assert!(remaining.is_empty()); + assert_eq!(tx, decoded); + } } diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index 2cafdbb..86295bb 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -25,7 +25,6 @@ alloy-evm.workspace = true alloy-primitives.workspace = true alloy-consensus.workspace = true alloy-sol-types.workspace = true -alloy-rlp.workspace = true alloy-eips.workspace = true auto_impl.workspace = true diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index 4a311f5..340224f 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -1,24 +1,37 @@ -use crate::{MorphBlockEnv, MorphTxEnv, precompiles::MorphPrecompiles}; +use crate::{ + MorphBlockEnv, MorphTxEnv, precompiles::MorphPrecompiles, runtime::MorphTxRuntime, + token_fee::TokenFeeInfo, +}; use alloy_evm::Database; -use alloy_primitives::{Log, U256, keccak256}; +use alloy_primitives::{U256, keccak256}; use morph_chainspec::hardfork::MorphHardfork; use revm::{ Context, Inspector, - context::{CfgEnv, ContextError, Evm, FrameStack}, - context_interface::{cfg::gas::BLOCKHASH, host::LoadError}, + context::{CfgEnv, ContextError, Evm, FrameStack, Journal}, + context_interface::{ + cfg::gas::{BLOCKHASH, WARM_STORAGE_READ_COST}, + host::LoadError, + }, handler::{ EthFrame, EvmTr, FrameInitOrResult, FrameTr, ItemOrResult, instructions::EthInstructions, }, inspector::InspectorEvmTr, interpreter::{ - Host, Instruction, InstructionContext, interpreter::EthInterpreter, - interpreter_types::StackTr, + Host, Instruction, InstructionContext, InstructionResult, + interpreter::EthInterpreter, + interpreter_types::{RuntimeFlag, StackTr}, + }, + primitives::{ + BLOCK_HASH_HISTORY, + hardfork::SpecId::{BERLIN, ISTANBUL}, }, - primitives::BLOCK_HASH_HISTORY, }; /// The Morph EVM context type. -pub type MorphContext = Context, DB>; +/// +/// Uses [`MorphTxRuntime`] as the extra per-transaction runtime state payload. +pub type MorphContext = + Context, DB, Journal, MorphTxRuntime>; #[inline] fn as_u64_saturated(value: U256) -> u64 { @@ -50,105 +63,162 @@ fn morph_blockhash_result(chain_id: u64, current_number: u64, requested_number: } } -/// Morph custom SLOAD opcode. -/// -/// This wraps the standard SLOAD logic and adds a fix for the revm-state 9.0.0 -/// `mark_warm_with_transaction_id` behavior change. -/// -/// ## Background -/// -/// In revm-state 9.0.0, `EvmStorageSlot::mark_warm_with_transaction_id` resets -/// `original_value = present_value` when a cold slot becomes warm. This is -/// semantically correct for standard per-transaction state, but breaks Morph's -/// token fee mechanism where storage slots are modified *before* the main -/// transaction and then marked cold. -/// -/// When the main transaction SLOADs such a slot, the cold→warm transition -/// resets `original_value`, making the slot appear "clean" (original == present). -/// A subsequent SSTORE then uses EIP-2200 "clean" pricing (2900 gas) instead of -/// "dirty" pricing (100 gas), creating a **2800 gas** discrepancy vs go-ethereum. +/// Morph custom BLOCKHASH opcode. /// -/// ## Fix +/// Morph geth does not read historical header hashes for BLOCKHASH. Instead it returns: +/// `keccak256(chain_id(8-byte big-endian) || block_number(8-byte big-endian))` +/// for numbers within the 256-block lookup window. +fn blockhash_morph( + context: InstructionContext<'_, MorphContext, EthInterpreter>, +) { + let Some(([], number)) = StackTr::popn_top::<0>(&mut context.interpreter.stack) else { + context.interpreter.halt_underflow(); + return; + }; + + let requested_number_u64 = as_u64_saturated(*number); + let current_number_u64 = as_u64_saturated(context.host.block_number()); + let chain_id_u64 = as_u64_saturated(context.host.chain_id()); + + *number = morph_blockhash_result(chain_id_u64, current_number_u64, requested_number_u64); +} + +#[inline] +fn restore_tracked_original_value( + context: &mut InstructionContext<'_, MorphContext, EthInterpreter>, + address: alloy_primitives::Address, + slot: U256, +) -> Option { + context.host.chain.restore_tracked_original_value( + &mut context.host.journaled_state.inner.state, + address, + slot, + ) +} + +/// Morph custom SLOAD opcode. /// -/// After the standard SLOAD completes (including `mark_warm_with_transaction_id`), -/// this instruction checks whether the loaded slot is one of the fee-deducted -/// slots whose original DB value was saved in `MorphTxEnv::fee_slot_original_values`. -/// If so, it restores `original_value` on the `EvmStorageSlot` in journal state -/// to the true DB value, so that SSTORE gas calculation sees the correct -/// dirty/clean status. -fn sload_morph(context: InstructionContext<'_, MorphContext, EthInterpreter>) { - let Some(([], index)) = StackTr::popn_top::<0>(&mut context.interpreter.stack) else { +/// Restores tx-original values for forced-cold slots after revm warms them. +fn sload_morph( + mut context: InstructionContext<'_, MorphContext, EthInterpreter>, +) { + let Some([key]) = StackTr::popn::<1>(&mut context.interpreter.stack) else { context.interpreter.halt_underflow(); return; }; let target = context.interpreter.input.target_address; - let key = *index; - // Berlin+ path (Morph is always post-Berlin). - // Charge WARM_STORAGE_READ_COST (100) as static gas via the Instruction wrapper, - // then charge the additional cold cost (2000) here if the slot is cold. let additional_cold_cost = context.host.gas_params().cold_storage_additional_cost(); let skip_cold = context.interpreter.gas.remaining() < additional_cold_cost; + let res = context.host.sload_skip_cold_load(target, key, skip_cold); - match context.host.sload_skip_cold_load(target, key, skip_cold) { + match res { Ok(storage) => { - if storage.is_cold && !context.interpreter.gas.record_cost(additional_cold_cost) { - context.interpreter.halt_oog(); - return; + if storage.is_cold { + let _ = restore_tracked_original_value(&mut context, target, key); + if !context.interpreter.gas.record_cost(additional_cold_cost) { + context.interpreter.halt_oog(); + return; + } } - *index = storage.data; - } - Err(LoadError::ColdLoadSkipped) => { - context.interpreter.halt_oog(); - return; - } - Err(LoadError::DBError) => { - context.interpreter.halt_fatal(); - return; - } - } - // Morph fix: restore original_value for slots modified by token fee deduction. - // After mark_warm_with_transaction_id reset original_value = present_value, - // we set it back to the true DB value so SSTORE sees the slot as dirty. - // - // We use `.iter().find()` instead of `.remove()` so the entry is kept for the - // lifetime of the transaction. This is revert-safe: if a sub-call REVERTs, - // the journal rolls the slot back to cold, and a later SLOAD to the same slot - // would corrupt original_value again — the kept entry ensures every cold→warm - // transition is corrected, not just the first one. - if let Some(&(_, _, original_db_value)) = context - .host - .tx - .fee_slot_original_values - .iter() - .find(|(addr, slot_key, _)| *addr == target && *slot_key == key) - && let Some(acc) = context.host.journaled_state.state.get_mut(&target) - && let Some(slot) = acc.storage.get_mut(&key) - { - slot.original_value = original_db_value; + let _ = context.interpreter.stack.push(storage.data); + } + Err(LoadError::ColdLoadSkipped) => context.interpreter.halt_oog(), + Err(LoadError::DBError) => context.interpreter.halt_fatal(), } } -/// Morph custom BLOCKHASH opcode. +/// Morph custom SSTORE opcode. /// -/// Morph geth does not read historical header hashes for BLOCKHASH. Instead it returns: -/// `keccak256(chain_id(8-byte big-endian) || block_number(8-byte big-endian))` -/// for numbers within the 256-block lookup window. -fn blockhash_morph( - context: InstructionContext<'_, MorphContext, EthInterpreter>, +/// revm's standard SSTORE warms a cold slot through the same path as SLOAD, so +/// forced-cold token-fee slots need the same tx-original restoration before gas +/// accounting uses `SStoreResult::original_value`. +fn sstore_morph( + mut context: InstructionContext<'_, MorphContext, EthInterpreter>, ) { - let Some(([], number)) = StackTr::popn_top::<0>(&mut context.interpreter.stack) else { + if context.interpreter.runtime_flag.is_static() { + context + .interpreter + .halt(InstructionResult::StateChangeDuringStaticCall); + return; + } + + let Some([index, value]) = StackTr::popn::<2>(&mut context.interpreter.stack) else { context.interpreter.halt_underflow(); return; }; - let requested_number_u64 = as_u64_saturated(*number); - let current_number_u64 = as_u64_saturated(context.host.block_number()); - let chain_id_u64 = as_u64_saturated(context.host.chain_id()); + let target = context.interpreter.input.target_address; + let spec_id = context.interpreter.runtime_flag.spec_id(); - *number = morph_blockhash_result(chain_id_u64, current_number_u64, requested_number_u64); + if spec_id.is_enabled_in(ISTANBUL) + && context.interpreter.gas.remaining() <= context.host.gas_params().call_stipend() + { + context + .interpreter + .halt(InstructionResult::ReentrancySentryOOG); + return; + } + + if !context + .interpreter + .gas + .record_cost(context.host.gas_params().sstore_static_gas()) + { + context.interpreter.halt_oog(); + return; + } + + let mut state_load = if spec_id.is_enabled_in(BERLIN) { + let additional_cold_cost = context.host.gas_params().cold_storage_additional_cost(); + let skip_cold = context.interpreter.gas.remaining() < additional_cold_cost; + match context + .host + .sstore_skip_cold_load(target, index, value, skip_cold) + { + Ok(load) => load, + Err(LoadError::ColdLoadSkipped) => { + context.interpreter.halt_oog(); + return; + } + Err(LoadError::DBError) => { + context.interpreter.halt_fatal(); + return; + } + } + } else { + let Some(load) = context.host.sstore(target, index, value) else { + context.interpreter.halt_fatal(); + return; + }; + load + }; + + if state_load.is_cold + && let Some(original_value) = restore_tracked_original_value(&mut context, target, index) + { + state_load.data.original_value = original_value; + } + + let is_istanbul = spec_id.is_enabled_in(ISTANBUL); + let dynamic_gas = context.host.gas_params().sstore_dynamic_gas( + is_istanbul, + &state_load.data, + state_load.is_cold, + ); + if !context.interpreter.gas.record_cost(dynamic_gas) { + context.interpreter.halt_oog(); + return; + } + + context.interpreter.gas.record_refund( + context + .host + .gas_params() + .sstore_refund(is_istanbul, &state_load.data), + ); } /// MorphEvm extends the Evm with Morph specific types and logic. @@ -165,8 +235,25 @@ pub struct MorphEvm { MorphPrecompiles, EthFrame, >, - /// Preserved logs from the last transaction - pub logs: Vec, + /// Cached token fee info from the validation/deduction phase. + /// Ensures consistent price_ratio/scale between deduct and reimburse, + /// matching go-ethereum's `st.feeRate`/`st.tokenScale` caching pattern. + pub(crate) cached_token_fee_info: Option, + /// Cached L1 data fee calculated during handler validation. + /// Avoids re-encoding the full transaction RLP in the block executor's + /// receipt-building path (the handler already has the encoded bytes via + /// `MorphTxEnv.rlp_bytes`). + pub(crate) cached_l1_data_fee: U256, + /// Transfer event logs from token fee deduction (pre-execution phase). + /// + /// In go-ethereum, `buyAltTokenGas()` emits Transfer events into `StateDB.logs` + /// which is independent of the state snapshot/revert mechanism — logs survive + /// regardless of main tx result. revm's `ExecutionResult::Revert` has no `logs` + /// field, so we cache fee-related logs separately from the journal and merge + /// them into the receipt in the block executor. + pub(crate) pre_fee_logs: Vec, + /// Transfer event logs from token fee reimbursement (post-execution phase). + pub(crate) post_fee_logs: Vec, } impl MorphEvm { @@ -180,18 +267,14 @@ impl MorphEvm { let precompiles = MorphPrecompiles::new_with_spec(spec); let mut instructions = EthInstructions::new_mainnet(); - // Morph custom SLOAD: restores original_value after revm-state 9.0.0's - // mark_warm_with_transaction_id resets it, fixing EIP-2200 gas for - // token fee deducted slots. Static gas = WARM_STORAGE_READ_COST (100). - instructions.insert_instruction( - 0x54, // SLOAD - Instruction::new( - sload_morph::, - revm::context_interface::cfg::gas::WARM_STORAGE_READ_COST, - ), - ); // Morph custom BLOCKHASH implementation (matches Morph geth). instructions.insert_instruction(0x40, Instruction::new(blockhash_morph::, BLOCKHASH)); + // Morph custom SLOAD: fixes original_value corruption from token fee deduction. + instructions.insert_instruction( + 0x54, + Instruction::new(sload_morph::, WARM_STORAGE_READ_COST), + ); + instructions.insert_instruction(0x55, Instruction::new(sstore_morph::, 0)); // SELFDESTRUCT is disabled in Morph instructions.insert_instruction(0xff, Instruction::unknown()); // BLOBHASH is disabled in Morph @@ -207,7 +290,6 @@ impl MorphEvm { }) } - /// Inner helper function to create a new Morph EVM with empty logs. #[inline] #[expect(clippy::type_complexity)] fn new_inner( @@ -221,13 +303,16 @@ impl MorphEvm { ) -> Self { Self { inner, - logs: Vec::new(), + cached_token_fee_info: None, + cached_l1_data_fee: U256::ZERO, + pre_fee_logs: Vec::new(), + post_fee_logs: Vec::new(), } } } impl MorphEvm { - /// Consumed self and returns a new Evm type with given Inspector. + /// Consumes self and returns a new Evm type with given Inspector. pub fn with_inspector(self, inspector: OINSP) -> MorphEvm { MorphEvm::new_inner(self.inner.with_inspector(inspector)) } @@ -242,10 +327,36 @@ impl MorphEvm { self.inner.into_inspector() } - /// Take logs from the EVM. + /// Returns the cached token fee info set during handler validation. + /// + /// The cache is populated by `validate_and_deduct_token_fee` and persists + /// through the handler lifecycle so that post-execution code (e.g., the + /// block executor's receipt builder) can reuse it without re-reading the DB. + #[inline] + pub fn cached_token_fee_info(&self) -> Option { + self.cached_token_fee_info + } + + /// Returns the L1 data fee cached during handler validation. + /// + /// Set in `validate_and_deduct_eth_fee` / `validate_and_deduct_token_fee` and + /// reused by `reward_beneficiary` and the block executor's receipt builder, + /// avoiding redundant `calculate_tx_l1_cost` calls and RLP re-encoding. + #[inline] + pub fn cached_l1_data_fee(&self) -> U256 { + self.cached_l1_data_fee + } + + /// Takes the cached pre-execution fee logs (token fee deduction Transfer events). + #[inline] + pub fn take_pre_fee_logs(&mut self) -> Vec { + std::mem::take(&mut self.pre_fee_logs) + } + + /// Takes the cached post-execution fee logs (token fee reimbursement Transfer events). #[inline] - pub fn take_logs(&mut self) -> Vec { - std::mem::take(&mut self.logs) + pub fn take_post_fee_logs(&mut self) -> Vec { + std::mem::take(&mut self.post_fee_logs) } } diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index cdb6c92..5ce44b0 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -2,7 +2,7 @@ use alloy_primitives::{Address, Bytes, U256}; use revm::{ - ExecuteEvm, SystemCallEvm, + ExecuteEvm, context::{ Cfg, ContextTr, JournalTr, Transaction, result::{EVMError, ExecutionResult, InvalidTransaction}, @@ -14,11 +14,11 @@ use revm::{ }; use crate::{ - MorphEvm, MorphInvalidTransaction, + MorphEvm, MorphInvalidTransaction, MorphTxEnv, error::MorphHaltReason, evm::MorphContext, l1block::L1BlockInfo, - token_fee::{TokenFeeInfo, compute_mapping_slot_for_address}, + token_fee::{TokenFeeInfo, compute_mapping_slot_for_address, encode_balance_of_calldata}, tx::MorphTxExt, }; @@ -75,11 +75,6 @@ where evm: &mut Self::Evm, result: <::Frame as FrameTr>::FrameResult, ) -> Result, Self::Error> { - evm.logs.clear(); - if !result.instruction_result().is_ok() { - evm.logs = evm.journal_mut().take_logs(); - } - MainnetHandler::default() .execution_result(evm, result) .map(|result| result.map_haltreason(Into::into)) @@ -95,14 +90,19 @@ where &self, evm: &mut Self::Evm, ) -> Result<(), Self::Error> { - let (_, tx, _, journal, _, _) = evm.ctx().all_mut(); + // Reset per-transaction caches from the previous iteration. + evm.cached_l1_data_fee = U256::ZERO; + evm.cached_token_fee_info = None; + evm.pre_fee_logs.clear(); + evm.post_fee_logs.clear(); + + let (_, tx, _, journal, chain, _) = evm.ctx().all_mut(); + chain.reset(); - // L1 message - skip fee validation if tx.is_l1_msg() { - // Load caller's account let mut caller = journal.load_account_with_code_mut(tx.caller())?.data; - // Bump nonce for calls (CREATE nonce is bumped in make_create_frame) + // CREATE nonce is bumped later in make_create_frame if tx.kind().is_call() { caller.bump_nonce(); } @@ -129,7 +129,7 @@ where ) -> Result<(), Self::Error> { let (_, tx, _, _, _, _) = evm.ctx().all_mut(); - // For L1 message transactions & system transactions, no reimbursement is needed + // L1 message gas is prepaid on L1, no reimbursement needed. if tx.is_l1_msg() { return Ok(()); } @@ -138,7 +138,12 @@ where if tx.is_morph_tx() { let token_id = tx.fee_token_id.unwrap_or_default(); if token_id > 0 { - return self.reimburse_caller_token_fee(evm, exec_result.gas(), token_id); + // When fee charge was disabled (eth_call), no token was deducted and + // cached_token_fee_info was not set — skip reimbursement entirely. + if evm.cached_token_fee_info.is_none() { + return Ok(()); + } + return self.reimburse_caller_token_fee(evm, exec_result.gas()); } // fee_token_id == 0 follows standard ETH reimbursement flow post_execution::reimburse_caller(evm.ctx(), exec_result.gas(), U256::ZERO)?; @@ -175,7 +180,12 @@ where evm: &mut Self::Evm, exec_result: &mut <::Frame as FrameTr>::FrameResult, ) -> Result<(), Self::Error> { - let (block, tx, cfg, journal, _, _) = evm.ctx().all_mut(); + // Reuse the L1 data fee cached during validate_and_deduct_eth_fee / + // validate_and_deduct_token_fee, avoiding a redundant calculate_tx_l1_cost call. + // Read before ctx().all_mut() borrows evm. + let l1_data_fee = evm.cached_l1_data_fee; + + let (block, tx, _, journal, _, _) = evm.ctx().all_mut(); // L1 messages skip all reward. // Token-fee MorphTx rewards are already applied when token fee is deducted. @@ -188,28 +198,10 @@ where let basefee = block.basefee() as u128; let effective_gas_price = tx.effective_gas_price(basefee); - // Get the current hardfork for L1 fee calculation - let hardfork = cfg.spec(); - - // Fetch L1 block info from the L1 Gas Price Oracle contract - let l1_block_info = L1BlockInfo::try_fetch(journal.db_mut(), *hardfork)?; - - // Get RLP-encoded transaction bytes for L1 fee calculation - // This represents the full transaction data posted to L1 for data availability - let rlp_bytes = tx - .rlp_bytes - .as_ref() - .map(|b| b.as_ref()) - .unwrap_or_default(); - - // Calculate L1 data fee based on full RLP-encoded transaction - let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, *hardfork); - let gas_used = exec_result.gas().used(); let execution_fee = U256::from(effective_gas_price).saturating_mul(U256::from(gas_used)); - // reward beneficiary let mut beneficiary_account = journal.load_account_mut(beneficiary)?; beneficiary_account .data @@ -323,18 +315,20 @@ where DB: alloy_evm::Database, { /// Validate and deduct ETH-based gas fees. + #[inline] fn validate_and_deduct_eth_fee( &self, evm: &mut MorphEvm, ) -> Result<(), EVMError> { - // Get the current hardfork for L1 fee calculation let hardfork = *evm.ctx_ref().cfg().spec(); - // Fetch L1 block info from the L1 Gas Price Oracle contract + // Fetch L1 block info from the L1 Gas Price Oracle contract per-tx. + // Must NOT use a per-block cache because the oracle can be updated by a + // regular transaction (from the external gas-oracle service) within the + // same block. Subsequent user txs must see the updated fee parameters, + // matching go-ethereum's per-tx L1BlockInfo read. let l1_block_info = L1BlockInfo::try_fetch(evm.ctx_mut().db_mut(), hardfork)?; - // Get RLP-encoded transaction bytes for L1 fee calculation - // This represents the full transaction data posted to L1 for data availability let rlp_bytes = evm .ctx_ref() .tx() @@ -343,16 +337,13 @@ where .map(|b| b.as_ref()) .unwrap_or_default(); - // Calculate L1 data fee based on full RLP-encoded transaction let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, hardfork); + evm.cached_l1_data_fee = l1_data_fee; - // Get mutable access to context components let (block, tx, cfg, journal, _, _) = evm.ctx().all_mut(); - // Load caller's account let mut caller = journal.load_account_with_code_mut(tx.caller())?.data; - // Validate account nonce and code (EIP-3607) pre_execution::validate_account_nonce_and_code( &caller.account().info, tx.nonce(), @@ -360,15 +351,12 @@ where cfg.is_nonce_check_disabled(), )?; - // Calculate L2 fee and validate balance - // This includes: gas_limit * gas_price + value + blob_fee let new_balance_after_l2_fee = calculate_caller_fee_with_l1_cost(*caller.balance(), tx, block, cfg, l1_data_fee)?; - // Set the new balance (deducting L2 fee + L1 data fee) caller.set_balance(new_balance_after_l2_fee); - // Bump nonce for calls (CREATE nonce is bumped in make_create_frame) + // CREATE nonce is bumped later in make_create_frame if tx.kind().is_call() { caller.bump_nonce(); } @@ -376,67 +364,97 @@ where Ok(()) } - /// Validate and deduct token-based gas fees. + /// Reimburse unused gas fees in ERC20 tokens. /// - /// This handles gas payment using ERC20 tokens instead of ETH. + /// Uses the cached `TokenFeeInfo` from the deduction phase to ensure + /// consistent price_ratio/scale, matching go-ethereum's `st.feeRate`/`st.tokenScale`. + #[inline] fn reimburse_caller_token_fee( &self, evm: &mut MorphEvm, gas: &Gas, - token_id: u16, ) -> Result<(), EVMError> { - // Get caller address let caller = evm.ctx_ref().tx().caller(); - // Get coinbase address let beneficiary = evm.ctx_ref().block().beneficiary(); let basefee = evm.ctx.block().basefee() as u128; let effective_gas_price = evm.ctx.tx().effective_gas_price(basefee); + let refunded = gas.refunded().max(0) as u64; let reimburse_eth = U256::from( - effective_gas_price.saturating_mul((gas.remaining() + gas.refunded() as u64) as u128), + effective_gas_price.saturating_mul(gas.remaining().saturating_add(refunded) as u128), ); if reimburse_eth.is_zero() { return Ok(()); } - // Fetch token fee info from Token Registry - let spec = *evm.ctx_ref().cfg().spec(); + // Use cached token fee info from the deduction phase (set in validate_and_deduct_token_fee). + // This ensures the same price_ratio/scale is used for both deduction and reimbursement. + // The cache is kept populated (not taken) so the block executor's receipt builder + // can also read it without re-querying the DB. let token_fee_info = - TokenFeeInfo::load_for_caller(evm.ctx_mut().db_mut(), token_id, caller, spec)? - .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; - - // Check if token is active - if !token_fee_info.is_active { - return Err(MorphInvalidTransaction::TokenNotActive(token_id).into()); - } + evm.cached_token_fee_info + .ok_or(MorphInvalidTransaction::TokenTransferFailed { + reason: "cached_token_fee_info not set by validate_and_deduct_token_fee".into(), + })?; // Calculate token amount required for total fee let token_amount_required = token_fee_info.eth_to_token_amount(reimburse_eth); - // Get mutable access to journal components - let journal = evm.ctx().journal_mut(); - - if let Some(balance_slot) = token_fee_info.balance_slot { - // Transfer with token slot. - let _ = transfer_erc20_with_slot( + // Attempt token refund. Matches go-ethereum's refundGas() which silently logs + // and continues on failure: "Continue execution even if refund fails - refund + // should not cause transaction to fail" (state_transition.go:698). + let refund_result = if let Some(balance_slot) = token_fee_info.balance_slot { + let (_, _, _, journal, runtime, _) = evm.ctx().all_mut(); + let from_storage_slot = compute_mapping_slot_for_address(balance_slot, beneficiary); + let to_storage_slot = compute_mapping_slot_for_address(balance_slot, caller); + let result = transfer_erc20_with_slot( journal, beneficiary, caller, token_fee_info.token_address, token_amount_required, balance_slot, - )?; + ) + .map(|_| ()); + restore_tracked_original_values( + runtime, + &mut journal.state, + token_fee_info.token_address, + [from_storage_slot, to_storage_slot], + ); + result } else { - // Transfer with evm call. - transfer_erc20_with_evm( + // Cache refund Transfer logs separately, matching the pre_fee_logs + // pattern from validate_and_deduct_token_fee. + let log_count_before = evm.ctx_mut().journal_mut().logs.len(); + let result = transfer_erc20_with_evm( evm, beneficiary, - token_fee_info.caller, + caller, token_fee_info.token_address, token_amount_required, - )?; + None, + ); + let refund_logs: Vec<_> = evm + .ctx_mut() + .journal_mut() + .logs + .drain(log_count_before..) + .collect(); + evm.post_fee_logs = refund_logs; + result + }; + + if let Err(err) = refund_result { + tracing::error!( + target: "morph::evm", + token_id = ?evm.ctx_ref().tx().fee_token_id, + %err, + "failed to refund alt token gas, continuing execution" + ); } + Ok(()) } @@ -448,48 +466,102 @@ where evm: &mut MorphEvm, token_id: u16, ) -> Result<(), EVMError> { - // Token ID 0 not supported for gas payment. + // Token ID 0 means ETH — routed through validate_and_deduct_eth_fee instead. if token_id == 0 { return Err(MorphInvalidTransaction::TokenIdZeroNotSupported.into()); } - let (block, tx, cfg, journal, _, _) = evm.ctx_mut().all_mut(); + { + let (_, tx, cfg, journal, _, _) = evm.ctx_mut().all_mut(); + let caller_addr = tx.caller(); + let nonce = tx.nonce(); + + // Validate account nonce and code (EIP-3607) BEFORE any state mutations, + // matching the order used in validate_and_deduct_eth_fee. + let caller = journal.load_account_with_code_mut(caller_addr)?.data; + pre_execution::validate_account_nonce_and_code( + &caller.account().info, + nonce, + cfg.is_eip3607_disabled(), + cfg.is_nonce_check_disabled(), + )?; + } - // Get caller address - let caller_addr = tx.caller(); - // Get coinbase address - let beneficiary = block.beneficiary(); - // Get the current hardfork for L1 fee calculation - let hardfork = *cfg.spec(); + let caller_addr = evm.ctx_ref().tx().caller(); + let is_call = evm.ctx_ref().tx().kind().is_call(); + + // eth_call (disable_fee_charge): skip token fee deduction entirely. + // Only nonce/code validation (above) and nonce bump are needed. + // This matches the ETH path's disable_fee_charge semantics and ensures + // eth_call is a pure simulation without token registry lookups, balance + // checks, or ERC20 transfers. + if evm.ctx_ref().cfg().is_fee_charge_disabled() { + if is_call { + let mut caller = evm + .ctx_mut() + .journal_mut() + .load_account_with_code_mut(caller_addr)? + .data; + caller.bump_nonce(); + } + return Ok(()); + } + + let beneficiary = evm.ctx_ref().block().beneficiary(); + let hardfork = *evm.ctx_ref().cfg().spec(); + let tx_value = evm.ctx_ref().tx().value(); + let rlp_bytes = evm.ctx_ref().tx().rlp_bytes.clone().unwrap_or_default(); + let gas_limit = evm.ctx_ref().tx().gas_limit(); + let fee_limit_from_tx = evm.ctx_ref().tx().fee_limit.unwrap_or_default(); + let basefee = evm.ctx_ref().block().basefee() as u128; + let effective_gas_price = evm.ctx_ref().tx().effective_gas_price(basefee); + + // Check that caller has enough ETH to cover the value transfer. + // This matches go-ethereum's buyAltTokenGas() which checks + // state.GetBalance(from) >= value before proceeding. + // Without this, the tx would proceed to EVM execution and fail there + // (consuming gas), whereas go-ethereum rejects at the preCheck stage + // (not consuming gas). + if !tx_value.is_zero() { + let caller_eth_balance = *evm + .ctx_mut() + .journal_mut() + .load_account_mut(caller_addr)? + .data + .balance(); + if caller_eth_balance < tx_value { + return Err(MorphInvalidTransaction::EthInvalidTransaction( + InvalidTransaction::LackOfFundForMaxFee { + fee: Box::new(tx_value), + balance: Box::new(caller_eth_balance), + }, + ) + .into()); + } + } // Fetch token fee info from Token Registry - let token_fee_info = - TokenFeeInfo::load_for_caller(journal.db_mut(), token_id, caller_addr, hardfork)? - .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; + let token_fee_info = TokenFeeInfo::load_for_caller( + evm.ctx_mut().journal_mut().db_mut(), + token_id, + caller_addr, + hardfork, + )? + .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; - // Check if token is active if !token_fee_info.is_active { return Err(MorphInvalidTransaction::TokenNotActive(token_id).into()); } - // Fetch L1 block info from the L1 Gas Price Oracle contract - let l1_block_info = L1BlockInfo::try_fetch(journal.db_mut(), hardfork)?; - // Get RLP-encoded transaction bytes for L1 fee calculation - // This represents the full transaction data posted to L1 for data availability - let rlp_bytes = tx - .rlp_bytes - .as_ref() - .map(|b| b.as_ref()) - .unwrap_or_default(); - - // Calculate L1 data fee (in ETH) based on full RLP-encoded transaction - let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, hardfork); + // Fetch L1 block info per-tx (same rationale as validate_and_deduct_eth_fee). + let l1_block_info = L1BlockInfo::try_fetch(evm.ctx_mut().journal_mut().db_mut(), hardfork)?; + let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes.as_ref(), hardfork); - // Calculate L2 gas fee (in ETH) - let gas_limit = tx.gas_limit(); - let gas_price = tx.gas_price(); - let l2_gas_fee = U256::from(gas_limit).saturating_mul(U256::from(gas_price)); + // Calculate L2 gas fee using effective_gas_price (= min(gasTipCap + baseFee, gasFeeCap)), + // matching go-ethereum's buyAltTokenGas() which uses st.gasPrice (effective gas price). + // tx.gas_price() returns max_fee_per_gas and would overcharge when tip + basefee < feeCap. + let l2_gas_fee = U256::from(gas_limit).saturating_mul(U256::from(effective_gas_price)); // Total fee in ETH let total_eth_fee = l2_gas_fee.saturating_add(l1_data_fee); @@ -498,7 +570,7 @@ where let token_amount_required = token_fee_info.eth_to_token_amount(total_eth_fee); // Determine fee limit - let mut fee_limit = tx.fee_limit.unwrap_or_default(); + let mut fee_limit = fee_limit_from_tx; if fee_limit.is_zero() || fee_limit > token_fee_info.balance { fee_limit = token_fee_info.balance } @@ -512,19 +584,11 @@ where .into()); } - // Collect original DB values for fee-deducted slots (revm-state 9.0.0 workaround). - // - // revm-state 9.0.0's `mark_warm_with_transaction_id` resets - // `original_value = present_value` when a cold slot becomes warm. This - // breaks EIP-2200 gas for slots modified during pre-tx fee deduction. - // We save the true DB values here; the custom SLOAD instruction in - // `sload_morph` restores them after the cold→warm transition. - let mut fee_slot_saves: Vec<(Address, U256, U256)> = Vec::new(); - if let Some(balance_slot) = token_fee_info.balance_slot { // Transfer with token slot. // Ensure token account is loaded into the journal state, because `sload`/`sstore` // assume the account is present. + let (_, _, _, journal, chain, _) = evm.ctx_mut().all_mut(); let _ = journal.load_account_mut(token_fee_info.token_address)?; journal.touch(token_fee_info.token_address); let (from_storage_slot, to_storage_slot) = transfer_erc20_with_slot( @@ -540,40 +604,50 @@ where if let Some(token_acc) = journal.state.get_mut(&token_fee_info.token_address) { token_acc.mark_cold(); if let Some(slot) = token_acc.storage.get_mut(&from_storage_slot) { - fee_slot_saves.push(( + chain.track_forced_cold_slot( token_fee_info.token_address, from_storage_slot, slot.original_value, - )); + ); slot.mark_cold(); } if let Some(slot) = token_acc.storage.get_mut(&to_storage_slot) { - fee_slot_saves.push(( + chain.track_forced_cold_slot( token_fee_info.token_address, to_storage_slot, slot.original_value, - )); + ); slot.mark_cold(); } } } else { - // Transfer with evm call. + // Transfer with evm call (from=caller, balance known from token registry). transfer_erc20_with_evm( evm, - token_fee_info.caller, + caller_addr, beneficiary, token_fee_info.token_address, token_amount_required, + Some(token_fee_info.balance), )?; + // Cache fee Transfer logs separately from the journal. + // + // go-ethereum's StateDB.logs is independent of the state snapshot/revert + // mechanism — fee logs survive regardless of main tx result. revm's + // ExecutionResult::Revert has no logs field, so we keep fee logs out of + // the handler pipeline entirely and merge them in the receipt builder. + evm.pre_fee_logs = std::mem::take(&mut evm.ctx_mut().journal_mut().logs); + // State changes should be marked cold to avoid warm access in the main tx execution. - // Also save original_value for changed slots (see workaround above). + // finalize() clears journal state (including logs, which we already took above). let mut state = evm.finalize(); + let (_, _, _, _, chain, _) = evm.ctx_mut().all_mut(); state.iter_mut().for_each(|(addr, acc)| { acc.mark_cold(); acc.storage.iter_mut().for_each(|(key, slot)| { if slot.original_value != slot.present_value { - fee_slot_saves.push((*addr, *key, slot.original_value)); + chain.track_forced_cold_slot(*addr, *key, slot.original_value); } slot.mark_cold(); }); @@ -581,41 +655,28 @@ where evm.ctx_mut().journal_mut().state.extend(state); } - // Store the saved original values in the tx env. We access the `tx` field - // directly because `ContextTr::all_mut()` returns tx as `&Self::Tx` (immutable). - if !fee_slot_saves.is_empty() { - evm.inner.ctx.tx.fee_slot_original_values = fee_slot_saves; - } - - let (_, tx, cfg, journal, _, _) = evm.ctx().all_mut(); - - // Extract the required tx fields (Copy) before mutating accounts. - let caller_addr = tx.caller(); - let nonce = tx.nonce(); - let is_call = tx.kind().is_call(); - - // Load caller's account for nonce/code validation - let mut caller = journal.load_account_with_code_mut(caller_addr)?.data; - - // Validate account nonce and code (EIP-3607) - pre_execution::validate_account_nonce_and_code( - &caller.account().info, - nonce, - cfg.is_eip3607_disabled(), - cfg.is_nonce_check_disabled(), - )?; - - // Bump nonce for calls (CREATE nonce is bumped in make_create_frame) + // CREATE nonce is bumped later in make_create_frame if is_call { + let mut caller = evm + .ctx_mut() + .journal_mut() + .load_account_with_code_mut(caller_addr)? + .data; caller.bump_nonce(); } + // Cache token fee info for the reimburse phase, ensuring consistent + // price_ratio/scale between deduction and reimbursement. + evm.cached_token_fee_info = Some(token_fee_info); + evm.cached_l1_data_fee = l1_data_fee; + Ok(()) } } /// Performs an ERC20 balance transfer by directly `sload`/`sstore`-ing the token contract storage /// using the known `balance` mapping base slot, returning the computed storage slots for `from`/`to`. +#[inline] fn transfer_erc20_with_slot( journal: &mut revm::Journal, from: Address, @@ -627,62 +688,212 @@ fn transfer_erc20_with_slot( where DB: alloy_evm::Database, { - // Sub amount + // Sub amount (checked: reject if insufficient, matching go-ethereum's + // changeAltTokenBalanceByState which returns an error on underflow) let from_storage_slot = compute_mapping_slot_for_address(token_balance_slot, from); - let balance = journal.sload(token, from_storage_slot)?; - journal.sstore( - token, - from_storage_slot, - balance.saturating_sub(token_amount), + let balance = *journal.sload(token, from_storage_slot)?; + let new_balance = balance.checked_sub(token_amount).ok_or( + MorphInvalidTransaction::InsufficientTokenBalance { + required: token_amount, + available: balance, + }, )?; + journal.sstore(token, from_storage_slot, new_balance)?; - // Add amount + // Add amount (checked: unlike go-ethereum's unbounded big.Int Add, + // we reject on overflow to maintain token conservation) let to_storage_slot = compute_mapping_slot_for_address(token_balance_slot, to); let balance = journal.sload(token, to_storage_slot)?; - journal.sstore(token, to_storage_slot, balance.saturating_add(token_amount))?; + let new_to_balance = + balance + .checked_add(token_amount) + .ok_or(MorphInvalidTransaction::TokenTransferFailed { + reason: "recipient token balance overflow".into(), + })?; + journal.sstore(token, to_storage_slot, new_to_balance)?; Ok((from_storage_slot, to_storage_slot)) } -/// Transfers ERC20 tokens by executing a `transfer(address,uint256)` call via the EVM. -fn transfer_erc20_with_evm( +/// Restores tx-original values for tracked slots after direct journal access. +#[inline] +fn restore_tracked_original_values( + runtime: &crate::MorphTxRuntime, + state: &mut revm::state::EvmState, + address: Address, + slots: impl IntoIterator, +) { + slots.into_iter().for_each(|slot| { + let _ = runtime.restore_tracked_original_value(state, address, slot); + }); +} + +/// Gas limit for internal EVM calls (ERC20 transfer, balanceOf). +const EVM_CALL_GAS_LIMIT: u64 = 200_000; + +/// Execute an internal EVM call, matching go-ethereum's `evm.Call()` semantics. +/// +/// Unlike `system_call_one_with_caller`, this only runs the handler's `execution()` +/// phase — NOT `execution_result()`. This means: +/// - Logs emitted during the call (e.g., ERC20 Transfer events) remain in the journal +/// - State changes remain in the journal +/// +/// **Caller is responsible for saving/restoring `evm.tx` if needed.** +fn evm_call( evm: &mut MorphEvm, caller: Address, + target: Address, + calldata: Bytes, +) -> Result> +where + DB: alloy_evm::Database, +{ + evm.tx = MorphTxEnv { + inner: revm::context::TxEnv { + caller, + kind: target.into(), + data: calldata, + gas_limit: EVM_CALL_GAS_LIMIT, + ..Default::default() + }, + ..Default::default() + }; + let mut h = MorphEvmHandler::::new(); + h.execution(evm, &InitialAndFloorGas::new(0, 0)) +} + +/// Query ERC20 `balanceOf(address)` via an internal EVM call. +/// +/// Uses [`evm_call`] so that journal logs are not drained. +fn evm_call_balance_of(evm: &mut MorphEvm, token: Address, account: Address) -> U256 +where + DB: alloy_evm::Database, +{ + // Snapshot the journal so this helper matches go-ethereum's StaticCall + // semantics even though we route through the normal CALL machinery. + let checkpoint = evm.ctx_mut().journal_mut().checkpoint(); + let calldata = encode_balance_of_calldata(account); + let result = evm_call(evm, Address::ZERO, token, calldata); + let balance = match result { + Ok(ref result) if result.instruction_result().is_ok() => { + let output = &result.interpreter_result().output; + if output.len() >= 32 { + U256::from_be_slice(&output[..32]) + } else { + U256::ZERO + } + } + _ => U256::ZERO, + }; + evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); + balance +} + +/// Matches go-ethereum's `transferAltTokenByEVM` validation: +/// 1. Checks EVM call succeeded (no revert) +/// 2. Validates ABI-decoded bool return value (supports old tokens with no return data) +/// 3. Verifies sender balance changed by the expected amount +/// +/// Uses [`evm_call`] instead of `system_call_one_with_caller` so that event logs +/// (e.g., ERC20 Transfer) naturally remain in the journal and appear in the +/// transaction receipt, matching go-ethereum's `evm.Call()` behavior. +/// +/// `from_balance_before` is the sender's balance before the transfer. If `None`, +/// the balance is queried via EVM call (matching go-eth's nil `userBalanceBefore`). +fn transfer_erc20_with_evm( + evm: &mut MorphEvm, + from: Address, to: Address, token_address: Address, token_amount: U256, + from_balance_before: Option, ) -> Result<(), EVMError> where DB: alloy_evm::Database, { - let tx_origin = evm.tx.clone(); + // Save the original tx by swapping in a default, avoiding a full clone of + // MorphTxEnv (which contains Bytes, AccessList, etc.). + let tx_origin = std::mem::take(&mut evm.tx); + + // Read sender balance before transfer if not provided + let from_balance_before = match from_balance_before { + Some(b) => b, + None => evm_call_balance_of(evm, token_address, from), + }; + let checkpoint = evm.ctx_mut().journal_mut().checkpoint(); let calldata = build_transfer_calldata(to, token_amount); - let res = match evm.system_call_one_with_caller(caller, token_address, calldata) { - Ok(result) => { - if result.is_success() { - Ok(()) - } else { - Err(MorphInvalidTransaction::TokenTransferFailed { - reason: format!("{result:?}"), - } - .into()) + let frame_result = match evm_call(evm, from, token_address, calldata) { + Ok(result) => result, + Err(e) => { + evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); + evm.tx = tx_origin; + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: format!("Error: {e:?}"), } + .into()); } - Err(e) => Err(MorphInvalidTransaction::TokenTransferFailed { - reason: format!("Error: {e:?}"), - } - .into()), }; - // restore the original transaction + if !frame_result.instruction_result().is_ok() { + evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); + evm.tx = tx_origin; + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: format!("{:?}", frame_result.interpreter_result()), + } + .into()); + } + + // Validate ABI bool return value, matching go-ethereum behavior: + // - No return data: accepted (old tokens that don't return bool) + // - 32+ bytes with last byte == 1: accepted (standard ERC20) + // - Otherwise: rejected + let output = &frame_result.interpreter_result().output; + if !output.is_empty() && (output.len() < 32 || output[31] != 1) { + evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); + evm.tx = tx_origin; + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: "alt token transfer returned failure".to_string(), + } + .into()); + } + + // Verify sender balance changed by the expected amount, matching go-ethereum. + let from_balance_after = evm_call_balance_of(evm, token_address, from); + + // Restore the original transaction evm.tx = tx_origin; - res + // Verify sender balance decreased by exactly the transfer amount. + // Matches go-ethereum's transferAltTokenByEVM which always checks this, + // even for self-transfers (from == to), where it would fail because the + // net balance change is zero but the expected decrease is `token_amount`. + let expected_balance = from_balance_before + .checked_sub(token_amount) + .ok_or_else(|| { + EVMError::Transaction(MorphInvalidTransaction::TokenTransferFailed { + reason: format!( + "sender balance {from_balance_before} less than token amount {token_amount}" + ), + }) + })?; + if from_balance_after != expected_balance { + evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: format!( + "sender balance mismatch: expected {expected_balance}, got {from_balance_after}" + ), + } + .into()); + } + + evm.ctx_mut().journal_mut().checkpoint_commit(); + Ok(()) } -/// Build the calldata for ERC20 transfer(address,amount) call. +/// Build the calldata for ERC20 `transfer(address,uint256)` call. /// -/// Method signature: `transfer(address,amount) -> 0xa9059cbb` +/// Method selector: `0xa9059cbb` +#[inline] fn build_transfer_calldata(to: Address, token_amount: alloy_primitives::Uint<256, 4>) -> Bytes { let method_id = [0xa9u8, 0x05, 0x9c, 0xbb]; // Encode calldata: method_id + padded to address + amount @@ -709,6 +920,7 @@ fn build_transfer_calldata(to: Address, token_amount: alloy_primitives::Uint<256 /// /// # Returns /// The new balance after deducting all fees, or an error if balance is insufficient. +#[inline] fn calculate_caller_fee_with_l1_cost( balance: U256, tx: impl Transaction, @@ -721,31 +933,33 @@ fn calculate_caller_fee_with_l1_cost( let is_balance_check_disabled = cfg.is_balance_check_disabled(); let is_fee_charge_disabled = cfg.is_fee_charge_disabled(); - // Calculate L2 effective balance spending (gas + value + blob fees) - let effective_balance_spending = tx - .effective_balance_spending(basefee, blob_price) - .expect("effective balance is always smaller than max balance so it can't overflow"); - - // Total spending = L2 fees + L1 data fee - let total_spending = effective_balance_spending.saturating_add(l1_data_fee); - - // Skip balance check if either: - // - Balance check is explicitly disabled (for special scenarios) - // - Fee charge is disabled (eth_call simulation - no point checking balance if fees won't be charged) - if !is_balance_check_disabled && !is_fee_charge_disabled && balance < total_spending { - return Err(InvalidTransaction::LackOfFundForMaxFee { - fee: Box::new(total_spending), - balance: Box::new(balance), - }); + // Validate balance against max possible spending using max_fee_per_gas (not effective_gas_price). + // go-eth's buyGas() checks: balance >= gasFeeCap * gas + value + l1DataFee. + // This ensures the sender can afford the worst-case gas cost before deducting the actual cost. + if !is_balance_check_disabled && !is_fee_charge_disabled { + let max_gas_spending = U256::from( + (tx.gas_limit() as u128) + .checked_mul(tx.max_fee_per_gas()) + .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?, + ); + let max_spending = max_gas_spending + .checked_add(tx.value()) + .and_then(|v| v.checked_add(l1_data_fee)) + .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?; + if balance < max_spending { + return Err(InvalidTransaction::LackOfFundForMaxFee { + fee: Box::new(max_spending), + balance: Box::new(balance), + }); + } } - // Calculate gas balance spending (excluding value transfer) + // Deduct using effective_gas_price (not max_fee_per_gas). + // go-eth's buyGas(): SubBalance(from, gasPrice * gas + l1DataFee) + let effective_balance_spending = tx.effective_balance_spending(basefee, blob_price)?; let gas_balance_spending = effective_balance_spending - tx.value(); - - // Total fee deduction = L2 gas fees + L1 data fee (value is transferred separately) let total_fee_deduction = gas_balance_spending.saturating_add(l1_data_fee); - // New balance after fee deduction let mut new_balance = balance.saturating_sub(total_fee_deduction); if is_balance_check_disabled { @@ -755,3 +969,333 @@ fn calculate_caller_fee_with_l1_cost( Ok(new_balance) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::MorphBlockEnv; + use alloy_primitives::{Bytes, address, keccak256}; + use morph_chainspec::hardfork::MorphHardfork; + use revm::{ + context::{BlockEnv, TxEnv}, + database::{CacheDB, EmptyDB}, + inspector::NoOpInspector, + state::{AccountInfo, Bytecode}, + }; + + fn sstore_code(slot: U256, value: u8) -> Bytes { + let mut code = Vec::with_capacity(1 + 1 + 1 + 32 + 2); + code.push(0x60); // PUSH1 value + code.push(value); + code.push(0x7f); // PUSH32 slot + code.extend_from_slice(&slot.to_be_bytes::<32>()); + code.push(0x55); // SSTORE + code.push(0x00); // STOP + Bytes::from(code) + } + + fn mutating_return_code(write_value: u8, return_value: u8) -> Bytes { + Bytes::from(vec![ + 0x60, + write_value, // PUSH1 write_value + 0x60, + 0x00, // PUSH1 slot 0 + 0x55, // SSTORE + 0x60, + return_value, // PUSH1 return_value + 0x60, + 0x00, // PUSH1 offset 0 + 0x52, // MSTORE + 0x60, + 0x20, // PUSH1 size 32 + 0x60, + 0x00, // PUSH1 offset 0 + 0xf3, // RETURN + ]) + } + + #[test] + fn forced_cold_slot_sstore_preserves_original_value() { + let caller = address!("1000000000000000000000000000000000000001"); + let token = address!("3000000000000000000000000000000000000003"); + let balance_slot = U256::from(7); + let caller_slot = compute_mapping_slot_for_address(balance_slot, caller); + + let original_balance = U256::from(100); + let deducted_balance = U256::from(90); + let final_balance = U256::from(80); + let contract_code = sstore_code(caller_slot, 80); + + let mut db = CacheDB::new(EmptyDB::default()); + db.insert_account_info( + caller, + AccountInfo { + balance: U256::from(1_000_000), + ..Default::default() + }, + ); + db.insert_account_info( + token, + AccountInfo { + code_hash: keccak256(contract_code.as_ref()), + code: Some(Bytecode::new_raw(contract_code)), + ..Default::default() + }, + ); + db.insert_account_storage(token, caller_slot, original_balance) + .unwrap(); + + let mut evm = MorphEvm::new( + MorphContext::new(db, MorphHardfork::default()), + NoOpInspector, + ); + evm.tx = MorphTxEnv { + inner: TxEnv { + caller, + kind: token.into(), + gas_limit: 100_000, + ..Default::default() + }, + ..Default::default() + }; + evm.block = MorphBlockEnv { + inner: BlockEnv::default(), + }; + + { + let (_, _, _, journal, chain, _) = evm.ctx().all_mut(); + let _ = journal.load_account_mut(token).unwrap(); + journal.touch(token); + journal + .sstore(token, caller_slot, deducted_balance) + .unwrap(); + + let token_account = journal.state.get_mut(&token).unwrap(); + token_account.mark_cold(); + token_account + .storage + .get_mut(&caller_slot) + .unwrap() + .mark_cold(); + + chain.track_forced_cold_slot(token, caller_slot, original_balance); + } + + let frame_result = MorphEvmHandler::, NoOpInspector>::new() + .execution(&mut evm, &InitialAndFloorGas::new(0, 0)) + .unwrap(); + assert!(frame_result.instruction_result().is_ok()); + + let slot_state = evm + .ctx_ref() + .journal() + .state + .get(&token) + .and_then(|account| account.storage.get(&caller_slot)) + .unwrap(); + + assert_eq!(slot_state.present_value, final_balance); + assert_eq!(slot_state.original_value, original_balance); + } + + #[test] + fn reimburse_token_fee_preserves_original_values_for_touched_slots() { + let caller = address!("1000000000000000000000000000000000000001"); + let beneficiary = address!("2000000000000000000000000000000000000002"); + let token = address!("3000000000000000000000000000000000000003"); + let balance_slot = U256::from(7); + let caller_slot = compute_mapping_slot_for_address(balance_slot, caller); + let beneficiary_slot = compute_mapping_slot_for_address(balance_slot, beneficiary); + + let caller_balance = U256::from(100); + let beneficiary_balance = U256::from(50); + let deducted = U256::from(10); + let refunded = U256::from(4); + + let mut db = CacheDB::new(EmptyDB::default()); + db.insert_account_info(token, AccountInfo::default()); + db.insert_account_storage(token, caller_slot, caller_balance) + .unwrap(); + db.insert_account_storage(token, beneficiary_slot, beneficiary_balance) + .unwrap(); + + let mut evm = MorphEvm::new( + MorphContext::new(db, MorphHardfork::default()), + NoOpInspector, + ); + evm.tx = MorphTxEnv { + inner: revm::context::TxEnv { + caller, + gas_price: 1, + gas_limit: 100_000, + ..Default::default() + }, + fee_token_id: Some(5), + ..Default::default() + }; + evm.block = MorphBlockEnv { + inner: BlockEnv { + beneficiary, + ..Default::default() + }, + }; + evm.cached_token_fee_info = Some(TokenFeeInfo { + token_address: token, + price_ratio: U256::from(1), + scale: U256::from(1), + balance_slot: Some(balance_slot), + ..Default::default() + }); + + { + let (_, _, _, journal, chain, _) = evm.ctx().all_mut(); + let _ = journal.load_account_mut(token).unwrap(); + journal.touch(token); + let (from_storage_slot, to_storage_slot) = transfer_erc20_with_slot( + journal, + caller, + beneficiary, + token, + deducted, + balance_slot, + ) + .unwrap(); + + let token_account = journal.state.get_mut(&token).unwrap(); + token_account.mark_cold(); + token_account + .storage + .get_mut(&from_storage_slot) + .unwrap() + .mark_cold(); + chain.track_forced_cold_slot(token, from_storage_slot, caller_balance); + token_account + .storage + .get_mut(&to_storage_slot) + .unwrap() + .mark_cold(); + chain.track_forced_cold_slot(token, to_storage_slot, beneficiary_balance); + } + + let mut gas = Gas::new(10); + gas.set_spent(6); + + MorphEvmHandler::, NoOpInspector>::new() + .reimburse_caller_token_fee(&mut evm, &gas) + .unwrap(); + + let token_account = evm.ctx_ref().journal().state.get(&token).unwrap(); + let caller_slot_state = token_account.storage.get(&caller_slot).unwrap(); + let beneficiary_slot_state = token_account.storage.get(&beneficiary_slot).unwrap(); + + assert_eq!(caller_slot_state.original_value, caller_balance); + assert_eq!( + caller_slot_state.present_value, + caller_balance - deducted + refunded + ); + assert_eq!(beneficiary_slot_state.original_value, beneficiary_balance); + assert_eq!( + beneficiary_slot_state.present_value, + beneficiary_balance + deducted - refunded + ); + } + + #[test] + fn transfer_erc20_with_evm_reverts_state_on_validation_failure() { + let from = address!("1000000000000000000000000000000000000001"); + let to = address!("2000000000000000000000000000000000000002"); + let token = address!("3000000000000000000000000000000000000003"); + let original_balance = U256::from(50); + let contract_code = mutating_return_code(1, 0); + + let mut db = CacheDB::new(EmptyDB::default()); + db.insert_account_info( + from, + AccountInfo { + balance: U256::from(1_000_000), + ..Default::default() + }, + ); + db.insert_account_info( + token, + AccountInfo { + code_hash: keccak256(contract_code.as_ref()), + code: Some(Bytecode::new_raw(contract_code)), + ..Default::default() + }, + ); + db.insert_account_storage(token, U256::ZERO, original_balance) + .unwrap(); + + let mut evm = MorphEvm::new( + MorphContext::new(db, MorphHardfork::default()), + NoOpInspector, + ); + evm.block = MorphBlockEnv { + inner: BlockEnv::default(), + }; + + let err = transfer_erc20_with_evm( + &mut evm, + from, + to, + token, + U256::from(4), + Some(original_balance), + ) + .unwrap_err(); + + assert!(matches!( + err, + EVMError::Transaction(MorphInvalidTransaction::TokenTransferFailed { .. }) + )); + let slot_state = evm + .ctx_ref() + .journal() + .state + .get(&token) + .and_then(|account| account.storage.get(&U256::ZERO)) + .unwrap(); + assert_eq!(slot_state.present_value, original_balance); + } + + #[test] + fn evm_call_balance_of_is_read_only() { + let token = address!("3000000000000000000000000000000000000003"); + let account = address!("1000000000000000000000000000000000000001"); + let original_balance = U256::from(50); + let contract_code = mutating_return_code(1, 42); + + let mut db = CacheDB::new(EmptyDB::default()); + db.insert_account_info( + token, + AccountInfo { + code_hash: keccak256(contract_code.as_ref()), + code: Some(Bytecode::new_raw(contract_code)), + ..Default::default() + }, + ); + db.insert_account_storage(token, U256::ZERO, original_balance) + .unwrap(); + + let mut evm = MorphEvm::new( + MorphContext::new(db, MorphHardfork::default()), + NoOpInspector, + ); + evm.block = MorphBlockEnv { + inner: BlockEnv::default(), + }; + + let balance = evm_call_balance_of(&mut evm, token, account); + + assert_eq!(balance, U256::from(42)); + let slot_state = evm + .ctx_ref() + .journal() + .state + .get(&token) + .and_then(|acct| acct.storage.get(&U256::ZERO)) + .unwrap(); + assert_eq!(slot_state.present_value, original_balance); + } +} diff --git a/crates/revm/src/l1block.rs b/crates/revm/src/l1block.rs index d570862..cbe5b6a 100644 --- a/crates/revm/src/l1block.rs +++ b/crates/revm/src/l1block.rs @@ -103,6 +103,10 @@ pub const INITIAL_BLOB_SCALAR: U256 = U256::from_limbs([417565260, 0, 0, 0]); /// Curie hardfork flag value (1 = true). pub const IS_CURIE: U256 = U256::from_limbs([1, 0, 0, 0]); +/// Maximum L1 data fee cap for circuit compatibility. +/// Matches go-ethereum's `CalculateL1DataFee` cap in `rollup/fees/rollup_fee.go`. +const L1_FEE_CAP: U256 = U256::from_limbs([u64::MAX, 0, 0, 0]); + /// Storage updates for L1 gas price oracle Curie hardfork initialization. /// /// These storage slots are initialized when the Curie hardfork activates: @@ -125,7 +129,7 @@ pub const CURIE_L1_GAS_PRICE_ORACLE_STORAGE: [(U256, U256); 4] = [ /// /// Contains the fee parameters fetched from the L1 Gas Price Oracle contract. /// These parameters are used to calculate the L1 data fee for transactions. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct L1BlockInfo { /// The base fee of the L1 origin block. pub l1_base_fee: U256, @@ -133,14 +137,14 @@ pub struct L1BlockInfo { pub l1_fee_overhead: U256, /// The current L1 fee scalar. pub l1_base_fee_scalar: U256, - /// The current L1 blob base fee, None if before Curie. - pub l1_blob_base_fee: Option, - /// The current L1 commit scalar, None if before Curie. - pub l1_commit_scalar: Option, - /// The current L1 blob scalar, None if before Curie. - pub l1_blob_scalar: Option, - /// The current call data gas (l1_commit_scalar * l1_base_fee), None if before Curie. - pub calldata_gas: Option, + /// The current L1 blob base fee (zero if before Curie). + pub l1_blob_base_fee: U256, + /// The current L1 commit scalar (zero if before Curie). + pub l1_commit_scalar: U256, + /// The current L1 blob scalar (zero if before Curie). + pub l1_blob_scalar: U256, + /// The current call data gas: `l1_commit_scalar * l1_base_fee` (zero if before Curie). + pub calldata_gas: U256, } impl L1BlockInfo { @@ -177,10 +181,10 @@ impl L1BlockInfo { l1_base_fee, l1_fee_overhead, l1_base_fee_scalar, - l1_blob_base_fee: Some(l1_blob_base_fee), - l1_commit_scalar: Some(l1_commit_scalar), - l1_blob_scalar: Some(l1_blob_scalar), - calldata_gas: Some(calldata_gas), + l1_blob_base_fee, + l1_commit_scalar, + l1_blob_scalar, + calldata_gas, }) } } @@ -204,8 +208,8 @@ impl L1BlockInfo { .saturating_add(TX_L1_COMMIT_EXTRA_COST) } else { U256::from(input.len()) - .saturating_mul(self.l1_blob_base_fee.unwrap_or_default()) - .saturating_mul(self.l1_blob_scalar.unwrap_or_default()) + .saturating_mul(self.l1_blob_base_fee) + .saturating_mul(self.l1_blob_scalar) } } @@ -225,7 +229,6 @@ impl L1BlockInfo { let blob_gas = self.data_gas(input, hardfork); self.calldata_gas - .unwrap_or_default() .saturating_add(blob_gas) .wrapping_div(TX_L1_FEE_PRECISION) } @@ -234,12 +237,17 @@ impl L1BlockInfo { /// /// This is the cost of posting the transaction data to L1 for data availability. /// The calculation method differs based on whether the Curie hardfork is active. + /// + /// The result is capped to `u64::MAX` for circuit compatibility, matching go-ethereum's + /// `CalculateL1DataFee` behavior in `rollup/fees/rollup_fee.go`. pub fn calculate_tx_l1_cost(&self, input: &[u8], hardfork: MorphHardfork) -> U256 { - if !hardfork.is_curie() { + let fee = if !hardfork.is_curie() { self.calculate_tx_l1_cost_pre_curie(input, hardfork) } else { self.calculate_tx_l1_cost_curie(input, hardfork) - } + }; + // Cap to u64::MAX for circuit compatibility (go-ethereum: rollup_fee.go:248-249) + fee.min(L1_FEE_CAP) } } @@ -253,10 +261,10 @@ mod tests { assert_eq!(info.l1_base_fee, U256::ZERO); assert_eq!(info.l1_fee_overhead, U256::ZERO); assert_eq!(info.l1_base_fee_scalar, U256::ZERO); - assert!(info.l1_blob_base_fee.is_none()); - assert!(info.l1_commit_scalar.is_none()); - assert!(info.l1_blob_scalar.is_none()); - assert!(info.calldata_gas.is_none()); + assert_eq!(info.l1_blob_base_fee, U256::ZERO); + assert_eq!(info.l1_commit_scalar, U256::ZERO); + assert_eq!(info.l1_blob_scalar, U256::ZERO); + assert_eq!(info.calldata_gas, U256::ZERO); } #[test] @@ -276,8 +284,8 @@ mod tests { #[test] fn test_data_gas_curie() { let info = L1BlockInfo { - l1_blob_base_fee: Some(U256::from(10)), - l1_blob_scalar: Some(U256::from(2)), + l1_blob_base_fee: U256::from(10), + l1_blob_scalar: U256::from(2), ..Default::default() }; diff --git a/crates/revm/src/lib.rs b/crates/revm/src/lib.rs index 75727d6..338c95a 100644 --- a/crates/revm/src/lib.rs +++ b/crates/revm/src/lib.rs @@ -51,6 +51,7 @@ pub mod exec; pub mod handler; pub mod l1block; pub mod precompiles; +pub mod runtime; pub mod token_fee; mod tx; @@ -78,6 +79,7 @@ pub use l1block::{ L1BlockInfo, }; pub use precompiles::MorphPrecompiles; +pub use runtime::MorphTxRuntime; pub use token_fee::{ L2_TOKEN_REGISTRY_ADDRESS, TokenFeeInfo, compute_mapping_slot, compute_mapping_slot_for_address, encode_balance_of_calldata, query_erc20_balance, diff --git a/crates/revm/src/precompiles.rs b/crates/revm/src/precompiles.rs index 650fac9..0dc8b12 100644 --- a/crates/revm/src/precompiles.rs +++ b/crates/revm/src/precompiles.rs @@ -12,11 +12,11 @@ //! └── Emerald = Morph203 + Osaka precompiles //! ``` //! -//! | Hardfork | Base | Added | Notes | +//! | Hardfork | Base | Changes | 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) | - | +//! | Bernoulli/Curie | Berlin | ripemd160/blake2f as disabled stubs; modexp 32B limit | - | +//! | Morph203/Viridian| Bernoulli | blake2f/ripemd160 re-enabled; BN256 pairing 4-pair limit | - | +//! | Emerald | Morph203 | BLS12-381, P256verify; modexp EIP-7823/7883 upgrade | NO KZG (0x0a) | //! //! ## Why Disabled Stubs? //! @@ -71,6 +71,20 @@ pub mod addresses { pub const BLAKE2F: Address = u64_to_address(9); /// point evaluation precompile address (10) - EIP-4844 pub const POINT_EVALUATION: Address = u64_to_address(10); + /// BLS12-381 G1 Add precompile address (0x0b) + pub const BLS12_G1ADD: Address = u64_to_address(0x0b); + /// BLS12-381 G1 MultiExp precompile address (0x0c) + pub const BLS12_G1MULTIEXP: Address = u64_to_address(0x0c); + /// BLS12-381 G2 Add precompile address (0x0d) + pub const BLS12_G2ADD: Address = u64_to_address(0x0d); + /// BLS12-381 G2 MultiExp precompile address (0x0e) + pub const BLS12_G2MULTIEXP: Address = u64_to_address(0x0e); + /// BLS12-381 Pairing precompile address (0x0f) + pub const BLS12_PAIRING: Address = u64_to_address(0x0f); + /// BLS12-381 Map FP to G1 precompile address (0x10) + pub const BLS12_MAP_FP_TO_G1: Address = u64_to_address(0x10); + /// BLS12-381 Map FP2 to G2 precompile address (0x11) + pub const BLS12_MAP_FP2_TO_G2: Address = u64_to_address(0x11); /// P256verify precompile address (256) - RIP-7212 pub const P256_VERIFY: Address = u64_to_address(256); } @@ -152,6 +166,73 @@ fn blake2f_disabled(_input: &[u8], _gas_limit: u64) -> PrecompileResult { )) } +/// Checks if a 32-byte big-endian length field at `offset` in `data` exceeds 32. +/// +/// Right-pads with zeros if `data` is shorter than `offset + 32`, matching +/// go-ethereum's `getData` semantics. +fn modexp_len_exceeds_32(data: &[u8], offset: usize) -> bool { + let mut buf = [0u8; 32]; + let start = offset.min(data.len()); + let end = (offset + 32).min(data.len()); + let n = end.saturating_sub(start); + if n > 0 { + buf[..n].copy_from_slice(&data[start..end]); + } + // A big-endian 256-bit value > 32 iff any of the high 31 bytes is non-zero, + // or the lowest byte exceeds 32. + buf[..31].iter().any(|&b| b != 0) || buf[31] > 32 +} + +/// Wraps Berlin modexp with go-ethereum's 32-byte input length limit. +/// +/// go-ethereum enforces `base_len, exp_len, mod_len <= 32` when `eip2565=true` +/// and neither `eip7823` nor `eip7883` is active (Bernoulli through Viridian). +/// Without this limit, morph-reth would accept arbitrarily large modexp inputs +/// that go-ethereum rejects, causing a consensus split. +/// +/// Ref: +fn modexp_with_32byte_limit(input: &[u8], gas_limit: u64) -> PrecompileResult { + // The first 96 bytes of modexp input are three 32-byte big-endian length fields: + // [0..32] = base_len, [32..64] = exp_len, [64..96] = mod_len + if modexp_len_exceeds_32(input, 0) + || modexp_len_exceeds_32(input, 32) + || modexp_len_exceeds_32(input, 64) + { + return Err(PrecompileError::Other( + "modexp temporarily only accepts inputs of 32 bytes (256 bits) or less".into(), + )); + } + + // Delegate to Berlin modexp (EIP-2565 gas pricing, standard computation) + Precompiles::berlin() + .get(&addresses::MODEXP) + .expect("Berlin precompiles must include modexp") + .execute(input, gas_limit) +} + +/// Wraps BN256 pairing with go-ethereum's 4-pair input length limit. +/// +/// go-ethereum limits BN256 pairing to at most 4 pairs (768 bytes) from Morph203 +/// onwards via `limitInputLength: true`. Without this limit, morph-reth would +/// accept larger pairing inputs, which can cause a consensus split if gas +/// accounting differs (the underlying computation is the same, but block gas +/// limits and metering become inconsistent). +/// +/// Ref: +fn bn256_pairing_with_4pair_limit(input: &[u8], gas_limit: u64) -> PrecompileResult { + if input.len() > 4 * 192 { + return Err(PrecompileError::Other( + "bad elliptic curve pairing size".into(), + )); + } + + // Delegate to Berlin/Istanbul BN256 pairing + Precompiles::berlin() + .get(&addresses::BN256_PAIRING) + .expect("Berlin precompiles must include BN256 pairing") + .execute(input, gas_limit) +} + /// Returns precompiles for Bernoulli hardfork. /// /// Based on Berlin with ripemd160 (0x03) and blake2f (0x09) replaced by disabled stubs. @@ -177,6 +258,15 @@ pub fn bernoulli() -> &'static Precompiles { Precompile::new(PrecompileId::Blake2F, addresses::BLAKE2F, blake2f_disabled), ]); + // Replace modexp (0x05) with 32-byte input limit wrapper. + // go-ethereum's Bernoulli modexp has eip2565=true but neither eip7823 nor eip7883, + // which enforces base/exp/mod <= 32 bytes. Berlin modexp in revm has no such limit. + precompiles.extend([Precompile::new( + PrecompileId::ModExp, + addresses::MODEXP, + modexp_with_32byte_limit, + )]); + precompiles }) } @@ -190,47 +280,79 @@ pub fn bernoulli() -> &'static Precompiles { pub fn morph203() -> &'static Precompiles { static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| { - // Start from Bernoulli and add blake2f + ripemd160 + // Start from Bernoulli and re-enable blake2f + ripemd160 let mut precompiles = bernoulli().clone(); let berlin = Precompiles::berlin(); - // Add blake2f back (was disabled in Bernoulli) + // Re-enable blake2f (0x09) — was disabled stub in Bernoulli if let Some(blake2f) = berlin.get(&addresses::BLAKE2F) { precompiles.extend([blake2f.clone()]); } - // Add ripemd160 back (was disabled in Bernoulli) + // Re-enable ripemd160 (0x03) — was disabled stub in Bernoulli if let Some(ripemd) = berlin.get(&addresses::RIPEMD160) { precompiles.extend([ripemd.clone()]); } + // Replace BN256 pairing (0x08) with 4-pair limited version. + // go-ethereum's Morph203 uses `limitInputLength: true` which caps + // pairing input to 4 pairs (768 bytes). + precompiles.extend([Precompile::new( + PrecompileId::Bn254Pairing, + addresses::BN256_PAIRING, + bn256_pairing_with_4pair_limit, + )]); + precompiles }) } /// Returns precompiles for Emerald hardfork. /// -/// Based on Morph203/Viridian with Osaka precompiles added. -/// - All standard precompiles (ecrecover, sha256, ripemd160, identity, modexp, bn256 ops, blake2f) -/// - Osaka precompiles (P256verify RIP-7212, BLS12-381 EIP-2537, etc.) +/// Based on Morph203/Viridian with explicit additions matching go-ethereum's +/// `PrecompiledContractsEmerald`: /// -/// Matches: PrecompiledContractsEmerald in Go +/// - Upgrades modexp (0x05) to EIP-7823 (1024-byte input cap) + EIP-7883 (new gas formula) +/// - Adds BLS12-381 precompiles (0x0b-0x11) from EIP-2537 +/// - Adds P256verify (0x100) from RIP-7212 +/// - Does **NOT** include KZG Point Evaluation (0x0a) — go-ethereum omits it +/// +/// Ref: pub fn emerald() -> &'static Precompiles { static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| { - // Start from Morph203/Viridian let mut precompiles = morph203().clone(); - - // Add Osaka precompiles (includes P256verify, BLS12-381, etc.) let osaka = Precompiles::osaka(); - for addr in osaka.addresses() { - // Skip precompiles we already have - if !precompiles.contains(addr) - && let Some(precompile) = osaka.get(addr) - { + + // Upgrade modexp (0x05) from 32-byte-limited wrapper to osaka version. + // Emerald uses eip7823=true (1024-byte input cap) + eip7883=true (new gas formula), + // which replaces the Bernoulli~Viridian 32-byte restriction. + if let Some(modexp) = osaka.get(&addresses::MODEXP) { + precompiles.extend([modexp.clone()]); + } + + // Add BLS12-381 precompiles (EIP-2537): 0x0b through 0x11 + for addr in [ + addresses::BLS12_G1ADD, + addresses::BLS12_G1MULTIEXP, + addresses::BLS12_G2ADD, + addresses::BLS12_G2MULTIEXP, + addresses::BLS12_PAIRING, + addresses::BLS12_MAP_FP_TO_G1, + addresses::BLS12_MAP_FP2_TO_G2, + ] { + if let Some(precompile) = osaka.get(&addr) { precompiles.extend([precompile.clone()]); } } + // Add P256verify (RIP-7212) at 0x100 + if let Some(p256) = osaka.get(&addresses::P256_VERIFY) { + precompiles.extend([p256.clone()]); + } + + // NOTE: KZG Point Evaluation (0x0a) is intentionally NOT included. + // go-ethereum's PrecompiledContractsEmerald skips 0x0a entirely. + precompiles }) } @@ -278,32 +400,52 @@ mod tests { fn test_bernoulli_precompiles() { let precompiles = bernoulli(); - // Should have ecrecover, sha256, identity, modexp, bn256 ops + // Should have all 9 Berlin addresses (ecrecover, sha256, ripemd160, identity, + // modexp, bn256 add/mul/pairing, blake2f) assert!(precompiles.contains(&addresses::ECRECOVER)); assert!(precompiles.contains(&addresses::SHA256)); assert!(precompiles.contains(&addresses::IDENTITY)); assert!(precompiles.contains(&addresses::MODEXP)); assert!(precompiles.contains(&addresses::BN256_ADD)); + assert!(precompiles.contains(&addresses::BN256_MUL)); + assert!(precompiles.contains(&addresses::BN256_PAIRING)); // 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)); + + // Exact count: 9 precompiles (matching go-eth PrecompiledContractsBernoulli) + assert_eq!(precompiles.len(), 9); + } + + #[test] + fn test_bernoulli_modexp_rejects_large_input() { + let precompiles = bernoulli(); + let modexp = precompiles.get(&addresses::MODEXP).unwrap(); + + // base_len=33 (exceeds 32-byte limit) — should be rejected + let mut input = vec![0u8; 96]; + input[31] = 33; // base_len = 33 + input[63] = 32; // exp_len = 32 + input[95] = 32; // mod_len = 32 + let result = modexp.execute(&input, 100_000); + assert!( + result.is_err(), + "modexp with base_len=33 should be rejected" + ); + + // base_len=32, exp_len=32, mod_len=32 — should succeed + input[31] = 32; + let result = modexp.execute(&input, 100_000); + assert!(result.is_ok(), "modexp with all lens=32 should succeed"); } #[test] fn test_curie_uses_bernoulli_precompiles() { - // Curie uses the same precompile set as Bernoulli - // Go implementation has no PrecompiledContractsCurie let bernoulli_p = MorphPrecompiles::new_with_spec(MorphHardfork::Bernoulli); let curie_p = MorphPrecompiles::new_with_spec(MorphHardfork::Curie); - // Both should have the same precompiles assert_eq!(bernoulli_p.precompiles().len(), curie_p.precompiles().len()); - - // 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)); @@ -313,79 +455,148 @@ mod tests { fn test_morph203_precompiles() { let precompiles = morph203(); - // Should have blake2f and ripemd160 re-enabled + // blake2f and ripemd160 re-enabled (working, not disabled stubs) assert!(precompiles.contains(&addresses::BLAKE2F)); assert!(precompiles.contains(&addresses::RIPEMD160)); - - // All standard precompiles assert!(precompiles.contains(&addresses::ECRECOVER)); assert!(precompiles.contains(&addresses::SHA256)); - // P256verify not yet added in Morph203 + // No Osaka-era precompiles yet assert!(!precompiles.contains(&addresses::P256_VERIFY)); + assert!(!precompiles.contains(&addresses::POINT_EVALUATION)); + + // Same count as Bernoulli (9 addresses, different implementations) + assert_eq!(precompiles.len(), 9); + } + + #[test] + fn test_morph203_pairing_rejects_large_input() { + let precompiles = morph203(); + let pairing = precompiles.get(&addresses::BN256_PAIRING).unwrap(); + + // 5 pairs (960 bytes) — exceeds 4-pair limit, should be rejected + let input = vec![0u8; 5 * 192]; + let result = pairing.execute(&input, 1_000_000); + assert!(result.is_err(), "pairing with 5 pairs should be rejected"); + + // 4 pairs (768 bytes) — within limit, should not be rejected for size + // (may still fail due to invalid curve points, but not for size) + let input = vec![0u8; 4 * 192]; + let result = pairing.execute(&input, 1_000_000); + // Zero-input pairing is valid and returns true + assert!( + result.is_ok(), + "pairing with 4 pairs should not be rejected for size" + ); } #[test] fn test_emerald_precompiles() { let precompiles = emerald(); - // All standard precompiles should be enabled + // All standard precompiles (0x01-0x09) assert!(precompiles.contains(&addresses::ECRECOVER)); assert!(precompiles.contains(&addresses::SHA256)); - assert!(precompiles.contains(&addresses::RIPEMD160)); // Now enabled! + assert!(precompiles.contains(&addresses::RIPEMD160)); assert!(precompiles.contains(&addresses::IDENTITY)); assert!(precompiles.contains(&addresses::MODEXP)); assert!(precompiles.contains(&addresses::BN256_ADD)); + assert!(precompiles.contains(&addresses::BN256_MUL)); + assert!(precompiles.contains(&addresses::BN256_PAIRING)); assert!(precompiles.contains(&addresses::BLAKE2F)); - // P256verify should be present + // BLS12-381 precompiles (0x0b-0x11) + assert!(precompiles.contains(&addresses::BLS12_G1ADD)); + assert!(precompiles.contains(&addresses::BLS12_G1MULTIEXP)); + assert!(precompiles.contains(&addresses::BLS12_G2ADD)); + assert!(precompiles.contains(&addresses::BLS12_G2MULTIEXP)); + assert!(precompiles.contains(&addresses::BLS12_PAIRING)); + assert!(precompiles.contains(&addresses::BLS12_MAP_FP_TO_G1)); + assert!(precompiles.contains(&addresses::BLS12_MAP_FP2_TO_G2)); + + // P256verify (0x100) assert!(precompiles.contains(&addresses::P256_VERIFY)); + + // KZG Point Evaluation (0x0a) must NOT be included + assert!( + !precompiles.contains(&addresses::POINT_EVALUATION), + "Emerald must NOT include KZG Point Evaluation (0x0a)" + ); + + // Exact count: 9 (standard) + 7 (BLS12-381) + 1 (P256verify) = 17 + // Matching go-eth PrecompiledContractsEmerald which has 17 entries + assert_eq!(precompiles.len(), 17); } #[test] - fn test_precompile_counts_increase() { - let bernoulli_count = bernoulli().len(); - let morph203_count = morph203().len(); - let emerald_count = emerald().len(); - - // 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); + fn test_emerald_modexp_accepts_large_input() { + let precompiles = emerald(); + let modexp = precompiles.get(&addresses::MODEXP).unwrap(); + + // base_len=64 — should succeed in Emerald (32-byte limit lifted) + let mut input = vec![0u8; 96 + 64 + 32 + 64]; // base_len + exp_len + mod_len + data + input[31] = 64; // base_len = 64 + input[63] = 32; // exp_len = 32 + input[95] = 64; // mod_len = 64 + let result = modexp.execute(&input, 1_000_000); + assert!(result.is_ok(), "Emerald modexp should accept base_len=64"); + } - // Emerald should have more than Morph203 (adds Osaka precompiles) - assert!(emerald_count > morph203_count); + #[test] + fn test_precompile_counts() { + assert_eq!(bernoulli().len(), 9); + assert_eq!(morph203().len(), 9); + assert_eq!(emerald().len(), 17); } #[test] fn test_hardfork_specific_precompiles() { - // Verify that each hardfork has the expected precompile configuration let bernoulli_p = MorphPrecompiles::new_with_spec(MorphHardfork::Bernoulli); let curie_p = MorphPrecompiles::new_with_spec(MorphHardfork::Curie); let morph203_p = MorphPrecompiles::new_with_spec(MorphHardfork::Morph203); let viridian_p = MorphPrecompiles::new_with_spec(MorphHardfork::Viridian); let emerald_p = MorphPrecompiles::new_with_spec(MorphHardfork::Emerald); - // 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. + // Bernoulli/Curie: disabled stubs present, same 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)); + assert_eq!(bernoulli_p.precompiles().len(), curie_p.precompiles().len()); - // Morph203 and Viridian: blake2f + ripemd160 enabled, no P256verify (same precompile set) + // Morph203/Viridian: re-enabled, no P256verify, same set assert!(morph203_p.contains(&addresses::RIPEMD160)); assert!(morph203_p.contains(&addresses::BLAKE2F)); assert!(!morph203_p.contains(&addresses::P256_VERIFY)); - assert!(viridian_p.contains(&addresses::RIPEMD160)); - assert!(viridian_p.contains(&addresses::BLAKE2F)); - assert!(!viridian_p.contains(&addresses::P256_VERIFY)); assert_eq!( morph203_p.precompiles().len(), viridian_p.precompiles().len() ); - // Emerald: all precompiles enabled including Osaka precompiles (P256verify, BLS12-381, etc) - assert!(emerald_p.contains(&addresses::RIPEMD160)); + // Emerald: full set with BLS12-381 + P256verify, no KZG assert!(emerald_p.contains(&addresses::P256_VERIFY)); + assert!(emerald_p.contains(&addresses::BLS12_G1ADD)); + assert!(!emerald_p.contains(&addresses::POINT_EVALUATION)); + } + + #[test] + fn test_modexp_len_check() { + // Value = 0 (all zeros) — should NOT exceed 32 + assert!(!modexp_len_exceeds_32(&[0u8; 32], 0)); + + // Value = 32 — should NOT exceed 32 + let mut data = [0u8; 32]; + data[31] = 32; + assert!(!modexp_len_exceeds_32(&data, 0)); + + // Value = 33 — should exceed 32 + data[31] = 33; + assert!(modexp_len_exceeds_32(&data, 0)); + + // Value has non-zero high byte — definitely exceeds 32 + data[0] = 1; + data[31] = 0; + assert!(modexp_len_exceeds_32(&data, 0)); + + // Empty input (right-padded to all zeros) — value = 0, should NOT exceed + assert!(!modexp_len_exceeds_32(&[], 0)); } } diff --git a/crates/revm/src/runtime.rs b/crates/revm/src/runtime.rs new file mode 100644 index 0000000..77eb37d --- /dev/null +++ b/crates/revm/src/runtime.rs @@ -0,0 +1,58 @@ +//! Morph transaction-scoped runtime state. +//! +//! This module stores per-transaction execution metadata that should not live in +//! the transaction input (`MorphTxEnv`) or fee-oracle data structures. + +use alloy_primitives::{Address, U256}; +use revm::{primitives::HashMap, state::EvmState}; + +/// Execution-scoped state for a single Morph transaction. +/// +/// The only tracked data today is the set of storage slots that Morph +/// intentionally re-marks cold after token-fee deduction. revm may later warm +/// these slots again and overwrite their `original_value`, so we retain the true +/// tx-original value here and restore it on every affected access path. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct MorphTxRuntime { + forced_cold_slot_originals: HashMap<(Address, U256), U256>, +} + +impl MorphTxRuntime { + /// Clears all transaction-scoped state. + #[inline] + pub fn reset(&mut self) { + self.forced_cold_slot_originals.clear(); + } + + /// Records the true tx-original value for a slot that will be marked cold. + #[inline] + pub fn track_forced_cold_slot(&mut self, address: Address, slot: U256, original_value: U256) { + self.forced_cold_slot_originals + .entry((address, slot)) + .or_insert(original_value); + } + + /// Returns the tracked tx-original value for a forced-cold slot, if any. + #[inline] + pub fn tracked_original_value(&self, address: Address, slot: U256) -> Option { + self.forced_cold_slot_originals + .get(&(address, slot)) + .copied() + } + + /// Restores a tracked tx-original value into journal state. + #[inline] + pub fn restore_tracked_original_value( + &self, + state: &mut EvmState, + address: Address, + slot: U256, + ) -> Option { + let original_value = self.tracked_original_value(address, slot)?; + let slot_state = state + .get_mut(&address) + .and_then(|account| account.storage.get_mut(&slot))?; + slot_state.original_value = original_value; + Some(original_value) + } +} diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs index a6f5f0f..9d0e813 100644 --- a/crates/revm/src/token_fee.rs +++ b/crates/revm/src/token_fee.rs @@ -9,7 +9,7 @@ use alloy_evm::Database; use alloy_primitives::{Address, Bytes, U256, address, keccak256}; use morph_chainspec::hardfork::MorphHardfork; use revm::SystemCallEvm; -use revm::{Inspector, context_interface::result::EVMError, inspector::NoOpInspector}; +use revm::{context_interface::result::EVMError, inspector::NoOpInspector}; use crate::evm::MorphContext; use crate::{MorphEvm, MorphInvalidTransaction}; @@ -27,7 +27,7 @@ const PRICE_RATIO_SLOT: U256 = U256::from_limbs([153u64, 0, 0, 0]); /// /// Contains the token parameters fetched from the L2 Token Registry contract. /// These parameters are used to calculate gas fees in alternative ERC20 tokens. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct TokenFeeInfo { /// The fee token address. pub token_address: Address, @@ -97,9 +97,12 @@ impl TokenFeeInfo { /// Calculate the token amount required for a given ETH amount. /// /// Uses the price ratio and scale to convert ETH value to token amount. + #[inline] pub fn eth_to_token_amount(&self, eth_amount: U256) -> U256 { + // If price_ratio or scale is zero (misconfigured token), return MAX to prevent + // free-ride transactions. The caller's balance check will reject the tx. if self.price_ratio.is_zero() || self.scale.is_zero() { - return U256::ZERO; + return U256::MAX; } // token_amount = eth_amount * scale / price_ratio @@ -122,7 +125,7 @@ fn read_registry_entry( // Get the base slot for this token_id in tokenRegistry mapping let mut token_id_bytes = [0u8; 32]; token_id_bytes[30..32].copy_from_slice(&token_id.to_be_bytes()); - let base = compute_mapping_slot(TOKEN_REGISTRY_SLOT, token_id_bytes.to_vec()); + let base = compute_mapping_slot(TOKEN_REGISTRY_SLOT, &token_id_bytes); // TokenInfo struct layout in storage (Solidity packing): // base + 0: tokenAddress (20 bytes) + padding @@ -156,7 +159,7 @@ fn read_registry_entry( db, L2_TOKEN_REGISTRY_ADDRESS, PRICE_RATIO_SLOT, - token_id_bytes.to_vec(), + &token_id_bytes, )?; Ok(Some(TokenRegistryEntry { @@ -173,10 +176,15 @@ fn read_registry_entry( /// /// For `mapping(keyType => valueType)` at slot `base_slot`, /// the value for `key` is at `keccak256(key ++ base_slot)`. -pub fn compute_mapping_slot(base_slot: U256, mut key: Vec) -> U256 { - let mut preimage = base_slot.to_be_bytes_vec(); - key.append(&mut preimage); - U256::from_be_bytes(keccak256(key).0) +/// +/// Uses a stack-allocated `[u8; 64]` preimage buffer (32-byte key + 32-byte slot) +/// to avoid heap allocations on every call. +#[inline] +pub fn compute_mapping_slot(base_slot: U256, key: &[u8; 32]) -> U256 { + let mut preimage = [0u8; 64]; + preimage[..32].copy_from_slice(key); + preimage[32..].copy_from_slice(&base_slot.to_be_bytes::<32>()); + U256::from_be_bytes(keccak256(preimage).0) } /// Calculate mapping slot for an address key (left-padded to 32 bytes). @@ -184,15 +192,16 @@ pub fn compute_mapping_slot(base_slot: U256, mut key: Vec) -> U256 { pub fn compute_mapping_slot_for_address(base_slot: U256, account: Address) -> U256 { let mut key = [0u8; 32]; key[12..32].copy_from_slice(account.as_slice()); - compute_mapping_slot(base_slot, key.to_vec()) + compute_mapping_slot(base_slot, &key) } /// Load a value from a mapping in contract storage. +#[inline] fn read_mapping_value( db: &mut DB, contract: Address, base_slot: U256, - key: Vec, + key: &[u8; 32], ) -> Result { db.storage(contract, compute_mapping_slot(base_slot, key)) } @@ -208,8 +217,8 @@ fn read_token_balance_with_fallback( balance_slot: Option, hardfork: MorphHardfork, ) -> Result { - if balance_slot.is_some() { - return read_balance_from_storage(db, token, account, balance_slot); + if let Some(slot) = balance_slot { + return read_balance_from_storage(db, token, account, slot); } // EVM fallback: construct temporary MorphEvm for balanceOf call @@ -224,22 +233,16 @@ fn read_token_balance_with_fallback( } /// Read ERC20 balance directly from storage slot. -/// -/// Returns zero if `balance_slot` is `None`. +#[inline] fn read_balance_from_storage( db: &mut DB, token: Address, account: Address, - balance_slot: Option, + balance_slot: U256, ) -> Result { - match balance_slot { - Some(slot) => { - let mut key = [0u8; 32]; - key[12..32].copy_from_slice(account.as_slice()); - read_mapping_value(db, token, slot, key.to_vec()) - } - None => Ok(U256::ZERO), - } + let mut key = [0u8; 32]; + key[12..32].copy_from_slice(account.as_slice()); + read_mapping_value(db, token, balance_slot, &key) } /// Execute EVM `balanceOf(address)` call. @@ -250,7 +253,6 @@ fn query_balance_via_system_call( ) -> Result> where DB: Database, - I: Inspector>, { let calldata = encode_balance_of_calldata(account); match evm.system_call_one(token, calldata) { @@ -277,7 +279,6 @@ pub fn query_erc20_balance( ) -> Result> where DB: Database, - I: Inspector>, { query_balance_via_system_call(evm, token, account) } @@ -314,9 +315,9 @@ mod tests { fn test_mapping_slot() { // Test that mapping slot calculation produces deterministic results let slot = U256::from(151); - let key = vec![0u8; 32]; - let result1 = compute_mapping_slot(slot, key.clone()); - let result2 = compute_mapping_slot(slot, key); + let key = [0u8; 32]; + let result1 = compute_mapping_slot(slot, &key); + let result2 = compute_mapping_slot(slot, &key); assert_eq!(result1, result2); } @@ -353,7 +354,8 @@ mod tests { let eth_amount = U256::from(1_000_000_000_000_000_000u128); let token_amount = info.eth_to_token_amount(eth_amount); - assert_eq!(token_amount, U256::ZERO); + // Misconfigured token returns MAX to prevent free-ride transactions + assert_eq!(token_amount, U256::MAX); } #[test] diff --git a/crates/revm/src/tx.rs b/crates/revm/src/tx.rs index dbcabb5..60753e3 100644 --- a/crates/revm/src/tx.rs +++ b/crates/revm/src/tx.rs @@ -9,7 +9,6 @@ use alloy_eips::eip2718::Encodable2718; use alloy_eips::eip2930::AccessList; use alloy_eips::eip7702::RecoveredAuthority; use alloy_primitives::{Address, B256, Bytes, Signature, TxKind, U256}; -use alloy_rlp::Decodable; use morph_primitives::{L1_TX_TYPE_ID, MORPH_TX_TYPE_ID, MorphTxEnvelope, TxMorph}; use reth_evm::{FromRecoveredTx, FromTxWithEncoded, ToTxEnv, TransactionEnv}; use revm::context::{Transaction, TxEnv}; @@ -49,17 +48,6 @@ pub struct MorphTxEnv { pub reference: Option, /// Memo field for arbitrary data. pub memo: Option, - /// Saved original DB values for storage slots modified by token fee deduction. - /// - /// In revm-state 9.0.0, `EvmStorageSlot::mark_warm_with_transaction_id` resets - /// `original_value = present_value` when a cold slot becomes warm. This breaks - /// EIP-2200 gas calculation for slots that were modified during pre-transaction - /// fee deduction and then marked cold. - /// - /// We save `(contract_address, storage_key, original_db_value)` here so that - /// the custom SLOAD instruction can restore `original_value` after the warm - /// transition, preserving the correct dirty/clean determination for SSTORE gas. - pub fee_slot_original_values: Vec<(Address, U256, U256)>, } impl MorphTxEnv { @@ -73,7 +61,6 @@ impl MorphTxEnv { fee_limit: None, reference: None, memo: None, - fee_slot_original_values: Vec::new(), } } @@ -140,7 +127,7 @@ impl MorphTxEnv { Some(TxMorph { chain_id: self.chain_id().unwrap_or(fallback_chain_id), nonce: self.inner.nonce, - gas_limit: self.gas_limit() as u128, + gas_limit: self.gas_limit(), max_fee_per_gas: self.max_fee_per_gas(), max_priority_fee_per_gas: self.max_priority_fee_per_gas().unwrap_or_default(), to: self.kind(), @@ -153,7 +140,7 @@ impl MorphTxEnv { // If version is missing (e.g. from an older transaction type), we fallback to V1 // to safely overestimate the L1 fee, ensuring we don't underprice the transaction. tracing::debug!( - target: "morph::revm", + target: "morph::evm", "MorphTx version not set, falling back to V1 for L1 fee calculation to safely overestimate" ); morph_primitives::transaction::morph_transaction::MORPH_TX_VERSION_1 @@ -240,11 +227,20 @@ impl MorphTxEnv { fn from_tx_with_rlp_bytes(tx: &MorphTxEnvelope, signer: Address, rlp_bytes: Bytes) -> Self { let tx_type: u8 = tx.tx_type().into(); - // Extract MorphTx fields for TxMorph (type 0x7F) - let morph_tx_info = if tx_type == MORPH_TX_TYPE_ID { - extract_morph_tx_fields_from_rlp(&rlp_bytes) - } else { - None + // Extract MorphTx fields directly from the typed envelope — avoids an + // unnecessary encode→decode RLP roundtrip that the old helper performed. + let morph_tx_info = match tx { + MorphTxEnvelope::Morph(signed) => { + let morph = signed.tx(); + Some(DecodedMorphTxFields { + version: morph.version, + fee_token_id: morph.fee_token_id, + fee_limit: morph.fee_limit, + reference: morph.reference, + memo: morph.memo.clone(), + }) + } + _ => None, }; // Build TxEnv from the transaction @@ -309,28 +305,6 @@ struct DecodedMorphTxFields { memo: Option, } -/// Extract all MorphTx fields from RLP-encoded TxMorph bytes. -/// -/// The bytes should be EIP-2718 encoded (type byte + RLP payload). -/// Returns None if decoding fails. -fn extract_morph_tx_fields_from_rlp(rlp_bytes: &Bytes) -> Option { - if rlp_bytes.is_empty() { - return None; - } - - // Skip the type byte (0x7F) and decode the TxMorph - let payload = &rlp_bytes[1..]; - TxMorph::decode(&mut &payload[..]) - .map(|tx| DecodedMorphTxFields { - version: tx.version, - fee_token_id: tx.fee_token_id, - fee_limit: tx.fee_limit, - reference: tx.reference, - memo: tx.memo, - }) - .ok() -} - impl Deref for MorphTxEnv { type Target = TxEnv; diff --git a/crates/rpc/src/eth/transaction.rs b/crates/rpc/src/eth/transaction.rs index cae8b72..650fe15 100644 --- a/crates/rpc/src/eth/transaction.rs +++ b/crates/rpc/src/eth/transaction.rs @@ -38,7 +38,7 @@ impl FromConsensusTx for MorphRpcTransaction { let fee_token_id = tx.fee_token_id().map(U64::from); let fee_limit = tx.fee_limit(); let reference = tx.reference(); - let memo = tx.memo(); + let memo = tx.memo().cloned(); let effective_gas_price = tx_info.base_fee.map(|base_fee| { tx.effective_tip_per_gas(base_fee) @@ -267,7 +267,7 @@ fn try_build_morph_tx_from_request( let chain_id = req .chain_id .ok_or("missing chain_id for morph transaction")?; - let gas_limit = req.gas.unwrap_or_default() as u128; + let gas_limit = req.gas.unwrap_or_default(); let nonce = req.nonce.unwrap_or_default(); let max_fee_per_gas = req.max_fee_per_gas.or(req.gas_price).unwrap_or_default(); let max_priority_fee_per_gas = req.max_priority_fee_per_gas.unwrap_or_default(); @@ -275,7 +275,7 @@ fn try_build_morph_tx_from_request( let input = req.input.clone().into_input().unwrap_or_default(); let to = req.to.unwrap_or(TxKind::Create); - Ok(Some(TxMorph { + let morph_tx = TxMorph { chain_id, nonce, gas_limit, @@ -290,7 +290,13 @@ fn try_build_morph_tx_from_request( version, reference, memo, - })) + }; + + // Validate all MorphTx constraints: version-specific rules, gas fee ordering, + // and memo length. This catches invalid combinations early at the RPC layer. + morph_tx.validate()?; + + Ok(Some(morph_tx)) } #[cfg(test)] diff --git a/crates/txpool/src/error.rs b/crates/txpool/src/error.rs index 8d0ccce..497c885 100644 --- a/crates/txpool/src/error.rs +++ b/crates/txpool/src/error.rs @@ -33,14 +33,6 @@ pub enum MorphTxError { token_id: u16, }, - /// The fee_limit is lower than the required token amount. - FeeLimitTooLow { - /// The fee_limit specified in the transaction. - fee_limit: U256, - /// The required token amount for the transaction. - required: U256, - }, - /// Insufficient ERC20 token balance to pay for gas. InsufficientTokenBalance { /// The token ID. @@ -69,6 +61,12 @@ pub enum MorphTxError { /// Error message. message: String, }, + + /// MorphTx format validation failed (version, memo length, gas fee ordering). + InvalidFormat { + /// Reason for the validation failure. + reason: String, + }, } impl fmt::Display for MorphTxError { @@ -89,15 +87,6 @@ impl fmt::Display for MorphTxError { Self::InvalidPriceRatio { token_id } => { write!(f, "token ID {token_id} has invalid price ratio (zero)") } - Self::FeeLimitTooLow { - fee_limit, - required, - } => { - write!( - f, - "fee_limit ({fee_limit}) is lower than required token amount ({required})" - ) - } Self::InsufficientTokenBalance { token_id, token_address, @@ -119,6 +108,9 @@ impl fmt::Display for MorphTxError { Self::TokenInfoFetchFailed { token_id, message } => { write!(f, "failed to fetch token info for ID {token_id}: {message}") } + Self::InvalidFormat { reason } => { + write!(f, "invalid MorphTx format: {reason}") + } } } } @@ -132,14 +124,14 @@ impl PoolTransactionError for MorphTxError { match self { // Missing/invalid MorphTx fee fields indicate malformed transaction input. Self::InvalidTokenId => true, + // Format violations (bad version, memo too long, etc.) are malformed input. + Self::InvalidFormat { .. } => true, // Token not found or not active - could be due to temporary state, not penalizable Self::TokenNotFound { .. } | Self::TokenNotActive { .. } => false, // Invalid price ratio - configuration issue, not penalizable Self::InvalidPriceRatio { .. } => false, // Insufficient balance or fee limit - normal validation failure - Self::FeeLimitTooLow { .. } - | Self::InsufficientTokenBalance { .. } - | Self::InsufficientEthForValue { .. } => false, + Self::InsufficientTokenBalance { .. } | Self::InsufficientEthForValue { .. } => false, // Fetch failures - infrastructure issue, not penalizable Self::TokenInfoFetchFailed { .. } => false, } @@ -186,13 +178,6 @@ mod tests { assert!(err.to_string().contains("token ID 2")); assert!(err.to_string().contains("not active")); - let err = MorphTxError::FeeLimitTooLow { - fee_limit: U256::from(100), - required: U256::from(200), - }; - assert!(err.to_string().contains("100")); - assert!(err.to_string().contains("200")); - let err = MorphTxError::InsufficientTokenBalance { token_id: 1, token_address: address!("1234567890123456789012345678901234567890"), diff --git a/crates/txpool/src/maintain.rs b/crates/txpool/src/maintain.rs index 2c32851..1b469b4 100644 --- a/crates/txpool/src/maintain.rs +++ b/crates/txpool/src/maintain.rs @@ -21,12 +21,12 @@ //! and `demoteUnexecutables` (tx_pool.go), but implemented as a separate //! maintenance task since we cannot modify reth's internal pool logic. +use crate::MorphPooledTransaction; use alloy_consensus::Transaction; -use alloy_eips::eip2718::Encodable2718; +use alloy_consensus::Typed2718; use alloy_primitives::{Address, TxHash, U256}; use futures::StreamExt; use morph_chainspec::hardfork::MorphHardforks; -use morph_primitives::MorphTxEnvelope; use morph_revm::L1BlockInfo; use reth_chainspec::ChainSpecProvider; use reth_primitives_traits::AlloyBlockHeader; @@ -34,8 +34,8 @@ use reth_provider::CanonStateSubscriptions; use reth_revm::Database; use reth_revm::database::StateProviderDatabase; use reth_storage_api::StateProviderFactory; -use reth_transaction_pool::{EthPoolTransaction, PoolTransaction, TransactionPool}; -use std::collections::{HashMap, HashSet}; +use reth_transaction_pool::{PoolTransaction, TransactionPool}; +use std::collections::HashMap; /// Sender-level rolling affordability budget used during maintenance revalidation. #[derive(Debug, Clone, Default)] @@ -113,8 +113,7 @@ fn consume_token_budget( /// pub async fn maintain_morph_pool(pool: Pool, client: Client) where - Pool: TransactionPool + Clone, - Pool::Transaction: EthPoolTransaction, + Pool: TransactionPool + Clone, Client: ChainSpecProvider + StateProviderFactory + CanonStateSubscriptions @@ -123,12 +122,12 @@ where { let mut chain_events = client.canonical_state_stream(); - tracing::info!(target: "morph_txpool::maintain", "Starting MorphTx maintenance task"); + tracing::info!(target: "morph::txpool::maintain", "Starting MorphTx maintenance task"); loop { // Wait for the next canonical state change let Some(event) = chain_events.next().await else { - tracing::debug!(target: "morph_txpool::maintain", "Chain event stream ended"); + tracing::debug!(target: "morph::txpool::maintain", "Chain event stream ended"); break; }; @@ -137,7 +136,7 @@ where let block_timestamp = new_tip.timestamp(); tracing::trace!( - target: "morph_txpool::maintain", + target: "morph::txpool::maintain", block_number, "Processing new block for MorphTx validation" ); @@ -153,7 +152,7 @@ where .pending .iter() .chain(all_txs.queued.iter()) - .filter(|tx| tx.transaction.clone_into_consensus().is_morph_tx()) + .filter(|tx| tx.transaction.ty() == morph_primitives::MORPH_TX_TYPE_ID) .collect(); if morph_txs.is_empty() { @@ -165,7 +164,7 @@ where Ok(provider) => provider, Err(err) => { tracing::warn!( - target: "morph_txpool::maintain", + target: "morph::txpool::maintain", %err, "Failed to get state provider for MorphTx revalidation" ); @@ -180,7 +179,7 @@ where Ok(info) => info, Err(err) => { tracing::warn!( - target: "morph_txpool::maintain", + target: "morph::txpool::maintain", ?err, "Failed to fetch L1 block info for MorphTx revalidation" ); @@ -189,7 +188,7 @@ where }; tracing::trace!( - target: "morph_txpool::maintain", + target: "morph::txpool::maintain", count = morph_txs.len(), "Revalidating MorphTx transactions" ); @@ -202,11 +201,10 @@ where } // Revalidate each sender's MorphTx set and collect invalid ones - let mut to_remove: HashSet = HashSet::new(); + let mut to_remove: Vec = Vec::new(); for (sender, mut sender_txs) in txs_by_sender { - sender_txs - .sort_by_key(|pooled_tx| pooled_tx.transaction.clone_into_consensus().nonce()); + sender_txs.sort_by_key(|pooled_tx| pooled_tx.transaction.nonce()); // Initialize sender ETH budget once. let eth_balance = match db.basic(sender) { @@ -214,7 +212,7 @@ where Ok(None) => U256::ZERO, Err(err) => { tracing::warn!( - target: "morph_txpool::maintain", + target: "morph::txpool::maintain", ?sender, ?err, "Failed to get account balance" @@ -230,16 +228,15 @@ where for pooled_tx in sender_txs { let tx = &pooled_tx.transaction; - let consensus_tx = tx.clone_into_consensus(); + // Access the consensus tx by reference (via Deref chain) instead of + // cloning. Use the pool tx's cached EIP-2718 encoding for L1 fee. + let consensus_tx = tx.transaction(); - // Calculate L1 data fee for this transaction. - let mut encoded = Vec::with_capacity(consensus_tx.encode_2718_len()); - consensus_tx.encode_2718(&mut encoded); - let l1_data_fee = l1_block_info.calculate_tx_l1_cost(&encoded, hardfork); + let l1_data_fee = l1_block_info.calculate_tx_l1_cost(tx.encoded_2718(), hardfork); // Use shared validation logic first with current sender ETH budget. let input = crate::MorphTxValidationInput { - consensus_tx: &consensus_tx, + consensus_tx, sender, eth_balance: budget.eth_balance, l1_data_fee, @@ -250,13 +247,13 @@ where Ok(v) => v, Err(err) => { tracing::debug!( - target: "morph_txpool::maintain", + target: "morph::txpool::maintain", tx_hash = ?tx.hash(), ?sender, ?err, "Removing MorphTx: validation failed" ); - to_remove.insert(*tx.hash()); + to_remove.push(*tx.hash()); break; } }; @@ -286,7 +283,7 @@ where }; if !affordable { tracing::debug!( - target: "morph_txpool::maintain", + target: "morph::txpool::maintain", tx_hash = ?tx.hash(), ?sender, uses_token_fee = validation.uses_token_fee, @@ -294,19 +291,20 @@ where required_token_amount = ?validation.required_token_amount, "Removing MorphTx: insufficient cumulative sender budget" ); - to_remove.insert(*tx.hash()); + to_remove.push(*tx.hash()); break; } } } - // Remove invalid transactions + // Remove invalid transactions and all higher-nonce descendants from the same sender. + // Using remove_transactions_and_descendants ensures that nonce-dependent txs are cleaned + // up immediately rather than becoming orphans that are re-validated every block. if !to_remove.is_empty() { let count = to_remove.len(); - let hashes: Vec<_> = to_remove.into_iter().collect(); - pool.remove_transactions(hashes); + pool.remove_transactions_and_descendants(to_remove); tracing::info!( - target: "morph_txpool::maintain", + target: "morph::txpool::maintain", count, block_number, "Removed invalid MorphTx transactions" diff --git a/crates/txpool/src/morph_tx_validation.rs b/crates/txpool/src/morph_tx_validation.rs index 2a781d8..1dd9652 100644 --- a/crates/txpool/src/morph_tx_validation.rs +++ b/crates/txpool/src/morph_tx_validation.rs @@ -4,7 +4,6 @@ //! that is used by both the validator (for new transactions) and the maintenance //! task (for revalidating existing transactions). -use alloy_consensus::Transaction; use alloy_evm::Database; use alloy_primitives::{Address, U256}; use morph_chainspec::hardfork::MorphHardfork; @@ -46,15 +45,29 @@ pub struct MorphTxValidationResult { /// Validates a MorphTx transaction's token-related fields. /// /// This is the main entry point for MorphTx validation. It: -/// 1. Validates ETH balance >= tx.value() (value is still paid in ETH) -/// 2. For `fee_token_id > 0`, validates token balance with REVM-compatible fee_limit semantics -/// 3. For `fee_token_id == 0`, validates ETH can cover full tx cost + L1 data fee +/// 1. Validates structural MorphTx rules (`version`, `fee_limit`, memo length, fee ordering) +/// 2. Validates ETH balance >= tx.value() (value is still paid in ETH) +/// 3. For `fee_token_id > 0`, validates token balance with REVM-compatible fee_limit semantics +/// 4. For `fee_token_id == 0`, validates ETH can cover full tx cost + L1 data fee /// pub fn validate_morph_tx( db: &mut DB, input: &MorphTxValidationInput<'_>, ) -> Result { - let tx_value = input.consensus_tx.value(); + // Keep MorphTx structural validation in the shared path so both initial + // admission and background revalidation enforce the same invariants. + let morph_tx = match input.consensus_tx { + MorphTxEnvelope::Morph(signed) => signed.tx(), + _ => return Err(MorphTxError::InvalidTokenId), + }; + + if let Err(reason) = morph_tx.validate() { + return Err(MorphTxError::InvalidFormat { + reason: reason.to_string(), + }); + } + + let tx_value = morph_tx.value; if tx_value > input.eth_balance { return Err(MorphTxError::InsufficientEthForValue { balance: input.eth_balance, @@ -62,16 +75,12 @@ pub fn validate_morph_tx( }); } - let fields = input - .consensus_tx - .morph_fields() - .ok_or(MorphTxError::InvalidTokenId)?; - let fee_token_id = fields.fee_token_id; - let fee_limit = fields.fee_limit; + let fee_token_id = morph_tx.fee_token_id; + let fee_limit = morph_tx.fee_limit; // Shared fee components used by both ETH-fee and token-fee branches. - let gas_limit = U256::from(input.consensus_tx.gas_limit()); - let max_fee_per_gas = U256::from(input.consensus_tx.max_fee_per_gas()); + let gas_limit = U256::from(morph_tx.gas_limit); + let max_fee_per_gas = U256::from(morph_tx.max_fee_per_gas); let gas_fee = gas_limit.saturating_mul(max_fee_per_gas); let total_eth_fee = gas_fee.saturating_add(input.l1_data_fee); let total_eth_cost = total_eth_fee.saturating_add(tx_value); @@ -147,12 +156,14 @@ pub fn validate_morph_tx( #[cfg(test)] mod tests { use super::*; - use alloy_primitives::address; + use alloy_consensus::Signed; + use alloy_primitives::{B256, Signature, TxKind, address}; + use morph_primitives::{TxMorph, transaction::morph_transaction::MORPH_TX_VERSION_1}; + use reth_revm::revm::database::EmptyDB; #[test] fn test_morph_tx_validation_input_construction() { - use alloy_consensus::{Signed, TxEip1559}; - use alloy_primitives::{B256, Signature, TxKind}; + use alloy_consensus::TxEip1559; let sender = address!("1000000000000000000000000000000000000001"); @@ -187,4 +198,47 @@ mod tests { assert_eq!(input.eth_balance, U256::from(1_000_000_000_000_000_000u128)); assert_eq!(input.l1_data_fee, U256::from(100_000)); } + + #[test] + fn test_validate_morph_tx_rejects_invalid_format_before_state_checks() { + let sender = address!("1000000000000000000000000000000000000001"); + let tx = TxMorph { + chain_id: 2818, + nonce: 0, + gas_limit: 21_000, + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::ZERO, + access_list: Default::default(), + version: MORPH_TX_VERSION_1, + fee_token_id: 0, + fee_limit: U256::from(1u64), + reference: Some(B256::ZERO), + memo: None, + input: Default::default(), + }; + let envelope = MorphTxEnvelope::Morph(Signed::new_unchecked( + tx, + Signature::test_signature(), + B256::ZERO, + )); + let input = MorphTxValidationInput { + consensus_tx: &envelope, + sender, + eth_balance: U256::from(1_000_000_000_000_000_000u128), + l1_data_fee: U256::ZERO, + hardfork: MorphHardfork::Viridian, + }; + let mut db = EmptyDB::default(); + + let err = validate_morph_tx(&mut db, &input).unwrap_err(); + + assert_eq!( + err, + MorphTxError::InvalidFormat { + reason: "version 1 MorphTx cannot have FeeLimit when FeeTokenID is 0".to_string(), + } + ); + } } diff --git a/crates/txpool/src/validator.rs b/crates/txpool/src/validator.rs index b53ac94..7e27047 100644 --- a/crates/txpool/src/validator.rs +++ b/crates/txpool/src/validator.rs @@ -9,7 +9,7 @@ use crate::MorphTxError; use alloy_consensus::{BlockHeader, Transaction}; -use alloy_eips::Encodable2718; +use alloy_eips::{Encodable2718, Typed2718}; use alloy_primitives::{Address, U256}; use morph_chainspec::hardfork::MorphHardforks; use morph_primitives::MorphTxEnvelope; @@ -51,7 +51,7 @@ impl MorphL1BlockInfo { /// Returns the current L1 block info. pub fn l1_block_info(&self) -> L1BlockInfo { - self.l1_block_info.read().clone() + *self.l1_block_info.read() } /// Updates the L1 block info. @@ -185,7 +185,7 @@ where { Ok(provider) => provider, Err(err) => { - tracing::warn!(target: "morph_txpool", %err, "Failed to get state provider for L1 block info update"); + tracing::warn!(target: "morph::txpool", %err, "Failed to get state provider for L1 block info update"); return; } }; @@ -200,7 +200,7 @@ where *self.block_info.l1_block_info.write() = l1_block_info; } Err(err) => { - tracing::warn!(target: "morph_txpool", ?err, "Failed to fetch L1 block info"); + tracing::warn!(target: "morph::txpool", ?err, "Failed to fetch L1 block info"); } } } @@ -240,7 +240,7 @@ where let outcome = self.inner.validate_one(origin, transaction); if outcome.is_invalid() || outcome.is_error() { - tracing::trace!(target: "morph_txpool", ?outcome, "tx pool validation failed"); + tracing::trace!(target: "morph::txpool", ?outcome, "tx pool validation failed"); return outcome; } @@ -254,22 +254,25 @@ where authorities, } = outcome { - let l1_block_info = self.block_info.l1_block_info.read().clone(); + let l1_block_info = *self.block_info.l1_block_info.read(); let hardfork = self .chain_spec() .morph_hardfork_at(self.block_number(), self.block_timestamp()); - // Calculate L1 data fee (always calculated for all transactions) + // Calculate L1 data fee (always calculated for all transactions). + // Clone consensus tx once — reused for both L1 fee encoding and MorphTx validation. let consensus_tx = valid_tx.transaction().clone_into_consensus(); let mut encoded = Vec::with_capacity(consensus_tx.encode_2718_len()); consensus_tx.encode_2718(&mut encoded); let l1_data_fee = l1_block_info.calculate_tx_l1_cost(&encoded, hardfork); if is_morph_tx { - // MorphTx: validate ERC20 token balance + // MorphTx: validate structural rules and ERC20 token balance via + // the shared helper used by both admission and maintenance. + // Pass &MorphTxEnvelope directly to avoid a second clone_into_consensus(). let sender = valid_tx.transaction().sender(); let validation = match self.validate_morph_tx_balance( - valid_tx.transaction(), + &consensus_tx, sender, balance, l1_data_fee, @@ -335,6 +338,9 @@ where /// Validates MorphTx (0x7F) ERC20 token balance and fee_limit. /// + /// Accepts `&Recovered` directly (already cloned by the caller) + /// to avoid a redundant second `clone_into_consensus()`. + /// /// This method performs the following checks (reference: go-ethereum tx_pool.go:727-791): /// 1. `fee_token_id == 0`: ETH-fee path, require ETH affordability for `cost + l1_fee` /// 2. `fee_token_id > 0`: token must be registered and active in L2TokenRegistry @@ -343,14 +349,12 @@ where /// 5. ETH balance must be >= transaction value (value is still in ETH) fn validate_morph_tx_balance( &self, - tx: &Tx, + consensus_tx: &reth_primitives_traits::Recovered, sender: Address, eth_balance: U256, l1_data_fee: U256, hardfork: morph_chainspec::hardfork::MorphHardfork, ) -> Result { - let consensus_tx = tx.clone_into_consensus(); - // Get state provider for token info lookup let provider = self .client() @@ -364,7 +368,7 @@ where // Use shared validation logic with unified API (includes ETH balance check) let input = crate::MorphTxValidationInput { - consensus_tx: &consensus_tx, + consensus_tx, sender, eth_balance, l1_data_fee, @@ -379,7 +383,7 @@ where .unwrap_or_default(); tracing::trace!( - target: "morph_txpool", + target: "morph::txpool", fee_token_id = ?consensus_tx.fee_token_id(), fee_limit = ?consensus_tx.fee_limit(), uses_token_fee = result.uses_token_fee, @@ -442,19 +446,13 @@ where } /// Helper function to check if a transaction is an L1 message. -fn is_l1_message(tx: &Tx) -> bool -where - Tx: EthPoolTransaction, -{ - tx.clone_into_consensus().is_l1_msg() +fn is_l1_message(tx: &impl Typed2718) -> bool { + tx.ty() == morph_primitives::L1_TX_TYPE_ID } /// Helper function to check if a transaction is a MorphTx (0x7F). -fn is_morph_tx(tx: &Tx) -> bool -where - Tx: EthPoolTransaction, -{ - tx.clone_into_consensus().is_morph_tx() +fn is_morph_tx(tx: &impl Typed2718) -> bool { + tx.ty() == morph_primitives::MORPH_TX_TYPE_ID } #[cfg(test)] diff --git a/local-test-hoodi/.gitignore b/local-test-hoodi/.gitignore new file mode 100644 index 0000000..30820c7 --- /dev/null +++ b/local-test-hoodi/.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 2910/) +[0-9]*/ + +# Downloaded artifacts +hoodi-data.zip +hoodi-data/ +.config-extract/ +config-prep.*/ diff --git a/local-test-hoodi/check-state-root.sh b/local-test-hoodi/check-state-root.sh new file mode 100755 index 0000000..5b88233 --- /dev/null +++ b/local-test-hoodi/check-state-root.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +# ─── Configuration ──────────────────────────────────────────────────────────── +: "${STATE_ROOT_CHECK_BIN:=./target/release/state-root-check}" +: "${CHAIN:=morph-hoodi}" +: "${GETH_RPC_URL:=}" +: "${CHECK_BLOCK:=}" # specific block to check (default: latest) +: "${BISECT:=0}" # set to 1 to bisect for first mismatch +: "${BISECT_FROM:=0}" # bisect range start +: "${BISECT_TO:=}" # bisect range end (default: latest) + +# ─── Build ──────────────────────────────────────────────────────────────────── + +echo "Building state-root-check..." +cargo build --release -p state-root-check +echo "Build complete: ${STATE_ROOT_CHECK_BIN}" +echo + +# ─── Run ────────────────────────────────────────────────────────────────────── + +args=( + --datadir "${RETH_DATA_DIR}" + --chain "${CHAIN}" +) + +if [[ "${BISECT}" == "1" ]]; then + # Bisect mode: find first mismatch in a range + if [[ -z "${GETH_RPC_URL}" ]]; then + echo "ERROR: --bisect requires GETH_RPC_URL" + echo "Usage: GETH_RPC_URL=http://localhost:8546 BISECT=1 $0" + exit 1 + fi + args+=(--bisect --from-block "${BISECT_FROM}") + if [[ -n "${BISECT_TO}" ]]; then + args+=(--to-block "${BISECT_TO}") + else + # Default to latest block from reth RPC + latest=$(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 "${latest}" && "${latest}" != "null" ]]; then + args+=(--to-block "$(printf "%d" "${latest}")") + else + echo "ERROR: cannot determine latest block. Is reth running?" + exit 1 + fi + fi + args+=(--geth-rpc-url "${GETH_RPC_URL}") + + echo "=== Bisect Mode ===" + echo " Range: ${BISECT_FROM} .. ${args[*]: -1}" + echo " Geth: ${GETH_RPC_URL}" +elif [[ -n "${GETH_RPC_URL}" ]]; then + # Compare mode: compare reth vs geth at a specific block + args+=(--geth-rpc-url "${GETH_RPC_URL}") + if [[ -n "${CHECK_BLOCK}" ]]; then + args+=(--block "${CHECK_BLOCK}") + echo "=== Compare Mode (block #${CHECK_BLOCK}) ===" + else + echo "=== Compare Mode (latest block) ===" + fi + echo " Geth: ${GETH_RPC_URL}" +else + # Local-only mode: just compute reth state root + if [[ -n "${CHECK_BLOCK}" ]]; then + args+=(--fresh-root-at "${CHECK_BLOCK}") + echo "=== Local Mode (block #${CHECK_BLOCK}) ===" + else + echo "=== Local Mode (latest block) ===" + fi +fi + +echo " Datadir: ${RETH_DATA_DIR}" +echo + +"${STATE_ROOT_CHECK_BIN}" "${args[@]}" diff --git a/local-test-hoodi/common.sh b/local-test-hoodi/common.sh new file mode 100755 index 0000000..c87cdf1 --- /dev/null +++ b/local-test-hoodi/common.sh @@ -0,0 +1,83 @@ +#!/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-hoodi) +: "${MORPHNODE_BIN:=../morph/node/build/bin/morphnode}" +: "${NODE_HOME:=./local-test-hoodi/node-data}" +: "${JWT_SECRET:=./local-test-hoodi/jwt-secret.txt}" +: "${NODE_LOG_FILE:=./local-test-hoodi/node.log}" +: "${MORPH_NODE_L1_RPC:=${MORPH_NODE_L1_ETH_RPC:-https://ethereum-hoodi-rpc.publicnode.com}}" +: "${MORPH_NODE_DEPOSIT_CONTRACT:=${MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS:-0xd7f39d837f4790b215ba67e0ab63665912648dbe}}" +: "${MORPH_NODE_ROLLUP_CONTRACT:=0x57e0e6dde89dc52c01fe785774271504b1e04664}" +: "${MORPH_NODE_EXTRA_FLAGS:=}" +: "${DOWNLOAD_CONFIG_IF_MISSING:=1}" +: "${HOODI_CONFIG_ZIP_URL:=https://raw.githubusercontent.com/morph-l2/run-morph-node/main/hoodi/data.zip}" +: "${CONFIG_ZIP_PATH:=./local-test-hoodi/hoodi-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-hoodi/geth-data}" +: "${GETH_LOG_FILE:=./local-test-hoodi/geth.log}" + +# Morph-Reth configuration +: "${RETH_BIN:=./target/release/morph-reth}" +: "${RETH_DATA_DIR:=./local-test-hoodi/reth-data}" +: "${RETH_LOG_FILE:=./local-test-hoodi/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:=}" +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-hoodi/geth-start.sh b/local-test-hoodi/geth-start.sh new file mode 100755 index 0000000..6c04721 --- /dev/null +++ b/local-test-hoodi/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 (hoodi)..." + +# 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-hoodi \ + --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-hoodi/geth-stop.sh b/local-test-hoodi/geth-stop.sh new file mode 100755 index 0000000..72df6ab --- /dev/null +++ b/local-test-hoodi/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-hoodi/node-start.sh b/local-test-hoodi/node-start.sh new file mode 100755 index 0000000..cb31877 --- /dev/null +++ b/local-test-hoodi/node-start.sh @@ -0,0 +1,46 @@ +#!/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 (hoodi)..." + +# 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-hoodi already running" + pm2 describe morph-node + exit 0 +fi + +# Ensure log directory exists +mkdir -p "$(dirname "${NODE_LOG_FILE}")" + +# Build node args +args=( + --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}" \ + --l1.rpc "${MORPH_NODE_L1_RPC}" \ + --sync.depositContractAddr "${MORPH_NODE_DEPOSIT_CONTRACT}" \ + --derivation.rollupAddress "${MORPH_NODE_ROLLUP_CONTRACT}" \ + --log.filename "${NODE_LOG_FILE}" +) + +if [[ -n "${MORPH_NODE_EXTRA_FLAGS}" ]]; then + # shellcheck disable=SC2206 + extra_flags=(${MORPH_NODE_EXTRA_FLAGS}) + args+=("${extra_flags[@]}") +fi + +# Start morphnode with pm2 +pm2 start "${MORPHNODE_BIN}" --name morph-node -- "${args[@]}" + +echo "Logs: pm2 logs morph-node" diff --git a/local-test-hoodi/node-stop.sh b/local-test-hoodi/node-stop.sh new file mode 100755 index 0000000..4c01909 --- /dev/null +++ b/local-test-hoodi/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}" + +pm2_stop "morph-node" diff --git a/local-test-hoodi/prepare.sh b/local-test-hoodi/prepare.sh new file mode 100755 index 0000000..31f3980 --- /dev/null +++ b/local-test-hoodi/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-hoodi/.config-extract" "./local-test-hoodi/hoodi-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 hoodi config bundle..." + curl -fL "${HOODI_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}/hoodi-data/node-data/config/config.toml" && -f "${temp_extract_dir}/hoodi-data/node-data/config/genesis.json" ]]; then + bundle_root="${temp_extract_dir}/hoodi-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}/hoodi-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 ${HOODI_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.sh" --yes + else + echo "If replay fails again, run: ./local-test-hoodi/reset.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-hoodi/reset.sh b/local-test-hoodi/reset.sh new file mode 100755 index 0000000..48bf6f6 --- /dev/null +++ b/local-test-hoodi/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 (hoodi: 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-hoodi/reth-start.sh b/local-test-hoodi/reth-start.sh new file mode 100755 index 0000000..9556112 --- /dev/null +++ b/local-test-hoodi/reth-start.sh @@ -0,0 +1,59 @@ +#!/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 (hoodi)..." + +# 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 hoodi + --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.persistence-threshold 256 + --engine.memory-block-buffer-target 16 +) + +# 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 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-hoodi/reth-stop.sh b/local-test-hoodi/reth-stop.sh new file mode 100755 index 0000000..e99d8fb --- /dev/null +++ b/local-test-hoodi/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}" + +pm2_stop "morph-reth" diff --git a/local-test-hoodi/start-all.sh b/local-test-hoodi/start-all.sh new file mode 100755 index 0000000..ef93364 --- /dev/null +++ b/local-test-hoodi/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 Hoodi 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 "Hoodi 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-hoodi/status.sh b/local-test-hoodi/status.sh new file mode 100755 index 0000000..dcd3d9e --- /dev/null +++ b/local-test-hoodi/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 Hoodi 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-hoodi/stop-all.sh b/local-test-hoodi/stop-all.sh new file mode 100755 index 0000000..33b768f --- /dev/null +++ b/local-test-hoodi/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-hoodi/sync-test.sh b/local-test-hoodi/sync-test.sh new file mode 100755 index 0000000..51640e1 --- /dev/null +++ b/local-test-hoodi/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 "==========================================" diff --git a/local-test/bench-sync-multi.sh b/local-test/bench-sync-multi.sh new file mode 100755 index 0000000..0dfcd27 --- /dev/null +++ b/local-test/bench-sync-multi.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Run bench-sync.sh multiple rounds and collect results. + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +: "${ROUNDS:=3}" +: "${TEST_DURATION:=300}" +: "${SKIP_GETH:=0}" +: "${SKIP_RETH:=0}" + +echo "==========================================================" +echo " Multi-Round Benchmark (${ROUNDS} rounds)" +echo " TEST_DURATION=${TEST_DURATION}s per config per round" +echo "==========================================================" +echo + +# Arrays to store per-round results +geth_bps_list=() +reth_bps_list=() + +geth_peak_list=() +reth_peak_list=() + +for round in $(seq 1 "${ROUNDS}"); do + echo "############################################################" + echo " ROUND ${round}/${ROUNDS}" + echo "############################################################" + echo + + # Capture output and parse results + output=$(TEST_DURATION="${TEST_DURATION}" \ + SKIP_GETH="${SKIP_GETH}" \ + SKIP_RETH="${SKIP_RETH}" \ + "${SCRIPT_DIR}/bench-sync.sh" 2>&1) + + echo "${output}" + echo + + # Parse Avg BPS from the RESULTS table + # Format: "Avg BPS 86.25 74.17" + avg_line=$(echo "${output}" | grep "^Avg BPS" || true) + if [[ -n "${avg_line}" ]]; then + geth_avg=$(echo "${avg_line}" | awk '{print $3}') + reth_avg=$(echo "${avg_line}" | awk '{print $4}') + + geth_bps_list+=("${geth_avg}") + reth_bps_list+=("${reth_avg}") + fi + + peak_line=$(echo "${output}" | grep "^Peak BPS" || true) + if [[ -n "${peak_line}" ]]; then + geth_peak=$(echo "${peak_line}" | awk '{print $3}') + reth_peak=$(echo "${peak_line}" | awk '{print $4}') + + geth_peak_list+=("${geth_peak}") + reth_peak_list+=("${reth_peak}") + fi + + echo + echo " Round ${round} complete." + echo +done + +# ─── Compute averages ───────────────────────────────────────────────────────── + +calc_avg() { + local arr=("$@") + local sum=0 + local count=0 + for v in "${arr[@]}"; do + if [[ "${v}" != "0" ]]; then + sum=$(echo "${sum} + ${v}" | bc) + count=$((count + 1)) + fi + done + if [[ ${count} -gt 0 ]]; then + echo "scale=2; ${sum} / ${count}" | bc + else + echo "0" + fi +} + +calc_min() { + local arr=("$@") + local min=999999 + for v in "${arr[@]}"; do + if [[ "${v}" != "0" ]] && [[ $(echo "${v} < ${min}" | bc -l) -eq 1 ]]; then + min="${v}" + fi + done + if [[ "${min}" == "999999" ]]; then echo "0"; else echo "${min}"; fi +} + +calc_max() { + local arr=("$@") + local max=0 + for v in "${arr[@]}"; do + if [[ $(echo "${v} > ${max}" | bc -l) -eq 1 ]]; then + max="${v}" + fi + done + echo "${max}" +} + +# ─── Final Summary ──────────────────────────────────────────────────────────── + +echo "==========================================================" +echo " MULTI-ROUND SUMMARY (${ROUNDS} rounds)" +echo "==========================================================" +echo + +# Per-round data +echo "--- Per-Round Avg BPS ---" +printf "%-8s %12s %12s\n" "Round" "Geth" "Reth" +for i in $(seq 0 $((ROUNDS - 1))); do + printf "%-8s %12s %12s\n" \ + "$((i + 1))" \ + "${geth_bps_list[$i]:-N/A}" \ + "${reth_bps_list[$i]:-N/A}" +done + +echo +echo "--- Aggregated Results (Avg BPS) ---" +geth_mean=$(calc_avg "${geth_bps_list[@]}") +reth_mean=$(calc_avg "${reth_bps_list[@]}") + +geth_min=$(calc_min "${geth_bps_list[@]}") +reth_min=$(calc_min "${reth_bps_list[@]}") + +geth_max=$(calc_max "${geth_bps_list[@]}") +reth_max=$(calc_max "${reth_bps_list[@]}") + +printf "%-12s %12s %12s\n" "" "Geth" "Reth" +printf "%-12s %12s %12s\n" "---" "---" "---" +printf "%-12s %12s %12s\n" "Mean" "${geth_mean}" "${reth_mean}" +printf "%-12s %12s %12s\n" "Min" "${geth_min}" "${reth_min}" +printf "%-12s %12s %12s\n" "Max" "${geth_max}" "${reth_max}" + +echo +echo "--- Peak BPS (per round) ---" +printf "%-8s %12s %12s\n" "Round" "Geth" "Reth" +for i in $(seq 0 $((ROUNDS - 1))); do + printf "%-8s %12s %12s\n" \ + "$((i + 1))" \ + "${geth_peak_list[$i]:-N/A}" \ + "${reth_peak_list[$i]:-N/A}" +done + +# Comparison +echo +echo "--- Geth vs Reth ---" +if [[ $(echo "${geth_mean} > 0 && ${reth_mean} > 0" | bc -l) -eq 1 ]]; then + if [[ $(echo "${reth_mean} > ${geth_mean}" | bc -l) -eq 1 ]]; then + diff_pct=$(echo "scale=1; (${reth_mean} - ${geth_mean}) * 100 / ${reth_mean}" | bc) + echo "Reth faster by ${diff_pct}% (mean: geth=${geth_mean}, reth=${reth_mean})" + elif [[ $(echo "${geth_mean} > ${reth_mean}" | bc -l) -eq 1 ]]; then + diff_pct=$(echo "scale=1; (${geth_mean} - ${reth_mean}) * 100 / ${geth_mean}" | bc) + echo "Geth faster by ${diff_pct}% (mean: geth=${geth_mean}, reth=${reth_mean})" + else + echo "Tie (mean: ${geth_mean})" + fi +else + echo "Insufficient data" +fi + +echo +echo "==========================================================" +echo " Multi-round benchmark complete" +echo "==========================================================" diff --git a/local-test/bench-sync.sh b/local-test/bench-sync.sh new file mode 100755 index 0000000..27806e0 --- /dev/null +++ b/local-test/bench-sync.sh @@ -0,0 +1,310 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Benchmark: Geth vs Reth sync speed comparison. +# No geth RPC URL cross-validation — pure sync speed comparison. + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +# ─── Configuration ──────────────────────────────────────────────────────────── +: "${TEST_DURATION:=300}" # seconds to run each config (5 min default) +: "${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 +} + +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) + + 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)" + + 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() { + 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 Benchmark" +echo " Geth vs Reth" +echo "==========================================================" +echo " Duration per config: ${TEST_DURATION}s" +echo " Sample interval: ${SAMPLE_INTERVAL}s" +echo " Mainnet tip (est): ${MAINNET_TIP}" +echo "==========================================================" +echo + +pm2_check + +# 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" + check_binary "${MORPHNODE_BIN}" "cd ../morph/node && make build" + + echo "=== Phase 1/2: Testing Geth ===" + echo + + full_reset + "${SCRIPT_DIR}/prepare.sh" 2>/dev/null + + echo " Starting morph-geth..." + "${SCRIPT_DIR}/geth-start.sh" + wait_for_rpc "geth" + + echo " Starting morphnode..." + "${SCRIPT_DIR}/node-start.sh" + + echo " Warming up (10s)..." + sleep 10 + + 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} + + 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 + + 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" + check_binary "${MORPHNODE_BIN}" "cd ../morph/node && make build" + + echo "=== Phase 2/2: Testing Reth ===" + echo + + full_reset + "${SCRIPT_DIR}/prepare.sh" 2>/dev/null + + echo " Starting morph-reth..." + "${SCRIPT_DIR}/reth-start.sh" + wait_for_rpc "reth" + + echo " Starting morphnode..." + "${SCRIPT_DIR}/node-start.sh" + + echo " Warming up (10s)..." + sleep 10 + + 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} + + 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 + + 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=$(echo "scale=0; ${MAINNET_TIP} / ${GETH_AVG_BPS}" | bc) + printf "%-20s %s (at %.2f bps)\n" "Geth:" "$(format_duration "${geth_eta}")" "${GETH_AVG_BPS}" +else + printf "%-20s N/A (no data)\n" "Geth:" +fi + +if [[ $(echo "${RETH_AVG_BPS} > 0" | bc -l) -eq 1 ]]; then + reth_eta=$(echo "scale=0; ${MAINNET_TIP} / ${RETH_AVG_BPS}" | bc) + printf "%-20s %s (at %.2f bps)\n" "Reth:" "$(format_duration "${reth_eta}")" "${RETH_AVG_BPS}" +else + printf "%-20s N/A (no data)\n" "Reth:" +fi + +# Comparison +echo +echo "--- Geth vs Reth ---" + +if [[ $(echo "${GETH_AVG_BPS} > 0 && ${RETH_AVG_BPS} > 0" | bc -l) -eq 1 ]]; then + if [[ $(echo "${GETH_AVG_BPS} > ${RETH_AVG_BPS}" | bc -l) -eq 1 ]]; then + diff_pct=$(echo "scale=1; (${GETH_AVG_BPS} - ${RETH_AVG_BPS}) * 100 / ${GETH_AVG_BPS}" | bc) + echo "Geth is faster by ${diff_pct}%" + echo " geth=${GETH_AVG_BPS} bps, reth=${RETH_AVG_BPS} bps" + elif [[ $(echo "${RETH_AVG_BPS} > ${GETH_AVG_BPS}" | bc -l) -eq 1 ]]; then + diff_pct=$(echo "scale=1; (${RETH_AVG_BPS} - ${GETH_AVG_BPS}) * 100 / ${RETH_AVG_BPS}" | bc) + echo "Reth is faster by ${diff_pct}%" + echo " geth=${GETH_AVG_BPS} bps, reth=${RETH_AVG_BPS} bps" + else + echo "Tie: both at ${GETH_AVG_BPS} bps" + fi +else + echo "Insufficient data for comparison" +fi + +echo +echo "==========================================================" +echo " Benchmark complete" +echo "==========================================================" diff --git a/local-test/check-state-root.sh b/local-test/check-state-root.sh new file mode 100755 index 0000000..29a7946 --- /dev/null +++ b/local-test/check-state-root.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +cd "${REPO_ROOT}" + +# ─── Configuration ──────────────────────────────────────────────────────────── +: "${STATE_ROOT_CHECK_BIN:=./target/release/state-root-check}" +: "${CHAIN:=morph}" +: "${GETH_RPC_URL:=}" +: "${CHECK_BLOCK:=}" # specific block to check (default: latest) +: "${BISECT:=0}" # set to 1 to bisect for first mismatch +: "${BISECT_FROM:=0}" # bisect range start +: "${BISECT_TO:=}" # bisect range end (default: latest) + +# ─── Build ──────────────────────────────────────────────────────────────────── + +echo "Building state-root-check..." +cargo build --release -p state-root-check +echo "Build complete: ${STATE_ROOT_CHECK_BIN}" +echo + +# ─── Run ────────────────────────────────────────────────────────────────────── + +args=( + --datadir "${RETH_DATA_DIR}" + --chain "${CHAIN}" +) + +if [[ "${BISECT}" == "1" ]]; then + # Bisect mode: find first mismatch in a range + if [[ -z "${GETH_RPC_URL}" ]]; then + echo "ERROR: --bisect requires GETH_RPC_URL" + echo "Usage: GETH_RPC_URL=http://localhost:8546 BISECT=1 $0" + exit 1 + fi + args+=(--bisect --from-block "${BISECT_FROM}") + if [[ -n "${BISECT_TO}" ]]; then + args+=(--to-block "${BISECT_TO}") + else + # Default to latest block from reth RPC + latest=$(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 "${latest}" && "${latest}" != "null" ]]; then + args+=(--to-block "$(printf "%d" "${latest}")") + else + echo "ERROR: cannot determine latest block. Is reth running?" + exit 1 + fi + fi + args+=(--geth-rpc-url "${GETH_RPC_URL}") + + echo "=== Bisect Mode ===" + echo " Range: ${BISECT_FROM} .. ${args[*]: -1}" + echo " Geth: ${GETH_RPC_URL}" +elif [[ -n "${GETH_RPC_URL}" ]]; then + # Compare mode: compare reth vs geth at a specific block + args+=(--geth-rpc-url "${GETH_RPC_URL}") + if [[ -n "${CHECK_BLOCK}" ]]; then + args+=(--block "${CHECK_BLOCK}") + echo "=== Compare Mode (block #${CHECK_BLOCK}) ===" + else + echo "=== Compare Mode (latest block) ===" + fi + echo " Geth: ${GETH_RPC_URL}" +else + # Local-only mode: just compute reth state root + if [[ -n "${CHECK_BLOCK}" ]]; then + args+=(--fresh-root-at "${CHECK_BLOCK}") + echo "=== Local Mode (block #${CHECK_BLOCK}) ===" + else + echo "=== Local Mode (latest block) ===" + fi +fi + +echo " Datadir: ${RETH_DATA_DIR}" +echo + +"${STATE_ROOT_CHECK_BIN}" "${args[@]}" diff --git a/local-test/common.sh b/local-test/common.sh index 2ab2f36..2a71cc0 100755 --- a/local-test/common.sh +++ b/local-test/common.sh @@ -10,6 +10,9 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" : "${NODE_HOME:=./local-test/node-data}" : "${JWT_SECRET:=./local-test/jwt-secret.txt}" : "${NODE_LOG_FILE:=./local-test/node.log}" +: "${MORPH_NODE_L1_RPC:=${MORPH_NODE_L1_ETH_RPC:-https://ethereum.publicnode.com}}" +: "${MORPH_NODE_DEPOSIT_CONTRACT:=${MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS:-0x3931ade842f5bb8763164bdd81e5361dce6cc1ef}}" +: "${MORPH_NODE_EXTRA_FLAGS:=--mainnet}" : "${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}" @@ -32,8 +35,6 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" : "${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" diff --git a/local-test/node-start.sh b/local-test/node-start.sh index d950781..67d0b6b 100755 --- a/local-test/node-start.sh +++ b/local-test/node-start.sh @@ -22,12 +22,24 @@ fi # Ensure log directory exists mkdir -p "$(dirname "${NODE_LOG_FILE}")" -# Start morphnode with pm2 -pm2 start "${MORPHNODE_BIN}" --name morph-node -- \ +# Build node args +args=( --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}" \ + --l1.rpc "${MORPH_NODE_L1_RPC}" \ + --sync.depositContractAddr "${MORPH_NODE_DEPOSIT_CONTRACT}" \ --log.filename "${NODE_LOG_FILE}" +) + +if [[ -n "${MORPH_NODE_EXTRA_FLAGS}" ]]; then + # shellcheck disable=SC2206 + extra_flags=(${MORPH_NODE_EXTRA_FLAGS}) + args+=("${extra_flags[@]}") +fi + +# Start morphnode with pm2 +pm2 start "${MORPHNODE_BIN}" --name morph-node -- "${args[@]}" echo "Logs: pm2 logs morph-node" diff --git a/local-test/reth-start.sh b/local-test/reth-start.sh index 186543e..7400458 100755 --- a/local-test/reth-start.sh +++ b/local-test/reth-start.sh @@ -39,7 +39,8 @@ args=( --log.file.filter info --morph.max-tx-payload-bytes "${MORPH_MAX_TX_PAYLOAD_BYTES}" --nat none - --engine.legacy-state-root + --engine.persistence-threshold 256 + --engine.memory-block-buffer-target 16 ) # Add optional max-tx-per-block if configured @@ -47,11 +48,6 @@ 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}")