diff --git a/Cargo.lock b/Cargo.lock index 08e8fd2651..2089c7cd2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,8 +343,7 @@ dependencies = [ [[package]] name = "alloy-evm" version = "0.29.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6ba2dafd6327f78f2b59ae539bd5c39c57a01dc76763e92942619d934a7bb" +source = "git+https://github.com/alloy-rs/evm?rev=ec772db#ec772db969c27aefe90e3655a304b4d1077a2005" dependencies = [ "alloy-consensus", "alloy-eips", @@ -355,8 +354,6 @@ dependencies = [ "alloy-sol-types", "auto_impl", "derive_more", - "op-alloy", - "op-revm", "revm", "thiserror 2.0.18", "tracing", @@ -6324,121 +6321,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "op-alloy" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a95dd0974d5e60ffe9342a70cc0033d299244fab01cb16a958eb7352ddba1fa7" -dependencies = [ - "op-alloy-consensus", - "op-alloy-network", - "op-alloy-provider", - "op-alloy-rpc-types", - "op-alloy-rpc-types-engine", -] - -[[package]] -name = "op-alloy-consensus" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadcb964b0aa645d12e952d470c7585d23286d8bcf1ac41589410b6724216b68" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "derive_more", - "serde", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "op-alloy-network" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ea44162d493219cc678aaca1253d46c3aa73aa361326dfa9d406f086dfa135" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types-eth", - "alloy-signer", - "op-alloy-consensus", - "op-alloy-rpc-types", -] - -[[package]] -name = "op-alloy-provider" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83aa8dc34bdf077c8e6d48ff75beff4ac14b428d982c9722483ccd7473c0e114" -dependencies = [ - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types-engine", - "alloy-transport", - "async-trait", - "op-alloy-rpc-types-engine", -] - -[[package]] -name = "op-alloy-rpc-types" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9d16ec9f7810e0623a37edc293d1c05fe9c58a5647f6973fdd93e0b4795015" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "derive_more", - "op-alloy-consensus", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "op-alloy-rpc-types-engine" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0ac5c5ddcbffadcd097d278c717d34849bcc629f6e4f8514eb000ec862def8" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-engine", - "alloy-serde", - "derive_more", - "ethereum_ssz 0.9.1", - "ethereum_ssz_derive 0.9.1", - "op-alloy-consensus", - "serde", - "sha2 0.10.9", - "snap", - "thiserror 2.0.18", -] - -[[package]] -name = "op-revm" -version = "17.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a98f3a512a7e02a1dcf1242b57302d83657b265a665d50ad98d0b158efaf2c" -dependencies = [ - "auto_impl", - "revm", - "serde", -] - [[package]] name = "opaque-debug" version = "0.3.1" @@ -7726,7 +7608,7 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "reth-basic-payload-builder" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7735,6 +7617,7 @@ dependencies = [ "futures-util", "metrics", "reth-chain-state", + "reth-execution-cache", "reth-metrics", "reth-payload-builder", "reth-payload-builder-primitives", @@ -7743,6 +7626,7 @@ dependencies = [ "reth-revm", "reth-storage-api", "reth-tasks", + "reth-trie-parallel", "serde", "tokio", "tracing", @@ -7751,7 +7635,7 @@ dependencies = [ [[package]] name = "reth-chain-state" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -7783,7 +7667,7 @@ dependencies = [ [[package]] name = "reth-chainspec" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-chains", "alloy-consensus", @@ -7803,7 +7687,7 @@ dependencies = [ [[package]] name = "reth-cli" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-genesis", "clap", @@ -7816,7 +7700,7 @@ dependencies = [ [[package]] name = "reth-cli-commands" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-chains", "alloy-consensus", @@ -7899,7 +7783,7 @@ dependencies = [ [[package]] name = "reth-cli-runner" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "reth-tasks", "tokio", @@ -7909,7 +7793,7 @@ dependencies = [ [[package]] name = "reth-cli-util" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-primitives", @@ -7962,7 +7846,7 @@ dependencies = [ [[package]] name = "reth-config" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "eyre", "humantime-serde", @@ -7978,7 +7862,7 @@ dependencies = [ [[package]] name = "reth-consensus" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -7991,7 +7875,7 @@ dependencies = [ [[package]] name = "reth-consensus-common" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8004,7 +7888,7 @@ dependencies = [ [[package]] name = "reth-consensus-debug-client" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8030,7 +7914,7 @@ dependencies = [ [[package]] name = "reth-db" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "derive_more", @@ -8058,7 +7942,7 @@ dependencies = [ [[package]] name = "reth-db-api" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8084,7 +7968,7 @@ dependencies = [ [[package]] name = "reth-db-common" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -8114,7 +7998,7 @@ dependencies = [ [[package]] name = "reth-db-models" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8129,7 +8013,7 @@ dependencies = [ [[package]] name = "reth-discv4" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8154,7 +8038,7 @@ dependencies = [ [[package]] name = "reth-discv5" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -8178,7 +8062,7 @@ dependencies = [ [[package]] name = "reth-dns-discovery" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "dashmap", @@ -8202,7 +8086,7 @@ dependencies = [ [[package]] name = "reth-downloaders" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8237,7 +8121,7 @@ dependencies = [ [[package]] name = "reth-e2e-test-utils" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8294,7 +8178,7 @@ dependencies = [ [[package]] name = "reth-ecies" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "aes", "alloy-primitives", @@ -8322,7 +8206,7 @@ dependencies = [ [[package]] name = "reth-engine-local" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8345,7 +8229,7 @@ dependencies = [ [[package]] name = "reth-engine-primitives" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8370,7 +8254,7 @@ dependencies = [ [[package]] name = "reth-engine-tree" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eip7928", @@ -8426,7 +8310,7 @@ dependencies = [ [[package]] name = "reth-engine-util" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -8454,7 +8338,7 @@ dependencies = [ [[package]] name = "reth-era" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8469,7 +8353,7 @@ dependencies = [ [[package]] name = "reth-era-downloader" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "bytes", @@ -8485,7 +8369,7 @@ dependencies = [ [[package]] name = "reth-era-utils" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8507,7 +8391,7 @@ dependencies = [ [[package]] name = "reth-errors" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "reth-consensus", "reth-execution-errors", @@ -8518,7 +8402,7 @@ dependencies = [ [[package]] name = "reth-eth-wire" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-chains", "alloy-primitives", @@ -8546,7 +8430,7 @@ dependencies = [ [[package]] name = "reth-eth-wire-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-chains", "alloy-consensus", @@ -8567,7 +8451,7 @@ dependencies = [ [[package]] name = "reth-ethereum" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", @@ -8608,7 +8492,7 @@ dependencies = [ [[package]] name = "reth-ethereum-cli" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "clap", "eyre", @@ -8631,7 +8515,7 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8647,7 +8531,7 @@ dependencies = [ [[package]] name = "reth-ethereum-engine-primitives" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8663,7 +8547,7 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -8676,7 +8560,7 @@ dependencies = [ [[package]] name = "reth-ethereum-payload-builder" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8690,6 +8574,7 @@ dependencies = [ "reth-ethereum-primitives", "reth-evm", "reth-evm-ethereum", + "reth-execution-cache", "reth-payload-builder", "reth-payload-builder-primitives", "reth-payload-primitives", @@ -8705,7 +8590,7 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8719,7 +8604,7 @@ dependencies = [ [[package]] name = "reth-etl" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "rayon", "reth-db-api", @@ -8729,7 +8614,7 @@ dependencies = [ [[package]] name = "reth-evm" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8753,7 +8638,7 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8773,7 +8658,7 @@ dependencies = [ [[package]] name = "reth-execution-cache" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "fixed-cache", @@ -8791,7 +8676,7 @@ dependencies = [ [[package]] name = "reth-execution-errors" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-evm", "alloy-primitives", @@ -8804,7 +8689,7 @@ dependencies = [ [[package]] name = "reth-execution-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8823,7 +8708,7 @@ dependencies = [ [[package]] name = "reth-exex" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8861,7 +8746,7 @@ dependencies = [ [[package]] name = "reth-exex-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-primitives", @@ -8875,7 +8760,7 @@ dependencies = [ [[package]] name = "reth-fs-util" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "serde", "serde_json", @@ -8885,7 +8770,7 @@ dependencies = [ [[package]] name = "reth-invalid-block-hooks" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -8913,7 +8798,7 @@ dependencies = [ [[package]] name = "reth-ipc" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "bytes", "futures", @@ -8933,7 +8818,7 @@ dependencies = [ [[package]] name = "reth-libmdbx" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "bitflags 2.11.0", "byteorder", @@ -8950,7 +8835,7 @@ dependencies = [ [[package]] name = "reth-mdbx-sys" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "bindgen", "cc", @@ -8959,7 +8844,7 @@ dependencies = [ [[package]] name = "reth-metrics" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "futures", "metrics", @@ -8971,7 +8856,7 @@ dependencies = [ [[package]] name = "reth-net-banlist" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "ipnet", @@ -8980,7 +8865,7 @@ dependencies = [ [[package]] name = "reth-net-nat" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "futures-util", "if-addrs", @@ -8994,7 +8879,7 @@ dependencies = [ [[package]] name = "reth-network" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9051,7 +8936,7 @@ dependencies = [ [[package]] name = "reth-network-api" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9076,7 +8961,7 @@ dependencies = [ [[package]] name = "reth-network-p2p" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9099,7 +8984,7 @@ dependencies = [ [[package]] name = "reth-network-peers" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9114,7 +8999,7 @@ dependencies = [ [[package]] name = "reth-network-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eip2124", "humantime-serde", @@ -9128,7 +9013,7 @@ dependencies = [ [[package]] name = "reth-nippy-jar" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "anyhow", "bincode", @@ -9145,7 +9030,7 @@ dependencies = [ [[package]] name = "reth-node-api" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-rpc-types-engine", "eyre", @@ -9169,7 +9054,7 @@ dependencies = [ [[package]] name = "reth-node-builder" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9237,7 +9122,7 @@ dependencies = [ [[package]] name = "reth-node-core" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9292,7 +9177,7 @@ dependencies = [ [[package]] name = "reth-node-ethereum" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-network", @@ -9330,7 +9215,7 @@ dependencies = [ [[package]] name = "reth-node-ethstats" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9354,7 +9239,7 @@ dependencies = [ [[package]] name = "reth-node-events" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9378,7 +9263,7 @@ dependencies = [ [[package]] name = "reth-node-metrics" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "bytes", "eyre", @@ -9407,7 +9292,7 @@ dependencies = [ [[package]] name = "reth-node-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "reth-chainspec", "reth-db-api", @@ -9419,19 +9304,22 @@ dependencies = [ [[package]] name = "reth-payload-builder" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-rpc-types", + "derive_more", "futures-util", "metrics", "reth-chain-state", "reth-ethereum-engine-primitives", + "reth-execution-cache", "reth-metrics", "reth-payload-builder-primitives", "reth-payload-primitives", "reth-primitives-traits", + "reth-trie-parallel", "tokio", "tokio-stream", "tracing", @@ -9440,7 +9328,7 @@ dependencies = [ [[package]] name = "reth-payload-builder-primitives" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "pin-project", "reth-payload-primitives", @@ -9452,7 +9340,7 @@ dependencies = [ [[package]] name = "reth-payload-primitives" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9461,7 +9349,6 @@ dependencies = [ "alloy-rpc-types-engine", "auto_impl", "either", - "op-alloy-rpc-types-engine", "reth-chain-state", "reth-chainspec", "reth-errors", @@ -9477,7 +9364,7 @@ dependencies = [ [[package]] name = "reth-payload-validator" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -9520,7 +9407,7 @@ dependencies = [ [[package]] name = "reth-provider" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9566,7 +9453,7 @@ dependencies = [ [[package]] name = "reth-prune" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9595,7 +9482,7 @@ dependencies = [ [[package]] name = "reth-prune-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "arbitrary", @@ -9611,7 +9498,7 @@ dependencies = [ [[package]] name = "reth-revm" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "reth-primitives-traits", @@ -9624,7 +9511,7 @@ dependencies = [ [[package]] name = "reth-rpc" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -9702,7 +9589,7 @@ dependencies = [ [[package]] name = "reth-rpc-api" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eip7928", "alloy-eips", @@ -9733,7 +9620,7 @@ dependencies = [ [[package]] name = "reth-rpc-builder" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-network", "alloy-provider", @@ -9776,7 +9663,7 @@ dependencies = [ [[package]] name = "reth-rpc-convert" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-evm", @@ -9796,7 +9683,7 @@ dependencies = [ [[package]] name = "reth-rpc-engine-api" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-primitives", @@ -9827,7 +9714,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-api" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -9871,7 +9758,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9919,7 +9806,7 @@ dependencies = [ [[package]] name = "reth-rpc-layer" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-rpc-types-engine", "http", @@ -9933,7 +9820,7 @@ dependencies = [ [[package]] name = "reth-rpc-server-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-primitives", @@ -9949,7 +9836,7 @@ dependencies = [ [[package]] name = "reth-stages" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10001,7 +9888,7 @@ dependencies = [ [[package]] name = "reth-stages-api" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-primitives", @@ -10029,7 +9916,7 @@ dependencies = [ [[package]] name = "reth-stages-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "arbitrary", @@ -10043,7 +9930,7 @@ dependencies = [ [[package]] name = "reth-static-file" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "parking_lot", @@ -10063,7 +9950,7 @@ dependencies = [ [[package]] name = "reth-static-file-types" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "clap", @@ -10078,7 +9965,7 @@ dependencies = [ [[package]] name = "reth-storage-api" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10102,7 +9989,7 @@ dependencies = [ [[package]] name = "reth-storage-errors" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-eips", "alloy-primitives", @@ -10120,7 +10007,7 @@ dependencies = [ [[package]] name = "reth-tasks" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "crossbeam-utils", "dashmap", @@ -10141,7 +10028,7 @@ dependencies = [ [[package]] name = "reth-testing-utils" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10157,7 +10044,7 @@ dependencies = [ [[package]] name = "reth-tokio-util" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "tokio", "tokio-stream", @@ -10167,7 +10054,7 @@ dependencies = [ [[package]] name = "reth-tracing" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "clap", "eyre", @@ -10186,7 +10073,7 @@ dependencies = [ [[package]] name = "reth-tracing-otlp" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "clap", "eyre", @@ -10204,7 +10091,7 @@ dependencies = [ [[package]] name = "reth-transaction-pool" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10248,7 +10135,7 @@ dependencies = [ [[package]] name = "reth-trie" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-eips", @@ -10274,7 +10161,7 @@ dependencies = [ [[package]] name = "reth-trie-common" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -10301,7 +10188,7 @@ dependencies = [ [[package]] name = "reth-trie-db" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "metrics", @@ -10321,8 +10208,10 @@ dependencies = [ [[package]] name = "reth-trie-parallel" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ + "alloy-eip7928", + "alloy-evm", "alloy-primitives", "alloy-rlp", "crossbeam-channel", @@ -10338,6 +10227,7 @@ dependencies = [ "reth-tasks", "reth-trie", "reth-trie-sparse", + "revm-state", "thiserror 2.0.18", "tracing", ] @@ -10345,7 +10235,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse" version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?rev=7f4a9a0#7f4a9a05ef38b2f88f900209d0d7f8d67ca148c1" +source = "git+https://github.com/paradigmxyz/reth?rev=b25459035f#b25459035f5946031fc8085b15fbb4c31ff66b46" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -10376,8 +10266,7 @@ dependencies = [ [[package]] name = "revm" version = "36.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0abc15d09cd211e9e73410ada10134069c794d4bcdb787dfc16a1bf0939849c" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "revm-bytecode", "revm-context", @@ -10395,8 +10284,7 @@ dependencies = [ [[package]] name = "revm-bytecode" version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86e468df3cf5cf59fa7ef71a3e9ccabb76bb336401ea2c0674f563104cf3c5e" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "bitvec", "phf", @@ -10407,8 +10295,7 @@ dependencies = [ [[package]] name = "revm-context" version = "15.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eb1f0a76b14d684a444fc52f7bf6b7564bf882599d91ee62e76d602e7a187c7" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "bitvec", "cfg-if", @@ -10424,8 +10311,7 @@ dependencies = [ [[package]] name = "revm-context-interface" version = "16.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc256b27743e2912ca16899568e6652a372eb5d1d573e6edb16c7836b16cf487" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "alloy-eip2930", "alloy-eip7702", @@ -10440,8 +10326,7 @@ dependencies = [ [[package]] name = "revm-database" version = "12.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0a7d6da41061f2c50f99a2632571026b23684b5449ff319914151f4449b6c8" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "alloy-eips", "revm-bytecode", @@ -10454,8 +10339,7 @@ dependencies = [ [[package]] name = "revm-database-interface" version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd497a38a79057b94a049552cb1f925ad15078bc1a479c132aeeebd1d2ccc768" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "auto_impl", "either", @@ -10468,8 +10352,7 @@ dependencies = [ [[package]] name = "revm-handler" version = "17.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1eed729ca9b228ae98688f352235871e9b8be3d568d488e4070f64c56e9d3d" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "auto_impl", "derive-where", @@ -10487,8 +10370,7 @@ dependencies = [ [[package]] name = "revm-inspector" version = "17.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf5102391706513689f91cb3cb3d97b5f13a02e8647e6e9cb7620877ef84847" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "auto_impl", "either", @@ -10505,8 +10387,7 @@ dependencies = [ [[package]] name = "revm-inspectors" version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9487362b728f80dd2033ef5f4d0688453435bbe7caa721fa7e3b8fa25d89242b" +source = "git+https://github.com/paradigmxyz/revm-inspectors?rev=ecc5bc7#ecc5bc71aecf8535e706859c344d9636f035a219" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -10525,8 +10406,7 @@ dependencies = [ [[package]] name = "revm-interpreter" version = "34.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf22f80612bb8f58fd1f578750281f2afadb6c93835b14ae6a4d6b75ca26f445" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "revm-bytecode", "revm-context-interface", @@ -10538,8 +10418,7 @@ dependencies = [ [[package]] name = "revm-precompile" version = "32.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ec11f45deec71e4945e1809736bb20d454285f9167ab53c5159dae1deb603f" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "ark-bls12-381", "ark-bn254", @@ -10553,6 +10432,7 @@ dependencies = [ "cfg-if", "k256", "p256", + "revm-context-interface", "revm-primitives", "ripemd", "secp256k1 0.31.1", @@ -10562,8 +10442,7 @@ dependencies = [ [[package]] name = "revm-primitives" version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfb5ce6cf18b118932bcdb7da05cd9c250f2cb9f64131396b55f3fe3537c35" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "alloy-primitives", "num_enum", @@ -10574,8 +10453,7 @@ dependencies = [ [[package]] name = "revm-state" version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29404707763da607e5d6e4771cb203998c28159279c2f64cc32de08d2814651" +source = "git+https://github.com/bluealloy/revm.git?rev=8891655bb#8891655bb66933f769554b4394e6410f4aa01bed" dependencies = [ "alloy-eip7928", "bitflags 2.11.0", @@ -12036,6 +11914,7 @@ dependencies = [ "reth-node-builder", "reth-node-core", "reth-node-metrics", + "reth-payload-primitives", "reth-rpc-builder", "serde_json", "tempfile", @@ -12155,6 +12034,7 @@ dependencies = [ "reth-node-core", "reth-node-ethereum", "reth-node-metrics", + "reth-payload-primitives", "reth-primitives-traits", "reth-provider", "reth-rpc", diff --git a/Cargo.toml b/Cargo.toml index 2d029b71a0..16f7a40004 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,81 +120,81 @@ tempo-telemetry-util = { path = "crates/telemetry-util", default-features = fals tempo-transaction-pool = { path = "crates/transaction-pool", default-features = false } tempo-validator-config = { path = "crates/validator-config", default-features = false } -reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-chainspec = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0", default-features = false } -reth-cli = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-cli-commands = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-cli-runner = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-cli-util = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-codecs = { version = "0.1.0", default-features = false } -reth-consensus = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-consensus-common = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-db = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-db-api = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-e2e-test-utils = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-engine-local = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-engine-tree = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-errors = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-eth-wire-types = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-etl = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-ethereum-cli = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-ethereum-consensus = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-ethereum-engine-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0", default-features = false } -reth-execution-types = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-evm = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-metrics = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-network-peers = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0", default-features = false } -reth-node-api = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-node-builder = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-node-core = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-node-metrics = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-payload-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } +reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-chainspec = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f", default-features = false } +reth-cli = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-cli-commands = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-cli-runner = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-cli-util = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-codecs = { version = "0.1.0" } +reth-consensus = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-consensus-common = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-db = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-db-api = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-e2e-test-utils = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-engine-local = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-engine-tree = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-errors = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-eth-wire-types = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-ethereum-cli = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-ethereum-consensus = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-ethereum-engine-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f", default-features = false } +reth-execution-types = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-evm = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-metrics = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-network-peers = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f", default-features = false } +reth-node-api = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-node-builder = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-node-core = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-node-metrics = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-payload-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } reth-primitives-traits = { version = "0.1.0", default-features = false } -reth-provider = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-rpc = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-rpc-builder = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-rpc-eth-types = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-rpc-server-types = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-storage-api = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-tracing = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-trie = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-trie-common = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } -reth-trie-db = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0" } +reth-provider = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-rpc = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-rpc-builder = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-rpc-eth-types = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-rpc-server-types = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-storage-api = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-tracing = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-etl = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-trie = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-trie-common = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } +reth-trie-db = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f" } -reth-revm = { git = "https://github.com/paradigmxyz/reth", rev = "7f4a9a0", features = [ +reth-revm = { git = "https://github.com/paradigmxyz/reth", rev = "b25459035f", features = [ "std", "optional-checks", ] } revm = { version = "36.0.0", features = ["optional_fee_charge"], default-features = false } -alloy = { version = "1.8.2", default-features = false } -alloy-consensus = { version = "1.8.2", default-features = false } -alloy-contract = { version = "1.8.2", default-features = false } -alloy-eips = { version = "1.8.2", default-features = false } +alloy = { version = "1.6.1", default-features = false } +alloy-consensus = { version = "1.6.1", default-features = false } +alloy-contract = { version = "1.6.1", default-features = false } +alloy-eips = { version = "1.6.1", default-features = false } alloy-evm = { version = "0.29.2", default-features = false } revm-inspectors = "0.36.1" -alloy-genesis = { version = "1.8.2", default-features = false } +alloy-genesis = { version = "1.6.1", default-features = false } alloy-hardforks = "0.4.7" -alloy-network = { version = "1.8.2", default-features = false } +alloy-network = { version = "1.6.1", default-features = false } alloy-primitives = { version = "1.5.7", default-features = false } -alloy-provider = { version = "1.8.2", default-features = false } +alloy-provider = { version = "1.6.1", default-features = false } alloy-rlp = { version = "0.3.13", default-features = false } -alloy-rpc-types-engine = "1.8.2" -alloy-rpc-types-eth = { version = "1.8.2" } -alloy-serde = { version = "1.8.2", default-features = false } -alloy-signer = "1.8.2" -alloy-signer-local = "1.8.2" +alloy-rpc-types-engine = "1.6.1" +alloy-rpc-types-eth = { version = "1.6.1" } +alloy-serde = { version = "1.6.1", default-features = false } +alloy-signer = "1.6.1" +alloy-signer-local = "1.6.1" coins-bip32 = "0.12" alloy-sol-types = { version = "1.5.7", default-features = false } -alloy-transport = "1.8.2" +alloy-transport = "1.6.1" commonware-broadcast = "2026.3.0" commonware-codec = "2026.3.0" @@ -346,6 +346,24 @@ vergen-git2 = "9.1.0" # reth-trie-sparse-parallel = { path = "../reth/crates/trie/sparse-parallel" } [patch.crates-io] +revm = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-bytecode = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-context = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-context-interface = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-database = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-database-interface = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-handler = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-inspector = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-interpreter = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-precompile = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-primitives = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +revm-state = { git = "https://github.com/bluealloy/revm.git", rev = "8891655bb" } +alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "ec772db" } +revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "ecc5bc7" } + +# [patch."https://github.com/bluealloy/revm.git"] +# Not needed: reth tip1016 already pins revm to the same rev (1e1d64d540) + # Commonware right after after PR #3298 was merged # commonware-broadcast = { git = "https://github.com/commonwarexyz/monorepo", rev = "240e0207ee10c3c37a42867ce4de97b581c06b32" } # commonware-codec = { git = "https://github.com/commonwarexyz/monorepo", rev = "240e0207ee10c3c37a42867ce4de97b581c06b32" } diff --git a/bin/tempo-bench/src/cmd/max_tps.rs b/bin/tempo-bench/src/cmd/max_tps.rs index 0cf10e831f..7fc5b69168 100644 --- a/bin/tempo-bench/src/cmd/max_tps.rs +++ b/bin/tempo-bench/src/cmd/max_tps.rs @@ -9,13 +9,13 @@ use reth_tracing::{ tracing::{debug, error, info}, }; use tempo_alloy::{ - TempoNetwork, fillers::ExpiringNonceFiller, provider::ext::TempoProviderBuilderExt, + TempoNetwork, TempoWallet, fillers::ExpiringNonceFiller, provider::ext::TempoProviderBuilderExt, }; use alloy::{ consensus::BlockHeader, eips::Encodable2718, - network::{EthereumWallet, ReceiptResponse, TransactionBuilder, TxSignerSync}, + network::{ReceiptResponse, TransactionBuilder, TxSignerSync}, primitives::{Address, B256, BlockNumber, U256}, providers::{ DynProvider, PendingTransactionBuilder, PendingTransactionError, Provider, ProviderBuilder, @@ -219,7 +219,7 @@ impl MaxTpsArgs { .fetch_chain_id() .with_gas_estimation() .with_nonce_management(cached_nonce_manager) - .wallet(EthereumWallet::from(signer)) + .wallet(TempoWallet::from(signer)) .connect_http(target_url) .erased() }); diff --git a/bin/tempo/src/defaults.rs b/bin/tempo/src/defaults.rs index 417c53395c..6f04c52478 100644 --- a/bin/tempo/src/defaults.rs +++ b/bin/tempo/src/defaults.rs @@ -2,9 +2,7 @@ use base64::{Engine, prelude::BASE64_STANDARD}; use eyre::Context as _; use jiff::SignedDuration; use reth_cli_commands::download::DownloadDefaults; -use reth_ethereum::node::core::args::{ - DefaultPayloadBuilderValues, DefaultStorageValues, DefaultTxPoolValues, -}; +use reth_ethereum::node::core::args::{DefaultPayloadBuilderValues, DefaultTxPoolValues}; use std::{borrow::Cow, str::FromStr, time::Duration}; use tempo_chainspec::hardfork::TempoHardfork; use url::Url; @@ -171,16 +169,7 @@ fn init_txpool_defaults() { .expect("failed to initialize txpool defaults"); } -fn init_storage_defaults() { - DefaultStorageValues::default() - // NOTE: when changing, don't forget to change in `e2e::launch_execution_node` - .with_v2(false) - .try_init() - .expect("failed to initialize storage defaults"); -} - pub(crate) fn init_defaults() { - init_storage_defaults(); init_download_urls(); init_payload_builder_defaults(); init_txpool_defaults(); diff --git a/bin/tempo/src/tempo_cmd.rs b/bin/tempo/src/tempo_cmd.rs index e3db8528fa..8cee88c24a 100644 --- a/bin/tempo/src/tempo_cmd.rs +++ b/bin/tempo/src/tempo_cmd.rs @@ -25,7 +25,7 @@ use eyre::{OptionExt as _, Report, WrapErr as _, eyre}; use reth_cli_runner::CliRunner; use reth_ethereum_cli::ExtendedCommand; use serde::Serialize; -use tempo_alloy::TempoNetwork; +use tempo_alloy::{TempoNetwork, TempoWallet}; use tempo_chainspec::spec::{TempoChainSpec, TempoChainSpecParser}; use tempo_commonware_node_config::SigningKey; use tempo_contracts::precompiles::{ @@ -401,7 +401,7 @@ impl ValidatorTransactionArgs { let provider = ProviderBuilder::new_with_network::() .fetch_chain_id() .with_gas_estimation() - .wallet(signer) + .wallet(TempoWallet::from(signer)) .connect(&self.rpc_url) .await .wrap_err("failed to connect to RPC")?; diff --git a/crates/alloy/Cargo.toml b/crates/alloy/Cargo.toml index 14f4681c5f..76cea684dd 100644 --- a/crates/alloy/Cargo.toml +++ b/crates/alloy/Cargo.toml @@ -26,7 +26,7 @@ alloy-primitives = { workspace = true, features = ["rand"] } alloy-provider.workspace = true alloy-rpc-types-eth.workspace = true alloy-serde.workspace = true -alloy-signer-local.workspace = true +alloy-signer.workspace = true alloy-transport.workspace = true reth-evm.workspace = true @@ -44,6 +44,7 @@ tracing.workspace = true [dev-dependencies] alloy = { workspace = true, features = ["providers", "rpc-types-eth", "sol-types"] } alloy-signer.workspace = true +alloy-signer-local.workspace = true eyre.workspace = true test-case.workspace = true tokio.workspace = true diff --git a/crates/alloy/src/network.rs b/crates/alloy/src/network.rs index 1bb937b053..c2867cc92d 100644 --- a/crates/alloy/src/network.rs +++ b/crates/alloy/src/network.rs @@ -4,7 +4,7 @@ use crate::rpc::{TempoHeaderResponse, TempoTransactionReceipt, TempoTransactionR use alloy_consensus::{ReceiptWithBloom, TxType, error::UnsupportedTransactionType}; use alloy_network::{ - BuildResult, EthereumWallet, IntoWallet, Network, NetworkWallet, TransactionBuilder, + BuildResult, EthereumWallet, Network, NetworkWallet, TransactionBuilder, TransactionBuilderError, UnbuiltTransactionError, }; use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256}; @@ -12,7 +12,6 @@ use alloy_provider::fillers::{ ChainIdFiller, GasFiller, JoinFill, NonceFiller, RecommendedFillers, }; use alloy_rpc_types_eth::{AccessList, Block, Transaction}; -use alloy_signer_local::PrivateKeySigner; use tempo_primitives::{ TempoHeader, TempoReceipt, TempoTxEnvelope, TempoTxType, transaction::TempoTypedTransaction, }; @@ -292,11 +291,50 @@ impl RecommendedFillers for TempoNetwork { } } -impl IntoWallet for PrivateKeySigner { - type NetworkWallet = EthereumWallet; +/// Wallet for signing Tempo transactions. +/// +/// Wraps an [`EthereumWallet`] and implements +/// [`NetworkWallet`](NetworkWallet) by delegating to the inner +/// wallet's signers while converting between Tempo and Ethereum transaction +/// types. +#[derive(Debug, Clone)] +pub struct TempoWallet(EthereumWallet); + +impl From for TempoWallet +where + EthereumWallet: From, +{ + fn from(signer: S) -> Self { + Self(EthereumWallet::from(signer)) + } +} + +impl NetworkWallet for TempoWallet { + fn default_signer_address(&self) -> Address { + NetworkWallet::::default_signer_address(&self.0) + } + + fn has_signer_for(&self, address: &Address) -> bool { + NetworkWallet::::has_signer_for(&self.0, address) + } + + fn signer_addresses(&self) -> impl Iterator { + NetworkWallet::::signer_addresses(&self.0) + } - fn into_wallet(self) -> Self::NetworkWallet { - self.into() + async fn sign_transaction_from( + &self, + sender: Address, + tx: TempoTypedTransaction, + ) -> alloy_signer::Result { + let mut signable = tx; + let signer = self.0.signer_by_address(sender).ok_or_else(|| { + alloy_signer::Error::other(format!("Missing signing credential for {sender}")) + })?; + let sig = signer + .sign_transaction(signable.as_dyn_signable_mut()) + .await?; + Ok(signable.into_envelope(sig)) } } diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index c2a0f06e59..17ae629db2 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -3,7 +3,7 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] -use alloy_consensus::{BlockHeader, Transaction, transaction::TxHashRef}; +use alloy_consensus::{BlockHeader, Transaction, TxReceipt, transaction::TxHashRef}; use alloy_evm::block::BlockExecutionResult; use reth_chainspec::EthChainSpec; use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom}; @@ -12,7 +12,7 @@ use reth_consensus_common::validation::{ validate_against_parent_gas_limit, validate_against_parent_hash_number, }; use reth_ethereum_consensus::EthBeaconConsensus; -use reth_primitives_traits::{RecoveredBlock, SealedBlock, SealedHeader}; +use reth_primitives_traits::{GotExpected, RecoveredBlock, SealedBlock, SealedHeader}; use std::sync::Arc; use tempo_chainspec::{ hardfork::TempoHardforks, @@ -203,10 +203,39 @@ impl FullConsensus for TempoConsensus { result: &BlockExecutionResult, receipt_root_bloom: Option, ) -> Result<(), ConsensusError> { + // TIP-1016: block header gas_used tracks execution gas only, while receipt + // cumulative_gas_used tracks total gas (execution + storage creation). The + // standard Ethereum check requires strict equality, but TIP-1016 allows + // header gas_used <= last receipt cumulative_gas_used. + let cumulative_gas_used = result + .receipts + .last() + .map(|r| r.cumulative_gas_used()) + .unwrap_or(0); + if block.header().gas_used() > cumulative_gas_used { + return Err(ConsensusError::BlockGasUsed { + gas: GotExpected { + got: cumulative_gas_used, + expected: block.header().gas_used(), + }, + gas_spent_by_tx: reth_primitives_traits::receipt::gas_spent_by_transactions( + &result.receipts, + ), + }); + } + + // Delegate receipt root, logs bloom, and requests hash validation to the + // inner Ethereum consensus. We construct a temporary result with gas_used + // matching the header so the inner gas check passes, while the actual + // TIP-1016 gas invariant (header <= receipts) is checked above. + let mut patched_result = result.clone(); + if let Some(last) = patched_result.receipts.last_mut() { + last.cumulative_gas_used = block.header().gas_used(); + } FullConsensus::::validate_block_post_execution( &self.inner, block, - result, + &patched_result, receipt_root_bloom, ) } diff --git a/crates/e2e/Cargo.toml b/crates/e2e/Cargo.toml index e0364bb5a4..6f26eea3d9 100644 --- a/crates/e2e/Cargo.toml +++ b/crates/e2e/Cargo.toml @@ -59,6 +59,7 @@ serde_json.workspace = true [dev-dependencies] commonware-consensus.workspace = true commonware-macros.workspace = true +reth-payload-primitives.workspace = true commonware-parallel.workspace = true jsonrpsee = { workspace = true, features = ["ws-client", "http-client"] } tempo-eyre.workspace = true diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 3022cd5569..dd5a7e97fb 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -30,13 +30,13 @@ reth-revm.workspace = true reth-primitives-traits.workspace = true reth-rpc-eth-api = { workspace = true, optional = true } -rayon = { workspace = true, optional = true } alloy-rlp.workspace = true alloy-evm.workspace = true alloy-consensus.workspace = true alloy-primitives.workspace = true derive_more.workspace = true +rayon = { workspace = true, optional = true } thiserror.workspace = true tracing.workspace = true diff --git a/crates/evm/src/block.rs b/crates/evm/src/block.rs index 57056f5e64..0ff3f5b90b 100644 --- a/crates/evm/src/block.rs +++ b/crates/evm/src/block.rs @@ -4,7 +4,7 @@ use alloy_evm::{ Database, Evm, RecoveredTx, block::{ BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockValidationError, - ExecutableTx, OnStateHook, TxResult, + ExecutableTx, GasOutput, OnStateHook, TxResult, }, eth::{ EthBlockExecutor, EthTxResult, @@ -102,6 +102,10 @@ impl TxResult for TempoTxResult { fn result(&self) -> &ResultAndState { self.inner.result() } + + fn into_result(self) -> ResultAndState { + self.inner.into_result() + } } /// Block executor for Tempo. @@ -421,7 +425,7 @@ where let inner = result?; - let next_section = self.validate_tx(recovered.tx(), inner.result.result.gas_used())?; + let next_section = self.validate_tx(recovered.tx(), inner.result.result.tx_gas_used())?; Ok(TempoTxResult { inner, next_section, @@ -431,7 +435,10 @@ where }) } - fn commit_transaction(&mut self, output: Self::Result) -> Result { + fn commit_transaction( + &mut self, + output: Self::Result, + ) -> Result { let TempoTxResult { inner, next_section, @@ -439,7 +446,12 @@ where tx, } = output; - let gas_used = self.inner.commit_transaction(inner)?; + // Snapshot block_regular_gas_used before commit to derive per-tx execution gas. + // block_regular_gas_used (from ResultGas::block_regular_gas_used()) correctly + // excludes state gas before applying floor, unlike tx_gas_used() - state_gas_used(). + let regular_gas_before = self.inner.block_regular_gas_used; + let gas_output = self.inner.commit_transaction(inner)?; + let gas_used = self.inner.block_regular_gas_used - regular_gas_before; // TODO: remove once revm supports emitting logs for reverted transactions // @@ -490,13 +502,21 @@ where } } - Ok(gas_used) + Ok(gas_output) } fn finish( self, ) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { - self.inner.finish() + let tx_gas_used = self.inner.cumulative_tx_gas_used; + let (evm, mut result) = self.inner.finish()?; + // The upstream `EthBlockExecutor::finish()` sets `gas_used` to + // `max_block_gas_used()` (the pre-refund `block_regular_gas_used`), + // but receipts accumulate the post-refund `tx_gas_used`. Override so + // `header.gas_used` matches `cumulative_gas_used` in the last receipt, + // which is required by post-execution validation. + result.gas_used = tx_gas_used; + Ok((evm, result)) } fn set_state_hook(&mut self, hook: Option>) { @@ -542,6 +562,11 @@ where pub(crate) fn section(&self) -> BlockSection { self.section } + + /// Get the non-shared gas left for assertions. + pub(crate) fn non_shared_gas_left(&self) -> u64 { + self.non_shared_gas_left + } } #[cfg(test)] @@ -592,7 +617,7 @@ mod tests { )]; let result: ExecutionResult = ExecutionResult::Success { reason: revm::context::result::SuccessReason::Return, - gas: ResultGas::default().with_limit(21000).with_spent(21000), + gas: ResultGas::new_with_state_gas(21000, 0, 0, 0), logs, output: revm::context::result::Output::Call(Bytes::new()), }; @@ -1101,7 +1126,7 @@ mod tests { result: ResultAndState { result: revm::context::result::ExecutionResult::Success { reason: revm::context::result::SuccessReason::Return, - gas: ResultGas::default().with_limit(21000).with_spent(21000), + gas: ResultGas::new_with_state_gas(21000, 0, 0, 0), logs: vec![], output: revm::context::result::Output::Call(Bytes::new()), }, @@ -1115,9 +1140,9 @@ mod tests { tx: None, }; - let gas_used = executor.commit_transaction(output).unwrap(); + let gas_output = executor.commit_transaction(output).unwrap(); - assert_eq!(gas_used, 21000); + assert_eq!(gas_output.tx_gas_used(), 21000); assert_eq!(executor.section(), BlockSection::NonShared); } @@ -1131,6 +1156,161 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_commit_transaction_tracks_total_cumulative_gas() { + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_general_gas_limit(30_000_000) + .with_parent_beacon_block_root(B256::ZERO) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + let tx = create_legacy_tx(); + let output = TempoTxResult { + inner: EthTxResult { + result: ResultAndState { + result: revm::context::result::ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Return, + gas: ResultGas::new_with_state_gas(21000, 0, 0, 0), + logs: vec![], + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: Default::default(), + }, + blob_gas_used: 0, + tx_type: tx.tx_type(), + }, + next_section: BlockSection::NonShared, + is_payment: false, + tx: None, + }; + + let gas_output = executor.commit_transaction(output).unwrap(); + + // With zero storage creation gas, execution gas equals total gas + assert_eq!(gas_output.tx_gas_used(), 21000); + } + + #[test] + fn test_cumulative_gas_accumulates_across_transactions() { + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_general_gas_limit(30_000_000) + .with_parent_beacon_block_root(B256::ZERO) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + // Commit first transaction (21000 gas) + let tx1 = create_legacy_tx(); + let output1 = TempoTxResult { + inner: EthTxResult { + result: ResultAndState { + result: revm::context::result::ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Return, + gas: ResultGas::new_with_state_gas(21000, 0, 0, 0), + logs: vec![], + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: Default::default(), + }, + blob_gas_used: 0, + tx_type: tx1.tx_type(), + }, + next_section: BlockSection::NonShared, + is_payment: false, + tx: None, + }; + executor.commit_transaction(output1).unwrap(); + + // Commit second transaction (50000 gas) + let tx2 = create_legacy_tx(); + let output2 = TempoTxResult { + inner: EthTxResult { + result: ResultAndState { + result: revm::context::result::ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Return, + gas: ResultGas::new_with_state_gas(50000, 0, 0, 0), + logs: vec![], + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: Default::default(), + }, + blob_gas_used: 0, + tx_type: tx2.tx_type(), + }, + next_section: BlockSection::NonShared, + is_payment: false, + tx: None, + }; + executor.commit_transaction(output2).unwrap(); + + // Receipts should have cumulative total gas (tracked by inner executor) + let receipts = executor.receipts(); + assert_eq!(receipts[0].cumulative_gas_used, 21000); + assert_eq!(receipts[1].cumulative_gas_used, 71000); + } + + #[test] + fn test_finish_returns_execution_gas_for_block_header() { + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_general_gas_limit(30_000_000) + .with_parent_beacon_block_root(B256::ZERO) + .with_section(BlockSection::NonShared) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + // Manually set state to simulate a committed transaction + executor.inner.block_regular_gas_used += 21000; + + let (_, result) = executor.finish().unwrap(); + // Block header gas_used should be execution gas + assert_eq!(result.gas_used, 21000); + } + + #[test] + fn test_non_shared_gas_uses_execution_gas_only() { + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_general_gas_limit(30_000_000) + .with_parent_beacon_block_root(B256::ZERO) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + let initial_non_shared = executor.non_shared_gas_left(); + + let tx = create_legacy_tx(); + let output = TempoTxResult { + inner: EthTxResult { + result: ResultAndState { + result: revm::context::result::ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Return, + gas: ResultGas::new_with_state_gas(50_000, 0, 0, 0), + logs: vec![], + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: Default::default(), + }, + blob_gas_used: 0, + tx_type: tx.tx_type(), + }, + next_section: BlockSection::NonShared, + is_payment: false, + tx: None, + }; + executor.commit_transaction(output).unwrap(); + + assert_eq!(executor.non_shared_gas_left(), initial_non_shared - 50_000); + } + #[test] fn test_apply_pre_execution_deploys_validator_v2_code() { use std::sync::Arc; @@ -1149,4 +1329,47 @@ mod tests { let info = acc.account_info().unwrap(); assert!(!info.is_empty_code_hash()); } + + /// Regression test: when a transaction earns a gas refund (e.g. clearing + /// storage), `finish()` must return a `gas_used` that matches the last + /// receipt's `cumulative_gas_used` (the post-refund / tx_gas_used value). + /// + /// Without the fix, `finish()` returns `max_block_gas_used()` which is the + /// pre-refund `block_regular_gas_used`, causing post-execution validation + /// to reject the block with `BlockGasUsed` mismatch. + #[test] + fn test_finish_gas_used_matches_receipt_cumulative_gas_with_refund() { + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_parent_beacon_block_root(B256::ZERO) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + // Simulate a transaction that spent 50_000 gas but received a 10_000 + // refund, so tx_gas_used = 40_000 while block_regular_gas_used = 50_000. + let tx_gas = 40_000u64; + let regular_gas = 50_000u64; + executor.inner.cumulative_tx_gas_used = tx_gas; + executor.inner.block_regular_gas_used = regular_gas; + + // Also add a matching receipt so the output is self-consistent. + executor.inner.receipts.push(TempoReceipt { + tx_type: TempoTxType::Legacy, + success: true, + cumulative_gas_used: tx_gas, + logs: vec![], + }); + + let (_evm, result) = executor.finish().expect("finish should succeed"); + + let last_cumulative = result.receipts.last().unwrap().cumulative_gas_used; + assert_eq!( + result.gas_used, last_cumulative, + "header gas_used ({}) must equal last receipt cumulative_gas_used ({}) \ + to pass post-execution validation", + result.gas_used, last_cumulative + ); + } } diff --git a/crates/evm/src/evm.rs b/crates/evm/src/evm.rs index c7bded6b15..824cb2c5e4 100644 --- a/crates/evm/src/evm.rs +++ b/crates/evm/src/evm.rs @@ -182,7 +182,7 @@ where ); }; - *gas = ResultGas::default().with_limit(tx.inner.gas_limit); + *gas = ResultGas::default(); Ok(result) } else if self.inspect { @@ -286,7 +286,7 @@ mod tests { let result = result.unwrap(); assert!(result.result.is_success()); - assert_eq!(result.result.gas_used(), 21000); + assert_eq!(result.result.tx_gas_used(), 21000); } #[test] @@ -312,7 +312,7 @@ mod tests { let result = result.unwrap(); assert!(result.result.is_success()); // System transactions should not consume gas - assert_eq!(result.result.gas_used(), 0); + assert_eq!(result.result.tx_gas_used(), 0); } #[test] diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 91cb3eb50c..bd9fbf9a46 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -135,6 +135,8 @@ impl ConfigureEvm for TempoEvmConfig { // Apply TIP-1000 gas params for T1 hardfork. let mut cfg_env = cfg_env.with_spec_and_gas_params(spec, tempo_gas_params(spec)); cfg_env.tx_gas_limit_cap = spec.tx_gas_limit_cap(); + // TIP-1016: Enable state gas tracking for T3+ + cfg_env.enable_amsterdam_eip8037 = spec.is_t3(); Ok(EvmEnv { cfg_env, @@ -172,6 +174,8 @@ impl ConfigureEvm for TempoEvmConfig { // Apply TIP-1000 gas params for T1 hardfork. let mut cfg_env = cfg_env.with_spec_and_gas_params(spec, tempo_gas_params(spec)); cfg_env.tx_gas_limit_cap = spec.tx_gas_limit_cap(); + // TIP-1016: Enable state gas tracking for T3+ + cfg_env.enable_amsterdam_eip8037 = spec.is_t3(); Ok(EvmEnv { cfg_env, @@ -263,7 +267,7 @@ mod tests { use alloy_rlp::{Encodable, bytes::BytesMut}; use reth_evm::{ConfigureEvm, NextBlockEnvAttributes}; use std::collections::HashMap; - use tempo_chainspec::hardfork::TempoHardfork; + use tempo_chainspec::{hardfork::TempoHardfork, spec::DEV}; use tempo_primitives::{ BlockBody, SubBlockMetadata, subblock::SubBlockVersion, transaction::envelope::TEMPO_SYSTEM_TX_SIGNATURE, @@ -319,8 +323,6 @@ mod tests { /// [TIP-1000]: #[test] fn test_evm_env_t1_gas_cap() { - use tempo_chainspec::spec::DEV; - // DEV chainspec has T1 activated at timestamp 0 let chainspec = DEV.clone(); let evm_config = TempoEvmConfig::new(chainspec.clone()); @@ -351,6 +353,104 @@ mod tests { ); } + /// TIP-1016: enable_amsterdam_eip8037 must be set when T3 hardfork is active. + /// This gates the reservoir model, tx cap bypass, and state gas tracking. + #[test] + fn test_evm_env_t3_enable_amsterdam_eip8037() { + let chainspec = DEV.clone(); + let evm_config = TempoEvmConfig::new(chainspec.clone()); + + let header = TempoHeader { + inner: alloy_consensus::Header { + number: 100, + timestamp: 1000, // After T3 activation in DEV + gas_limit: 30_000_000, + base_fee_per_gas: Some(1000), + ..Default::default() + }, + general_gas_limit: 10_000_000, + timestamp_millis_part: 0, + shared_gas_limit: 3_000_000, + }; + + // Verify we're in T3 + assert!(chainspec.tempo_hardfork_at(header.timestamp()).is_t3()); + + let evm_env = evm_config.evm_env(&header).unwrap(); + + assert!( + evm_env.cfg_env.enable_amsterdam_eip8037, + "TIP-1016: enable_amsterdam_eip8037 must be true when T3 hardfork is active" + ); + } + + /// TIP-1016: enable_amsterdam_eip8037 must NOT be set for pre-T3 hardforks. + #[test] + fn test_evm_env_pre_t3_no_state_gas() { + let chainspec = DEV.clone(); + let _evm_config = TempoEvmConfig::new(chainspec); + + // Verify that a T2-only spec does NOT enable state gas + let t2_spec = TempoHardfork::T2; + let gas_params = tempo_gas_params(t2_spec); + let cfg = revm::context::CfgEnv::new_with_spec_and_gas_params(t2_spec, gas_params); + + assert!( + !cfg.enable_amsterdam_eip8037, + "enable_amsterdam_eip8037 must be false for pre-T3 hardforks" + ); + } + + /// TIP-1016: next_evm_env must also set enable_amsterdam_eip8037 for T3. + #[test] + fn test_next_evm_env_t3_enable_amsterdam_eip8037() { + let chainspec = DEV.clone(); + let evm_config = TempoEvmConfig::new(chainspec.clone()); + + let parent = TempoHeader { + inner: alloy_consensus::Header { + number: 99, + timestamp: 900, + gas_limit: 30_000_000, + base_fee_per_gas: Some(1000), + ..Default::default() + }, + general_gas_limit: 10_000_000, + timestamp_millis_part: 0, + shared_gas_limit: 3_000_000, + }; + + let attributes = TempoNextBlockEnvAttributes { + inner: NextBlockEnvAttributes { + timestamp: 1000, // After T3 activation + suggested_fee_recipient: Address::repeat_byte(0x02), + prev_randao: B256::repeat_byte(0x03), + gas_limit: 30_000_000, + parent_beacon_block_root: Some(B256::ZERO), + withdrawals: None, + extra_data: Default::default(), + }, + general_gas_limit: 10_000_000, + shared_gas_limit: 3_000_000, + timestamp_millis_part: 0, + subblock_fee_recipients: HashMap::new(), + }; + + // Verify we're in T3 + assert!( + chainspec + .tempo_hardfork_at(attributes.inner.timestamp) + .is_t3() + ); + + let evm_env = evm_config.next_evm_env(&parent, &attributes).unwrap(); + + assert!( + evm_env.cfg_env.enable_amsterdam_eip8037, + "TIP-1016: next_evm_env must set enable_amsterdam_eip8037 for T3" + ); + } + #[test] fn test_next_evm_env() { let evm_config = TempoEvmConfig::new(test_chainspec()); diff --git a/crates/faucet/src/args.rs b/crates/faucet/src/args.rs index f1609b9fc4..301fcaeaa3 100644 --- a/crates/faucet/src/args.rs +++ b/crates/faucet/src/args.rs @@ -1,11 +1,10 @@ use alloy::{ - network::EthereumWallet, primitives::{Address, B256, U256}, providers::{DynProvider, Provider, ProviderBuilder}, signers::local::PrivateKeySigner, }; use clap::Args; -use tempo_alloy::{TempoNetwork, provider::ext::TempoProviderBuilderExt}; +use tempo_alloy::{TempoNetwork, TempoWallet, provider::ext::TempoProviderBuilderExt}; /// Faucet-specific CLI arguments #[derive(Debug, Clone, Default, Args, PartialEq, Eq)] @@ -49,12 +48,12 @@ pub struct FaucetArgs { } impl FaucetArgs { - pub fn wallet(&self) -> EthereumWallet { + pub fn wallet(&self) -> TempoWallet { let signer: PrivateKeySigner = PrivateKeySigner::from_bytes( &self.private_key.expect("No faucet private key provided"), ) .expect("Failed to decode private key"); - EthereumWallet::new(signer) + TempoWallet::from(signer) } pub fn addresses(&self) -> Vec
{ diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 1b2025c799..0827089f57 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -68,6 +68,7 @@ reth-ethereum = { workspace = true, features = ["node", "test-utils", "pool"] } reth-e2e-test-utils.workspace = true reth-node-core.workspace = true reth-node-metrics.workspace = true +reth-payload-primitives.workspace = true serde_json.workspace = true tempo-contracts.workspace = true tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } diff --git a/crates/node/tests/it/eth_call.rs b/crates/node/tests/it/eth_call.rs index f457699252..ec5ddace6f 100644 --- a/crates/node/tests/it/eth_call.rs +++ b/crates/node/tests/it/eth_call.rs @@ -37,7 +37,7 @@ fn extract_revert_data( /// Expected revert bytes for `Panic(UnderOverflow)`. fn under_overflow_revert() -> Bytes { TempoPrecompileError::under_overflow() - .into_precompile_result(0) + .into_precompile_result(0, 0) .unwrap() .bytes } diff --git a/crates/node/tests/it/main.rs b/crates/node/tests/it/main.rs index e82a466bfc..acc65e2487 100644 --- a/crates/node/tests/it/main.rs +++ b/crates/node/tests/it/main.rs @@ -11,6 +11,7 @@ mod payment_lane; mod pool; mod stablecoin_dex; mod tempo_transaction; +mod tip1016_storage_gas; mod tip20; mod tip20_factory; mod tip20_gas_fees; diff --git a/crates/node/tests/it/tempo_transaction/local.rs b/crates/node/tests/it/tempo_transaction/local.rs index eaa33ed781..7609df1884 100644 --- a/crates/node/tests/it/tempo_transaction/local.rs +++ b/crates/node/tests/it/tempo_transaction/local.rs @@ -21,7 +21,7 @@ use reth_ethereum::network::{NetworkSyncUpdater, SyncState}; use reth_node_api::BuiltPayload; use reth_primitives_traits::transaction::TxHashRef; use reth_transaction_pool::TransactionPool; -use tempo_alloy::TempoNetwork; +use tempo_alloy::{TempoNetwork, TempoWallet}; use tempo_chainspec::{hardfork::TempoHardfork, spec::TEMPO_T1_BASE_FEE}; use tempo_contracts::precompiles::{ DEFAULT_FEE_TOKEN, account_keychain::IAccountKeychain::revokeKeyCall, @@ -1349,7 +1349,7 @@ async fn test_aa_keychain_revocation_toctou_dos() -> eyre::Result<()> { let root_addr = root_signer.address(); let provider = ProviderBuilder::new_with_network::() - .wallet(root_signer.clone()) + .wallet(TempoWallet::from(root_signer.clone())) .connect_http(setup.node.rpc_url()); let chain_id = provider.get_chain_id().await?; @@ -1644,7 +1644,7 @@ async fn test_aa_keychain_spending_limit_toctou_dos() -> eyre::Result<()> { let root_addr = root_signer.address(); let provider = ProviderBuilder::new_with_network::() - .wallet(root_signer.clone()) + .wallet(TempoWallet::from(root_signer.clone())) .connect_http(setup.node.rpc_url()); let chain_id = provider.get_chain_id().await?; @@ -2190,7 +2190,7 @@ async fn test_aa_keychain_v2_signature() -> eyre::Result<()> { let root_signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC).build()?; let root_addr = root_signer.address(); let provider = ProviderBuilder::new_with_network::() - .wallet(root_signer.clone()) + .wallet(TempoWallet::from(root_signer.clone())) .connect_http(setup.node.rpc_url()); let chain_id = provider.get_chain_id().await?; diff --git a/crates/node/tests/it/tip1016_storage_gas.rs b/crates/node/tests/it/tip1016_storage_gas.rs new file mode 100644 index 0000000000..d6c5ca5fcd --- /dev/null +++ b/crates/node/tests/it/tip1016_storage_gas.rs @@ -0,0 +1,795 @@ +//! Tests for TIP-1016: Exempt Storage Creation from Gas Limits. +//! +//! TIP-1016 splits storage creation costs into two components: +//! - **Execution gas**: computational cost (writing, hashing) -- counts toward protocol limits +//! - **Storage creation gas**: permanent storage burden -- does NOT count toward protocol limits +//! +//! Key invariants tested: +//! 1. Block header gas_used reflects only execution gas (storage creation gas excluded) +//! 2. Receipt gas_used includes ALL gas (execution + storage creation) +//! 3. Therefore: sum of receipt gas_used > block header gas_used when storage is created +//! 4. Transactions that only touch existing storage have no difference +//! 5. Reverted txs still have state gas exempted from protocol limits (CPU time is bounded +//! regardless of whether state was committed) +//! 6. Multiple storage-creating operations in a single tx are additive +//! 7. Multiple storage-creating txs in a single block correctly accumulate exemptions +//! 8. Reverted inner CALLs do NOT contribute state gas to the parent frame's exemption + +use reth_node_api::BuiltPayload; + +use alloy::{ + consensus::{SignableTransaction, Transaction, TxEip1559, TxEnvelope}, + primitives::{Address, Bytes, U256}, + providers::{Provider, ProviderBuilder}, + signers::local::MnemonicBuilder, +}; +use alloy_eips::{BlockId, BlockNumberOrTag, eip2718::Encodable2718}; +use alloy_network::TxSignerSync; +use tempo_chainspec::spec::TEMPO_T1_BASE_FEE; +use tempo_contracts::{CREATEX_ADDRESS, CreateX}; + +use crate::utils::{TEST_MNEMONIC, TestNodeBuilder}; + +/// Builds and encodes a signed EIP-1559 CALL transaction. +fn build_call_tx( + signer: &alloy::signers::local::PrivateKeySigner, + chain_id: u64, + nonce: u64, + gas_limit: u64, + to: Address, + input: Bytes, +) -> Bytes { + let mut tx = TxEip1559 { + chain_id, + nonce, + gas_limit, + to: to.into(), + max_fee_per_gas: TEMPO_T1_BASE_FEE as u128, + max_priority_fee_per_gas: TEMPO_T1_BASE_FEE as u128, + input, + ..Default::default() + }; + let signature = signer.sign_transaction_sync(&mut tx).unwrap(); + TxEnvelope::Eip1559(tx.into_signed(signature)) + .encoded_2718() + .into() +} + +/// Gets the deployed contract address from CreateX's ContractCreation event, polling until +/// receipts are available. +async fn get_createx_deployed_address( + provider: &P, + block_number: u64, +) -> eyre::Result
{ + let block_id = BlockId::Number(BlockNumberOrTag::Number(block_number)); + for _ in 0..50 { + if let Some(receipts) = provider.get_block_receipts(block_id).await? { + let receipt = receipts + .iter() + .find(|r| !r.inner.logs().is_empty()) + .expect("should have a receipt with logs"); + assert!(receipt.status(), "deployment should succeed"); + let addr = Address::from_slice( + &receipt.inner.logs()[0].inner.data.topics()[1].as_slice()[12..], + ); + return Ok(addr); + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + eyre::bail!("timed out waiting for deploy receipts at block {block_number}") +} + +/// Returns the total gas_used from all receipts in a block, polling until the RPC catches up. +async fn total_receipt_gas_for_block( + provider: &P, + block_number: u64, +) -> eyre::Result { + let block_id = BlockId::Number(BlockNumberOrTag::Number(block_number)); + for _ in 0..50 { + if let Some(receipts) = provider.get_block_receipts(block_id).await? { + return Ok(receipts.iter().map(|r| r.gas_used).sum()); + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + eyre::bail!("timed out waiting for receipts at block {block_number}") +} + +/// Happy path: deploying a contract via CreateX creates new storage (account creation + +/// code storage), so block header gas_used should be less than the sum of receipt gas_used. +/// +/// The difference is the storage creation gas that TIP-1016 exempts from protocol limits. +#[tokio::test(flavor = "multi_thread")] +async fn test_tip1016_contract_deployment_exempts_storage_gas() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC) + .index(0)? + .build()?; + let provider = ProviderBuilder::new().connect_http(setup.node.rpc_url()); + let chain_id = provider.get_chain_id().await?; + + // Simple contract init code: PUSH1 0x2a PUSH1 0x00 MSTORE PUSH1 0x20 PUSH1 0x00 RETURN + // Deploys a contract that returns 42, creating new account + code storage. + let init_code = + Bytes::from_static(&[0x60, 0x2a, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3]); + + let createx = CreateX::new(CREATEX_ADDRESS, &provider); + let deploy_calldata: Bytes = createx.deployCreate(init_code).calldata().clone(); + + let raw_tx = build_call_tx( + &signer, + chain_id, + 0, + 5_000_000, + CREATEX_ADDRESS, + deploy_calldata, + ); + setup.node.rpc.inject_tx(raw_tx.clone()).await?; + + let payload = setup.node.advance_block().await?; + let block = payload.block(); + let block_number = block.header().inner.number; + let block_gas_used = block.header().inner.gas_used; + + // Verify user tx was included (non-system txs have gas_limit > 0) + let user_tx_count = block + .body() + .transactions() + .filter(|tx| (*tx).gas_limit() > 0) + .count(); + assert!(user_tx_count > 0, "deploy tx should be included in block"); + + let receipts_total_gas = total_receipt_gas_for_block(&provider, block_number).await?; + + // TIP-1016: block header gas_used should be LESS than the sum of all receipt gas_used + // because storage creation gas is exempted from the block header but charged to users. + // + // The deployed contract is 32 bytes of runtime code. Storage creation gas exempted: + // code_deposit_state_gas = 32 bytes x 2,300 gas/byte = 73,600 + let storage_creation_gas = receipts_total_gas - block_gas_used; + assert_eq!( + storage_creation_gas, 73_600, + "storage creation gas should be exactly 73,600 (32 bytes x 2,300 code_deposit_state_gas), \ + got {storage_creation_gas} (block_gas_used={block_gas_used}, receipts_total_gas={receipts_total_gas})" + ); + + Ok(()) +} + +/// Happy path: a SSTORE (zero -> non-zero) via a CALL to an existing contract +/// triggers the storage creation gas exemption. +/// +/// SSTORE zero->non-zero costs 250,000 gas total (5,000 exec + 245,000 storage). +#[tokio::test(flavor = "multi_thread")] +async fn test_tip1016_sstore_zero_to_nonzero_exempts_storage_gas() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC) + .index(0)? + .build()?; + let provider = ProviderBuilder::new().connect_http(setup.node.rpc_url()); + let chain_id = provider.get_chain_id().await?; + + // Step 1: Deploy a contract whose runtime code does SSTORE(calldataload(0), 1) + // + // Runtime bytecode (7 bytes): + // PUSH1 0x01 PUSH1 0x00 CALLDATALOAD SSTORE STOP + // + // Init code wraps runtime via CODECOPY + RETURN + let init_code = Bytes::from_static(&[ + // Init code (12 bytes) + 0x60, 0x07, // PUSH1 7 (runtime length) + 0x60, 0x0c, // PUSH1 12 (runtime offset) + 0x60, 0x00, // PUSH1 0 (memory dest) + 0x39, // CODECOPY + 0x60, 0x07, // PUSH1 7 (return length) + 0x60, 0x00, // PUSH1 0 (return offset) + 0xf3, // RETURN + // Runtime code (7 bytes) + 0x60, 0x01, // PUSH1 1 (value) + 0x60, 0x00, // PUSH1 0 (calldata offset) + 0x35, // CALLDATALOAD (slot) + 0x55, // SSTORE + 0x00, // STOP + ]); + + let createx = CreateX::new(CREATEX_ADDRESS, &provider); + let deploy_calldata: Bytes = createx.deployCreate(init_code).calldata().clone(); + + let deploy_raw = build_call_tx( + &signer, + chain_id, + 0, + 5_000_000, + CREATEX_ADDRESS, + deploy_calldata, + ); + setup.node.rpc.inject_tx(deploy_raw).await?; + let deploy_payload = setup.node.advance_block().await?; + + // Get deployed contract address from the CreateX ContractCreation event + let deploy_block_number = deploy_payload.block().header().inner.number; + let contract_addr = get_createx_deployed_address(&provider, deploy_block_number).await?; + + // Step 2: Call the deployed contract to trigger SSTORE zero->non-zero at slot 42 + let calldata: Bytes = alloy_primitives::B256::left_padding_from(&42u64.to_be_bytes()) + .as_slice() + .to_vec() + .into(); + let call_raw = build_call_tx(&signer, chain_id, 1, 5_000_000, contract_addr, calldata); + setup.node.rpc.inject_tx(call_raw).await?; + let call_payload = setup.node.advance_block().await?; + + let call_block_number = call_payload.block().header().inner.number; + let block_gas_used = call_payload.block().header().inner.gas_used; + let receipts_total_gas = total_receipt_gas_for_block(&provider, call_block_number).await?; + + // TIP-1016: block gas_used should be less than receipt gas because + // the SSTORE zero->non-zero has 245,000 storage creation gas exempted. + // + // sstore_set_state_gas = 250,000 - 5,000 = 245,000 + let storage_creation_gas = receipts_total_gas - block_gas_used; + assert_eq!( + storage_creation_gas, 245_000, + "storage creation gas should be exactly 245,000 (sstore_set_state_gas), \ + got {storage_creation_gas} (block_gas_used={block_gas_used}, receipts_total_gas={receipts_total_gas})" + ); + + Ok(()) +} + +/// Happy path: a SSTORE that modifies an existing slot (non-zero -> non-zero) should +/// NOT have any storage creation gas component, so block gas_used and total receipt gas +/// should be equal. +#[tokio::test(flavor = "multi_thread")] +async fn test_tip1016_sstore_nonzero_to_nonzero_no_exemption() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC) + .index(0)? + .build()?; + let provider = ProviderBuilder::new().connect_http(setup.node.rpc_url()); + let chain_id = provider.get_chain_id().await?; + + // Deploy a contract that does: SSTORE(slot=calldataload(0), value=calldataload(32)) + // + // Runtime (8 bytes): + // PUSH1 0x20 CALLDATALOAD PUSH1 0x00 CALLDATALOAD SSTORE STOP + let init_code = Bytes::from_static(&[ + // Init code (12 bytes) + 0x60, 0x08, 0x60, 0x0c, 0x60, 0x00, 0x39, 0x60, 0x08, 0x60, 0x00, 0xf3, + // Runtime code (8 bytes) + 0x60, 0x20, 0x35, 0x60, 0x00, 0x35, 0x55, 0x00, + ]); + + let createx = CreateX::new(CREATEX_ADDRESS, &provider); + let deploy_calldata: Bytes = createx.deployCreate(init_code).calldata().clone(); + + let deploy_raw = build_call_tx( + &signer, + chain_id, + 0, + 5_000_000, + CREATEX_ADDRESS, + deploy_calldata, + ); + setup.node.rpc.inject_tx(deploy_raw).await?; + let deploy_payload = setup.node.advance_block().await?; + + let deploy_blk = deploy_payload.block().header().inner.number; + let contract_addr = get_createx_deployed_address(&provider, deploy_blk).await?; + + // First call: SSTORE zero->non-zero at slot 0 + let mut calldata1 = [0u8; 64]; + calldata1[63] = 1; // value = 1 + let call1_raw = build_call_tx( + &signer, + chain_id, + 1, + 5_000_000, + contract_addr, + calldata1.to_vec().into(), + ); + setup.node.rpc.inject_tx(call1_raw).await?; + setup.node.advance_block().await?; + + // Second call: SSTORE non-zero->non-zero at slot 0 (value 1->2) + let mut calldata2 = [0u8; 64]; + calldata2[63] = 2; // value = 2 + let call2_raw = build_call_tx( + &signer, + chain_id, + 2, + 5_000_000, + contract_addr, + calldata2.to_vec().into(), + ); + setup.node.rpc.inject_tx(call2_raw).await?; + let call2_payload = setup.node.advance_block().await?; + + let blk_number = call2_payload.block().header().inner.number; + let block_gas_used = call2_payload.block().header().inner.gas_used; + let receipts_total_gas = total_receipt_gas_for_block(&provider, blk_number).await?; + + // For non-zero->non-zero SSTORE, there's no storage creation gas. + // Block gas_used and total receipt gas should be equal. + assert_eq!( + block_gas_used, receipts_total_gas, + "block gas_used ({block_gas_used}) should equal total receipt gas \ + ({receipts_total_gas}) for non-zero->non-zero SSTORE (no storage creation)" + ); + + Ok(()) +} + +/// Happy path: a TIP-20 transfer to an existing account (no new storage slots created) +/// should have identical block gas_used and total receipt gas. +#[tokio::test(flavor = "multi_thread")] +async fn test_tip1016_tip20_transfer_existing_no_storage_creation() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC) + .index(0)? + .build()?; + let provider = ProviderBuilder::new().connect_http(setup.node.rpc_url()); + let chain_id = provider.get_chain_id().await?; + + let sender = signer.address(); + let token = + tempo_precompiles::tip20::ITIP20::new(tempo_precompiles::PATH_USD_ADDRESS, &provider); + + // Mint tokens to sender + let mint_calldata: Bytes = token.mint(sender, U256::from(1_000_000)).calldata().clone(); + let mint_raw = build_call_tx( + &signer, + chain_id, + 0, + 5_000_000, + tempo_precompiles::PATH_USD_ADDRESS, + mint_calldata, + ); + setup.node.rpc.inject_tx(mint_raw).await?; + setup.node.advance_block().await?; + + // Transfer to self (existing account, existing balance slot) -- no storage creation + let transfer_calldata: Bytes = token.transfer(sender, U256::from(100)).calldata().clone(); + let transfer_raw = build_call_tx( + &signer, + chain_id, + 1, + 5_000_000, + tempo_precompiles::PATH_USD_ADDRESS, + transfer_calldata, + ); + setup.node.rpc.inject_tx(transfer_raw).await?; + let transfer_payload = setup.node.advance_block().await?; + + let blk_number = transfer_payload.block().header().inner.number; + let block_gas_used = transfer_payload.block().header().inner.gas_used; + let receipts_total_gas = total_receipt_gas_for_block(&provider, blk_number).await?; + + // No storage creation -> block gas_used should equal total receipt gas + assert_eq!( + block_gas_used, receipts_total_gas, + "block gas_used ({block_gas_used}) should equal total receipt gas \ + ({receipts_total_gas}) for TIP-20 transfer to existing account (no storage creation)" + ); + + Ok(()) +} + +// --------------------------------------------------------------------------- +// Unhappy path / corner case tests +// --------------------------------------------------------------------------- + +/// Unhappy path: a transaction that does SSTORE zero->non-zero then explicitly REVERTs. +/// +/// Even though the tx reverts (state changes rolled back), the storage creation gas +/// is still exempted from protocol limits because protocol limits bound CPU time, and +/// the SSTORE execution cost (5,000) is the only CPU-relevant part regardless of outcome. +/// +/// revm preserves state_gas_spent on all result paths (ok, revert, halt) via +/// `last_frame_result`, so the block header gas_used should still exclude the 245,000 +/// storage creation gas. +#[tokio::test(flavor = "multi_thread")] +async fn test_tip1016_reverted_sstore_still_exempts_state_gas() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC) + .index(0)? + .build()?; + let provider = ProviderBuilder::new().connect_http(setup.node.rpc_url()); + let chain_id = provider.get_chain_id().await?; + + // Deploy a contract whose runtime does SSTORE(0, 1) then REVERT. + // + // Runtime bytecode (10 bytes): + // PUSH1 0x01 PUSH1 0x00 SSTORE PUSH1 0x00 PUSH1 0x00 REVERT + let init_code = Bytes::from_static(&[ + // Init code (12 bytes) + 0x60, 0x0a, // PUSH1 10 (runtime length) + 0x60, 0x0c, // PUSH1 12 (runtime offset) + 0x60, 0x00, // PUSH1 0 (memory dest) + 0x39, // CODECOPY + 0x60, 0x0a, // PUSH1 10 (return length) + 0x60, 0x00, // PUSH1 0 (return offset) + 0xf3, // RETURN + // Runtime code (10 bytes) + 0x60, 0x01, // PUSH1 1 (value) + 0x60, 0x00, // PUSH1 0 (slot) + 0x55, // SSTORE (zero -> non-zero) + 0x60, 0x00, // PUSH1 0 (revert data size) + 0x60, 0x00, // PUSH1 0 (revert data offset) + 0xfd, // REVERT + ]); + + let createx = CreateX::new(CREATEX_ADDRESS, &provider); + let deploy_calldata: Bytes = createx.deployCreate(init_code).calldata().clone(); + + let deploy_raw = build_call_tx( + &signer, + chain_id, + 0, + 5_000_000, + CREATEX_ADDRESS, + deploy_calldata, + ); + setup.node.rpc.inject_tx(deploy_raw).await?; + let deploy_payload = setup.node.advance_block().await?; + let deploy_block_number = deploy_payload.block().header().inner.number; + let contract_addr = get_createx_deployed_address(&provider, deploy_block_number).await?; + + // Call the contract -- it will do SSTORE(0, 1) then REVERT + let call_raw = build_call_tx(&signer, chain_id, 1, 5_000_000, contract_addr, Bytes::new()); + setup.node.rpc.inject_tx(call_raw).await?; + let call_payload = setup.node.advance_block().await?; + + let call_block_number = call_payload.block().header().inner.number; + let block_gas_used = call_payload.block().header().inner.gas_used; + let receipts_total_gas = total_receipt_gas_for_block(&provider, call_block_number).await?; + + // Verify the tx reverted by checking receipts + let block_id = BlockId::Number(BlockNumberOrTag::Number(call_block_number)); + let receipts = provider + .get_block_receipts(block_id) + .await? + .expect("receipts should be available"); + let user_receipt = receipts + .iter() + .find(|r| r.gas_used > 0 && !r.status()) + .expect("should have a reverted user tx receipt"); + assert!(!user_receipt.status(), "tx should have reverted"); + + // Even though the tx reverted, the SSTORE zero->non-zero was attempted and its + // state gas (245,000) should be exempted from protocol limits. + let storage_creation_gas = receipts_total_gas - block_gas_used; + assert_eq!( + storage_creation_gas, 245_000, + "storage creation gas should be 245,000 even for reverted tx, \ + got {storage_creation_gas} (block_gas_used={block_gas_used}, receipts_total_gas={receipts_total_gas})" + ); + + Ok(()) +} + +/// Corner case: multiple SSTORE zero->non-zero in a single transaction. +/// +/// Storage creation gas should be additive: N slots x 245,000 per slot. +/// Block header gas_used should only include the execution component (5,000 per slot). +#[tokio::test(flavor = "multi_thread")] +async fn test_tip1016_multiple_sstore_zero_to_nonzero_additive() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC) + .index(0)? + .build()?; + let provider = ProviderBuilder::new().connect_http(setup.node.rpc_url()); + let chain_id = provider.get_chain_id().await?; + + // Deploy a contract that does 3 SSTOREs: slot 0, 1, 2 all zero->non-zero. + // + // Runtime bytecode (16 bytes): + // PUSH1 0x01 PUSH1 0x00 SSTORE (slot 0 = 1) + // PUSH1 0x01 PUSH1 0x01 SSTORE (slot 1 = 1) + // PUSH1 0x01 PUSH1 0x02 SSTORE (slot 2 = 1) + // STOP + let init_code = Bytes::from_static(&[ + // Init code (12 bytes) + 0x60, 0x10, // PUSH1 16 (runtime length) + 0x60, 0x0c, // PUSH1 12 (runtime offset) + 0x60, 0x00, // PUSH1 0 (memory dest) + 0x39, // CODECOPY + 0x60, 0x10, // PUSH1 16 (return length) + 0x60, 0x00, // PUSH1 0 (return offset) + 0xf3, // RETURN + // Runtime code (16 bytes) + 0x60, 0x01, // PUSH1 1 + 0x60, 0x00, // PUSH1 0 (slot 0) + 0x55, // SSTORE + 0x60, 0x01, // PUSH1 1 + 0x60, 0x01, // PUSH1 1 (slot 1) + 0x55, // SSTORE + 0x60, 0x01, // PUSH1 1 + 0x60, 0x02, // PUSH1 2 (slot 2) + 0x55, // SSTORE + 0x00, // STOP + ]); + + let createx = CreateX::new(CREATEX_ADDRESS, &provider); + let deploy_calldata: Bytes = createx.deployCreate(init_code).calldata().clone(); + + let deploy_raw = build_call_tx( + &signer, + chain_id, + 0, + 5_000_000, + CREATEX_ADDRESS, + deploy_calldata, + ); + setup.node.rpc.inject_tx(deploy_raw).await?; + let deploy_payload = setup.node.advance_block().await?; + let deploy_blk = deploy_payload.block().header().inner.number; + let contract_addr = get_createx_deployed_address(&provider, deploy_blk).await?; + + // Call the contract to trigger 3 SSTOREs zero->non-zero + let call_raw = build_call_tx(&signer, chain_id, 1, 5_000_000, contract_addr, Bytes::new()); + setup.node.rpc.inject_tx(call_raw).await?; + let call_payload = setup.node.advance_block().await?; + + let call_blk = call_payload.block().header().inner.number; + let block_gas_used = call_payload.block().header().inner.gas_used; + let receipts_total_gas = total_receipt_gas_for_block(&provider, call_blk).await?; + + // 3 SSTOREs zero->non-zero: 3 x 245,000 = 735,000 storage creation gas exempted + let storage_creation_gas = receipts_total_gas - block_gas_used; + assert_eq!( + storage_creation_gas, + 3 * 245_000, + "storage creation gas should be 3 x 245,000 = 735,000, \ + got {storage_creation_gas} (block_gas_used={block_gas_used}, receipts_total_gas={receipts_total_gas})" + ); + + Ok(()) +} + +/// Corner case: two storage-creating transactions in the same block. +/// +/// Each tx does SSTORE zero->non-zero. The block's cumulative storage creation gas +/// should be the sum of both (2 x 245,000 = 490,000). This tests that the inner +/// executor's `block_regular_gas_used` correctly excludes state gas across +/// multiple transactions. +#[tokio::test(flavor = "multi_thread")] +async fn test_tip1016_two_storage_txs_same_block() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC) + .index(0)? + .build()?; + let provider = ProviderBuilder::new().connect_http(setup.node.rpc_url()); + let chain_id = provider.get_chain_id().await?; + + // Deploy contract: SSTORE(calldataload(0), 1) -- same as existing test + // + // Runtime (7 bytes): + // PUSH1 0x01 PUSH1 0x00 CALLDATALOAD SSTORE STOP + let init_code = Bytes::from_static(&[ + // Init code (12 bytes) + 0x60, 0x07, // PUSH1 7 (runtime length) + 0x60, 0x0c, // PUSH1 12 (runtime offset) + 0x60, 0x00, // PUSH1 0 (memory dest) + 0x39, // CODECOPY + 0x60, 0x07, // PUSH1 7 (return length) + 0x60, 0x00, // PUSH1 0 (return offset) + 0xf3, // RETURN + // Runtime code (7 bytes) + 0x60, 0x01, // PUSH1 1 (value) + 0x60, 0x00, // PUSH1 0 (calldata offset) + 0x35, // CALLDATALOAD (slot) + 0x55, // SSTORE + 0x00, // STOP + ]); + + let createx = CreateX::new(CREATEX_ADDRESS, &provider); + let deploy_calldata: Bytes = createx.deployCreate(init_code).calldata().clone(); + + let deploy_raw = build_call_tx( + &signer, + chain_id, + 0, + 5_000_000, + CREATEX_ADDRESS, + deploy_calldata, + ); + setup.node.rpc.inject_tx(deploy_raw).await?; + let deploy_payload = setup.node.advance_block().await?; + let deploy_blk = deploy_payload.block().header().inner.number; + let contract_addr = get_createx_deployed_address(&provider, deploy_blk).await?; + + // Submit two txs that each do SSTORE zero->non-zero at different slots + let slot_100: Bytes = alloy_primitives::B256::left_padding_from(&100u64.to_be_bytes()) + .as_slice() + .to_vec() + .into(); + let slot_200: Bytes = alloy_primitives::B256::left_padding_from(&200u64.to_be_bytes()) + .as_slice() + .to_vec() + .into(); + + let tx1_raw = build_call_tx(&signer, chain_id, 1, 5_000_000, contract_addr, slot_100); + let tx2_raw = build_call_tx(&signer, chain_id, 2, 5_000_000, contract_addr, slot_200); + + // Inject both before advancing -- they should land in the same block + setup.node.rpc.inject_tx(tx1_raw).await?; + setup.node.rpc.inject_tx(tx2_raw).await?; + let payload = setup.node.advance_block().await?; + + let blk_number = payload.block().header().inner.number; + let block_gas_used = payload.block().header().inner.gas_used; + let receipts_total_gas = total_receipt_gas_for_block(&provider, blk_number).await?; + + // Verify both user txs were included (non-system txs with gas_limit > 0) + let user_tx_count = payload + .block() + .body() + .transactions() + .filter(|tx| (*tx).gas_limit() > 0) + .count(); + assert!( + user_tx_count >= 2, + "both SSTORE txs should be included in block, got {user_tx_count} user txs" + ); + + // Two SSTOREs zero->non-zero: 2 x 245,000 = 490,000 storage creation gas + let storage_creation_gas = receipts_total_gas - block_gas_used; + assert_eq!( + storage_creation_gas, + 2 * 245_000, + "storage creation gas should be 2 x 245,000 = 490,000 for two txs in same block, \ + got {storage_creation_gas} (block_gas_used={block_gas_used}, receipts_total_gas={receipts_total_gas})" + ); + + Ok(()) +} + +/// Unhappy path: inner CALL that reverts does NOT contribute state gas to the exemption. +/// +/// Contract A calls Contract B. B does SSTORE zero->non-zero then REVERTs. A ignores +/// the failure and STOPs successfully. Since B's frame reverted, `handle_reservoir_remaining_gas` +/// does NOT propagate B's state_gas_spent to A. The overall tx has state_gas_spent == 0, +/// so block gas_used should equal total receipt gas_used (no exemption). +#[tokio::test(flavor = "multi_thread")] +async fn test_tip1016_inner_call_revert_no_state_gas_exemption() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC) + .index(0)? + .build()?; + let provider = ProviderBuilder::new().connect_http(setup.node.rpc_url()); + let chain_id = provider.get_chain_id().await?; + + // Step 1: Deploy "reverting SSTORE" contract (B). + // Runtime: SSTORE(0, 1) then REVERT + let b_init_code = Bytes::from_static(&[ + // Init code (12 bytes) + 0x60, 0x0a, // PUSH1 10 (runtime length) + 0x60, 0x0c, // PUSH1 12 (runtime offset) + 0x60, 0x00, // PUSH1 0 (memory dest) + 0x39, // CODECOPY + 0x60, 0x0a, // PUSH1 10 (return length) + 0x60, 0x00, // PUSH1 0 (return offset) + 0xf3, // RETURN + // Runtime code (10 bytes) + 0x60, 0x01, // PUSH1 1 (value) + 0x60, 0x00, // PUSH1 0 (slot) + 0x55, // SSTORE + 0x60, 0x00, // PUSH1 0 (revert data size) + 0x60, 0x00, // PUSH1 0 (revert data offset) + 0xfd, // REVERT + ]); + + let createx = CreateX::new(CREATEX_ADDRESS, &provider); + let b_deploy_calldata: Bytes = createx.deployCreate(b_init_code).calldata().clone(); + + let b_deploy_raw = build_call_tx( + &signer, + chain_id, + 0, + 5_000_000, + CREATEX_ADDRESS, + b_deploy_calldata, + ); + setup.node.rpc.inject_tx(b_deploy_raw).await?; + let b_deploy_payload = setup.node.advance_block().await?; + let b_deploy_blk = b_deploy_payload.block().header().inner.number; + let b_addr = get_createx_deployed_address(&provider, b_deploy_blk).await?; + + // Step 2: Deploy "caller" contract (A). + // Runtime: CALL(gas=GAS, addr=calldataload(0), value=0, args=0, argsLen=0, ret=0, retLen=0) + // then STOP. + // A ignores B's revert (CALL pushes 0 on failure, but A doesn't check). + let a_init_code = Bytes::from_static(&[ + // Init code (12 bytes) + 0x60, 0x10, // PUSH1 16 (runtime length) + 0x60, 0x0c, // PUSH1 12 (runtime offset) + 0x60, 0x00, // PUSH1 0 (memory dest) + 0x39, // CODECOPY + 0x60, 0x10, // PUSH1 16 (return length) + 0x60, 0x00, // PUSH1 0 (return offset) + 0xf3, // RETURN + // Runtime code (16 bytes) + 0x60, 0x00, // PUSH1 0 (retSize) + 0x60, 0x00, // PUSH1 0 (retOffset) + 0x60, 0x00, // PUSH1 0 (argsSize) + 0x60, 0x00, // PUSH1 0 (argsOffset) + 0x60, 0x00, // PUSH1 0 (value) + 0x60, 0x00, // PUSH1 0 (calldata offset for calldataload) + 0x35, // CALLDATALOAD (loads B's address from calldata) + 0x5a, // GAS (forward all remaining gas) + 0xf1, // CALL + 0x50, // POP (discard CALL return value) + 0x00, // STOP + ]); + + let a_deploy_calldata: Bytes = createx.deployCreate(a_init_code).calldata().clone(); + + let a_deploy_raw = build_call_tx( + &signer, + chain_id, + 1, + 5_000_000, + CREATEX_ADDRESS, + a_deploy_calldata, + ); + setup.node.rpc.inject_tx(a_deploy_raw).await?; + let a_deploy_payload = setup.node.advance_block().await?; + let a_deploy_blk = a_deploy_payload.block().header().inner.number; + let a_addr = get_createx_deployed_address(&provider, a_deploy_blk).await?; + + // Step 3: Call A, passing B's address as calldata. + // A will CALL B, B does SSTORE + REVERT, A continues and STOPs. + let b_addr_calldata: Bytes = alloy_primitives::B256::left_padding_from(b_addr.as_slice()) + .as_slice() + .to_vec() + .into(); + + let call_raw = build_call_tx(&signer, chain_id, 2, 5_000_000, a_addr, b_addr_calldata); + setup.node.rpc.inject_tx(call_raw).await?; + let call_payload = setup.node.advance_block().await?; + + let call_blk = call_payload.block().header().inner.number; + let block_gas_used = call_payload.block().header().inner.gas_used; + let receipts_total_gas = total_receipt_gas_for_block(&provider, call_blk).await?; + + // Verify the tx succeeded (A ignores B's revert) + let block_id = BlockId::Number(BlockNumberOrTag::Number(call_blk)); + let receipts = provider + .get_block_receipts(block_id) + .await? + .expect("receipts should be available"); + let user_receipt = receipts + .iter() + .find(|r| r.gas_used > 21_000) + .expect("should have a user tx receipt with significant gas"); + assert!( + user_receipt.status(), + "tx should succeed (A ignores B's revert)" + ); + + // B's SSTORE reverted, so its state gas should NOT be exempted. + // Block gas_used should equal total receipt gas (no storage creation exemption). + assert_eq!( + block_gas_used, receipts_total_gas, + "block gas_used ({block_gas_used}) should equal total receipt gas \ + ({receipts_total_gas}) -- inner reverted CALL should NOT exempt state gas" + ); + + Ok(()) +} diff --git a/crates/payload/builder/src/lib.rs b/crates/payload/builder/src/lib.rs index 30bfb46d55..c4c69d3135 100644 --- a/crates/payload/builder/src/lib.rs +++ b/crates/payload/builder/src/lib.rs @@ -188,6 +188,8 @@ where self.build_payload( BuildArguments::new( Default::default(), + None, + None, config, Default::default(), Default::default(), @@ -208,7 +210,7 @@ where target = "payload_builder", skip_all, fields( - id = %args.config.payload_id, + id = %args.config.payload_id(), parent_number = %args.config.parent_header.number(), parent_hash = %args.config.parent_header.hash() ) @@ -227,11 +229,12 @@ where config, cancel, best_payload, + .. } = args; let PayloadConfig { parent_header, attributes, - payload_id: _, + .. } = config; let start = Instant::now(); @@ -389,16 +392,20 @@ where let execution_start = Instant::now(); let _block_fill_span = debug_span!(target: "payload_builder", "block_fill").entered(); while let Some(pool_tx) = best_txs.next() { + // TIP-1016: State gas does not count toward block gas capacity, so use + // execution_gas_limit (= gas_limit - state_gas) for the block-level checks below. + let tx_execution_gas_limit = pool_tx.transaction.execution_gas_limit(); + // Ensure we still have capacity for this transaction within the non-shared gas limit. // The remaining `shared_gas_limit` is reserved for validator subblocks and must not // be consumed by proposer's pool transactions. - if cumulative_gas_used + pool_tx.gas_limit() > non_shared_gas_limit { + if cumulative_gas_used + tx_execution_gas_limit > non_shared_gas_limit { // Mark this transaction as invalid since it doesn't fit // The iterator will handle lane switching internally when appropriate best_txs.mark_invalid( &pool_tx, &InvalidPoolTransactionError::ExceedsGasLimit( - pool_tx.gas_limit(), + tx_execution_gas_limit, non_shared_gas_limit - cumulative_gas_used, ), ); @@ -410,7 +417,7 @@ where // If the tx is not a payment and will exceed the general gas limit // mark the tx as invalid and continue if !pool_tx.transaction.is_payment() - && non_payment_gas_used + pool_tx.gas_limit() > general_gas_limit + && non_payment_gas_used + tx_execution_gas_limit > general_gas_limit { best_txs.mark_invalid( &pool_tx, @@ -613,7 +620,7 @@ where block, hashed_state, trie_updates, - } = builder.finish(instrumented_provider)?; + } = builder.finish(instrumented_provider, None)?; drop(_finish_span); let builder_finish_elapsed = builder_finish_start.elapsed(); self.metrics diff --git a/crates/payload/types/src/attrs.rs b/crates/payload/types/src/attrs.rs index e4a1410cc3..463793b926 100644 --- a/crates/payload/types/src/attrs.rs +++ b/crates/payload/types/src/attrs.rs @@ -138,12 +138,6 @@ impl From for TempoPayloadAttributes { impl PayloadAttributes for TempoPayloadAttributes { fn payload_id(&self, parent_hash: &B256) -> PayloadId { - // XXX: derives the payload ID from the parent so that - // overlong payload builds will eventually succeed on the - // next iteration: if all other nodes take equally as long, - // the consensus engine will kill the proposal task. Then eventually - // consensus will circle back to an earlier node, which then - // has the chance of picking up the old payload. payload_id_from_block_hash(parent_hash) } @@ -161,7 +155,7 @@ impl PayloadAttributes for TempoPayloadAttributes { } /// Constructs a [`PayloadId`] from the first 8 bytes of `block_hash`. -fn payload_id_from_block_hash(block_hash: &B256) -> PayloadId { +pub(crate) fn payload_id_from_block_hash(block_hash: &B256) -> PayloadId { PayloadId::new( <[u8; 8]>::try_from(&block_hash[0..8]) .expect("a 32 byte array always has more than 8 bytes"), @@ -235,7 +229,6 @@ mod tests { #[test] fn test_builder_attributes_construction() { - let parent = B256::random(); let recipient = Address::random(); let extra_data = Bytes::from(vec![1, 2, 3, 4, 5]); let timestamp_millis = 1500; // 1s + 500ms @@ -245,10 +238,6 @@ mod tests { TempoPayloadAttributes::new(recipient, timestamp_millis, extra_data.clone(), Vec::new); assert_eq!(attrs.extra_data(), &extra_data); assert_eq!(attrs.suggested_fee_recipient, recipient); - assert_eq!( - attrs.payload_id(&parent), - payload_id_from_block_hash(&parent) - ); assert_eq!(attrs.timestamp(), 1); assert_eq!(attrs.timestamp_millis_part(), 500); @@ -349,11 +338,6 @@ mod tests { let tempo_attrs: TempoPayloadAttributes = eth_attrs.clone().into(); // Inner fields preserved - let parent = B256::random(); - assert_eq!( - tempo_attrs.payload_id(&parent), - payload_id_from_block_hash(&parent) - ); assert_eq!(tempo_attrs.timestamp(), eth_attrs.timestamp); assert_eq!( tempo_attrs.suggested_fee_recipient, diff --git a/crates/precompiles/src/account_keychain/dispatch.rs b/crates/precompiles/src/account_keychain/dispatch.rs index ea89caf587..0bcdf565e4 100644 --- a/crates/precompiles/src/account_keychain/dispatch.rs +++ b/crates/precompiles/src/account_keychain/dispatch.rs @@ -3,11 +3,12 @@ use super::AccountKeychain; use crate::{Precompile, dispatch_call, input_cost, mutate_void, view}; use alloy::{primitives::Address, sol_types::SolInterface}; -use revm::precompile::{PrecompileError, PrecompileResult}; +use alloy_evm::precompiles::PrecompileResultExt; +use revm::precompile::PrecompileError; use tempo_contracts::precompiles::IAccountKeychain::IAccountKeychainCalls; impl Precompile for AccountKeychain { - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; diff --git a/crates/precompiles/src/error.rs b/crates/precompiles/src/error.rs index fddda126cf..1c80ef820f 100644 --- a/crates/precompiles/src/error.rs +++ b/crates/precompiles/src/error.rs @@ -14,9 +14,10 @@ use alloy::{ primitives::{Selector, U256}, sol_types::{Panic, PanicKind, SolError, SolInterface}, }; +use alloy_evm::precompiles::{PrecompileOutputExt, PrecompileResultExt}; use revm::{ - context::journaled_state::JournalLoadErasedError, - precompile::{PrecompileError, PrecompileOutput, PrecompileResult}, + context::journaled_state::JournalLoadErasedError, interpreter::gas::GasTracker, + precompile::PrecompileError, }; use tempo_contracts::precompiles::{ AccountKeychainError, FeeManagerError, NonceError, RolesAuthError, StablecoinDEXError, @@ -134,12 +135,16 @@ impl TempoPrecompileError { Self::Panic(PanicKind::ArrayOutOfBounds) } - /// ABI-encodes this error and wraps it as a reverted [`PrecompileResult`]. + /// ABI-encodes this error and wraps it as a reverted [`PrecompileResultExt`]. + /// + /// `gas_limit` is the original call gas budget; `gas_used` is how much has been consumed. + /// The resulting [`GasTracker`] will have `remaining = gas_limit - gas_used` so the + /// caller recovers unspent gas on revert. /// /// # Errors /// - `PrecompileError::OutOfGas` — if the variant is [`OutOfGas`](Self::OutOfGas) /// - `PrecompileError::Fatal` — if the variant is [`Fatal`](Self::Fatal) - pub fn into_precompile_result(self, gas: u64) -> PrecompileResult { + pub fn into_precompile_result(self, gas_limit: u64, gas_used: u64) -> PrecompileResultExt { let bytes = match self { Self::StablecoinDEX(e) => e.abi_encode().into(), Self::TIP20(e) => e.abi_encode().into(), @@ -160,7 +165,7 @@ impl TempoPrecompileError { Self::ValidatorConfigV2Error(e) => e.abi_encode().into(), Self::AccountKeychainError(e) => e.abi_encode().into(), Self::OutOfGas => { - return Err(PrecompileError::OutOfGas); + return Err(PrecompileError::OutOfGas.into()); } Self::UnknownFunctionSelector(selector) => UnknownFunctionSelector { selector: selector.into(), @@ -168,10 +173,14 @@ impl TempoPrecompileError { .abi_encode() .into(), Self::Fatal(msg) => { - return Err(PrecompileError::Fatal(msg)); + return Err(PrecompileError::Fatal(msg).into()); } }; - Ok(PrecompileOutput::new_reverted(gas, bytes)) + Ok(PrecompileOutputExt { + gas: GasTracker::new(gas_limit, gas_limit - gas_used, 0), + bytes, + reverted: true, + }) } } @@ -246,25 +255,33 @@ pub fn decode_error<'a>(data: &'a [u8]) -> Option` into a [`PrecompileResult`]. +/// Extension trait to convert `Result` into a [`PrecompileResultExt`]. pub trait IntoPrecompileResult { - /// Converts `self` into a [`PrecompileResult`], using `encode_ok` for the success path. + /// Converts `self` into a [`PrecompileResultExt`], using `encode_ok` for the success path. + /// + /// `gas_limit` is the original call gas budget; `gas_used` is how much has been consumed. fn into_precompile_result( self, - gas: u64, + gas_limit: u64, + gas_used: u64, encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes, - ) -> PrecompileResult; + ) -> PrecompileResultExt; } impl IntoPrecompileResult for Result { fn into_precompile_result( self, - gas: u64, + gas_limit: u64, + gas_used: u64, encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes, - ) -> PrecompileResult { + ) -> PrecompileResultExt { match self { - Ok(res) => Ok(PrecompileOutput::new(gas, encode_ok(res))), - Err(err) => err.into_precompile_result(gas), + Ok(res) => Ok(PrecompileOutputExt { + gas: GasTracker::new(gas_limit, gas_limit - gas_used, 0), + bytes: encode_ok(res), + reverted: false, + }), + Err(err) => err.into_precompile_result(gas_limit, gas_used), } } } @@ -272,10 +289,11 @@ impl IntoPrecompileResult for Result { impl IntoPrecompileResult for TempoPrecompileError { fn into_precompile_result( self, - gas: u64, + gas_limit: u64, + gas_used: u64, _encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes, - ) -> PrecompileResult { - Self::into_precompile_result(self, gas) + ) -> PrecompileResultExt { + Self::into_precompile_result(self, gas_limit, gas_used) } } @@ -367,6 +385,44 @@ mod tests { ); } + #[test] + fn test_into_precompile_result_revert_preserves_remaining_gas() { + let gas_limit = 100_000; + let gas_used = 1_000; + + let error = TempoPrecompileError::StablecoinDEX(StablecoinDEXError::order_does_not_exist()); + let result = error.into_precompile_result(gas_limit, gas_used); + + let output = result.expect("business-logic revert should be Ok"); + assert!(output.reverted); + assert_eq!( + output.gas.limit(), + gas_limit, + "gas tracker limit should equal the call's gas limit" + ); + assert_eq!( + output.gas.remaining(), + gas_limit - gas_used, + "remaining gas should equal gas_limit - gas_used" + ); + } + + #[test] + fn test_into_precompile_result_trait_success_preserves_remaining_gas() { + let gas_limit = 100_000; + let gas_used = 500; + + let result: Result = Ok(42); + let precompile_result = result.into_precompile_result(gas_limit, gas_used, |val| { + alloy::primitives::Bytes::from(val.to_be_bytes().to_vec()) + }); + + let output = precompile_result.expect("success should be Ok"); + assert!(!output.reverted); + assert_eq!(output.gas.limit(), gas_limit); + assert_eq!(output.gas.remaining(), gas_limit - gas_used); + } + #[test] fn test_decode_error_with_tip20_error() { // Use insufficient_allowance which has a unique selector (no collision with other errors) diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs index e6094e6a70..895d303525 100644 --- a/crates/precompiles/src/lib.rs +++ b/crates/precompiles/src/lib.rs @@ -43,11 +43,14 @@ use alloy::{ sol, sol_types::{SolCall, SolError}, }; -use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap}; +use alloy_evm::precompiles::{ + DynPrecompile, PrecompileOutputExt, PrecompileResultExt, PrecompilesMap, +}; use revm::{ context::CfgEnv, handler::EthPrecompiles, - precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult}, + interpreter::gas::GasTracker, + precompile::{PrecompileError, PrecompileId}, primitives::hardfork::SpecId, }; @@ -87,9 +90,10 @@ pub trait Precompile { /// 4-byte function selector from `calldata` and route to the matching method using /// `dispatch_call` combined with the `view`, `mutate`, or `mutate_void` helpers. /// - /// Business-logic errors are returned as reverted [`PrecompileOutput`]s with ABI-encoded - /// error data, while fatal failures (e.g. out-of-gas) are returned as [`PrecompileError`]. - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult; + /// Business-logic errors are returned as reverted [`PrecompileOutputExt`]s with ABI-encoded + /// error data, while fatal failures (e.g. out-of-gas) are returned as + /// [`PrecompileErrorExt`](alloy_evm::precompiles::PrecompileErrorExt). + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt; } /// Returns the full Tempo precompiles for the given config. @@ -151,10 +155,11 @@ macro_rules! tempo_precompile { let gas_params = $cfg.gas_params.clone(); DynPrecompile::new_stateful(PrecompileId::Custom($id.into()), move |$input| { if !$input.is_direct_call() { - return Ok(PrecompileOutput::new_reverted( - 0, - DelegateCallNotAllowed {}.abi_encode().into(), - )); + return Ok(PrecompileOutputExt { + gas: GasTracker::new($input.gas, $input.gas, 0), + bytes: DelegateCallNotAllowed {}.abi_encode().into(), + reverted: true, + }); } let mut storage = crate::storage::evm::EvmPrecompileStorageProvider::new( $input.internals, @@ -237,14 +242,14 @@ impl ValidatorConfigV2 { /// Dispatches a parameterless view call, encoding the return via `T`. #[inline] -fn metadata(f: impl FnOnce() -> Result) -> PrecompileResult { - f().into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into()) +fn metadata(f: impl FnOnce() -> Result) -> PrecompileResultExt { + f().into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into()) } /// Dispatches a read-only call with decoded arguments, encoding the return via `T`. #[inline] -fn view(call: T, f: impl FnOnce(T) -> Result) -> PrecompileResult { - f(call).into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into()) +fn view(call: T, f: impl FnOnce(T) -> Result) -> PrecompileResultExt { + f(call).into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into()) } /// Dispatches a state-mutating call that returns ABI-encoded data. @@ -255,14 +260,15 @@ fn mutate( call: T, sender: Address, f: impl FnOnce(Address, T) -> Result, -) -> PrecompileResult { +) -> PrecompileResultExt { if StorageCtx.is_static() { - return Ok(PrecompileOutput::new_reverted( - 0, - StaticCallNotAllowed {}.abi_encode().into(), - )); + return Ok(PrecompileOutputExt { + gas: GasTracker::new(0, 0, 0), + bytes: StaticCallNotAllowed {}.abi_encode().into(), + reverted: true, + }); } - f(sender, call).into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into()) + f(sender, call).into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into()) } /// Dispatches a state-mutating call that returns no data (e.g. `approve`, `transfer`). @@ -273,32 +279,39 @@ fn mutate_void( call: T, sender: Address, f: impl FnOnce(Address, T) -> Result<()>, -) -> PrecompileResult { +) -> PrecompileResultExt { if StorageCtx.is_static() { - return Ok(PrecompileOutput::new_reverted( - 0, - StaticCallNotAllowed {}.abi_encode().into(), - )); + return Ok(PrecompileOutputExt { + gas: GasTracker::new(0, 0, 0), + bytes: StaticCallNotAllowed {}.abi_encode().into(), + reverted: true, + }); } - f(sender, call).into_precompile_result(0, |()| Bytes::new()) + f(sender, call).into_precompile_result(0, 0, |()| Bytes::new()) } -/// Fills gas accounting fields on a [`PrecompileOutput`] from the storage context. +/// Fills gas accounting fields on a [`PrecompileOutputExt`] from the storage context. #[inline] -fn fill_precompile_output(mut output: PrecompileOutput, storage: &StorageCtx) -> PrecompileOutput { - output.gas_used = storage.gas_used(); +fn fill_precompile_output( + mut output: PrecompileOutputExt, + storage: &StorageCtx, +) -> PrecompileOutputExt { + let gas_limit = storage.gas_limit(); + let gas_used = storage.gas_used(); + output.gas = GasTracker::new(gas_limit, gas_limit - gas_used, 0); // add refund only if it is not reverted if !output.reverted { - output.gas_refunded = storage.gas_refunded(); + output.gas.set_refunded(storage.gas_refunded()); } output } /// Returns an ABI-encoded `UnknownFunctionSelector` revert for the given 4-byte selector. #[inline] -pub fn unknown_selector(selector: [u8; 4], gas: u64) -> PrecompileResult { - error::TempoPrecompileError::UnknownFunctionSelector(selector).into_precompile_result(gas) +pub fn unknown_selector(selector: [u8; 4], gas_limit: u64, gas_used: u64) -> PrecompileResultExt { + error::TempoPrecompileError::UnknownFunctionSelector(selector) + .into_precompile_result(gas_limit, gas_used) } /// Decodes calldata via `decode`, then dispatches to `f`. @@ -311,20 +324,24 @@ pub fn unknown_selector(selector: [u8; 4], gas: u64) -> PrecompileResult { fn dispatch_call( calldata: &[u8], decode: impl FnOnce(&[u8]) -> core::result::Result, - f: impl FnOnce(T) -> PrecompileResult, -) -> PrecompileResult { + f: impl FnOnce(T) -> PrecompileResultExt, +) -> PrecompileResultExt { let storage = StorageCtx::default(); if calldata.len() < 4 { if storage.spec().is_t1() { return Ok(fill_precompile_output( - PrecompileOutput::new_reverted(0, Bytes::new()), + PrecompileOutputExt { + gas: GasTracker::new(0, 0, 0), + bytes: Bytes::new(), + reverted: true, + }, &storage, )); } else { - return Err(PrecompileError::Other( - "Invalid input: missing function selector".into(), - )); + return Err( + PrecompileError::Other("Invalid input: missing function selector".into()).into(), + ); } } let result = decode(calldata); @@ -332,11 +349,15 @@ fn dispatch_call( match result { Ok(call) => f(call).map(|res| fill_precompile_output(res, &storage)), Err(alloy::sol_types::Error::UnknownSelector { selector, .. }) => { - unknown_selector(*selector, storage.gas_used()) + unknown_selector(*selector, storage.gas_limit(), storage.gas_used()) .map(|res| fill_precompile_output(res, &storage)) } Err(_) => Ok(fill_precompile_output( - PrecompileOutput::new_reverted(0, Bytes::new()), + PrecompileOutputExt { + gas: GasTracker::new(0, 0, 0), + bytes: Bytes::new(), + reverted: true, + }, &storage, )), } @@ -344,7 +365,7 @@ fn dispatch_call( /// Asserts that `result` is a reverted output whose bytes decode to `expected_error`. #[cfg(test)] -pub fn expect_precompile_revert(result: &PrecompileResult, expected_error: E) +pub fn expect_precompile_revert(result: &PrecompileResultExt, expected_error: E) where E: SolInterface + PartialEq + std::fmt::Debug, { @@ -400,6 +421,7 @@ mod tests { is_static: false, target_address, bytecode_address, + reservoir: 0, }; let result = AlloyEvmPrecompile::call(&precompile, input); @@ -446,6 +468,7 @@ mod tests { value: U256::ZERO, target_address: token_address, bytecode_address: token_address, + reservoir: 0, }; AlloyEvmPrecompile::call(&precompile, input) @@ -520,6 +543,7 @@ mod tests { value: U256::ZERO, target_address: PATH_USD_ADDRESS, bytecode_address: PATH_USD_ADDRESS, + reservoir: 0, }; AlloyEvmPrecompile::call(&precompile, input) @@ -530,8 +554,9 @@ mod tests { .expect("T1: expected Ok with reverted output"); assert!(empty.reverted, "T1: expected reverted output"); assert!(empty.bytes.is_empty()); - assert!(empty.gas_used != 0); - assert_eq!(empty.gas_refunded, 0); + // Gas was consumed (remaining < gas_limit) + assert!(empty.gas.remaining() < 1_000_000); + assert_eq!(empty.gas.refunded(), 0); // T1: unknown selector should return a reverted output with UnknownFunctionSelector error let unknown = call_with_spec(Bytes::from([0xAA; 4]), TempoHardfork::T1) @@ -545,15 +570,15 @@ mod tests { assert_eq!(decoded.selector.as_slice(), &[0xAA, 0xAA, 0xAA, 0xAA]); // Verify gas is tracked for both cases (unknown selector may cost slightly more due `INPUT_PER_WORD_COST`) - assert!(unknown.gas_used >= empty.gas_used); - assert_eq!(unknown.gas_refunded, empty.gas_refunded); + assert!(unknown.gas.remaining() <= empty.gas.remaining()); + assert_eq!(unknown.gas.refunded(), empty.gas.refunded()); // Pre-T1 (T0): invalid calldata should return PrecompileError let result = call_with_spec(Bytes::new(), TempoHardfork::T0); assert!( matches!( &result, - Err(PrecompileError::Other(msg)) if msg.contains("missing function selector") + Err(failure) if matches!(&failure.precompile_error, PrecompileError::Other(msg) if msg.contains("missing function selector")) ), "T0: expected PrecompileError for invalid calldata, got {result:?}" ); diff --git a/crates/precompiles/src/nonce/dispatch.rs b/crates/precompiles/src/nonce/dispatch.rs index 8a9df29912..5d4a8007f8 100644 --- a/crates/precompiles/src/nonce/dispatch.rs +++ b/crates/precompiles/src/nonce/dispatch.rs @@ -2,11 +2,12 @@ use crate::{Precompile, dispatch_call, input_cost, nonce::NonceManager, view}; use alloy::{primitives::Address, sol_types::SolInterface}; -use revm::precompile::{PrecompileError, PrecompileResult}; +use alloy_evm::precompiles::PrecompileResultExt; +use revm::precompile::PrecompileError; use tempo_contracts::precompiles::INonce::INonceCalls; impl Precompile for NonceManager { - fn call(&mut self, calldata: &[u8], _msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], _msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; diff --git a/crates/precompiles/src/stablecoin_dex/dispatch.rs b/crates/precompiles/src/stablecoin_dex/dispatch.rs index e7b4067ec5..7ca9d67b26 100644 --- a/crates/precompiles/src/stablecoin_dex/dispatch.rs +++ b/crates/precompiles/src/stablecoin_dex/dispatch.rs @@ -1,7 +1,8 @@ //! ABI dispatch for the [`StablecoinDEX`] precompile. use alloy::{primitives::Address, sol_types::SolInterface}; -use revm::precompile::{PrecompileError, PrecompileResult}; +use alloy_evm::precompiles::PrecompileResultExt; +use revm::precompile::PrecompileError; use tempo_contracts::precompiles::IStablecoinDEX::IStablecoinDEXCalls; use crate::{ @@ -11,7 +12,7 @@ use crate::{ }; impl Precompile for StablecoinDEX { - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; diff --git a/crates/precompiles/src/storage/evm.rs b/crates/precompiles/src/storage/evm.rs index a8eda374b6..a8c0e6ca52 100644 --- a/crates/precompiles/src/storage/evm.rs +++ b/crates/precompiles/src/storage/evm.rs @@ -1,7 +1,10 @@ use alloy::primitives::{Address, Log, LogData, U256}; use alloy_evm::{EvmInternals, EvmInternalsError}; use revm::{ - context::{Block, CfgEnv, journaled_state::JournalCheckpoint}, + context::{ + Block, CfgEnv, + journaled_state::{JournalCheckpoint, JournalLoadError}, + }, context_interface::cfg::{GasParams, gas}, state::{AccountInfo, Bytecode}, }; @@ -226,6 +229,11 @@ impl<'a> PrecompileStorageProvider for EvmPrecompileStorageProvider<'a> { self.gas_refunded = self.gas_refunded.saturating_add(gas); } + #[inline] + fn gas_limit(&self) -> u64 { + self.gas_limit + } + #[inline] fn gas_used(&self) -> u64 { self.gas_limit - self.gas_remaining @@ -302,6 +310,15 @@ impl From for TempoPrecompileError { } } +impl From> for TempoPrecompileError { + fn from(value: JournalLoadError) -> Self { + match value { + JournalLoadError::DBError(e) => Self::Fatal(e.to_string()), + JournalLoadError::ColdLoadSkipped => Self::OutOfGas, + } + } +} + /// Deducts gas from the remaining gas and returns an error if insufficient. #[inline] pub fn deduct_gas(gas: &mut u64, additional_cost: u64) -> Result<(), TempoPrecompileError> { diff --git a/crates/precompiles/src/storage/hashmap.rs b/crates/precompiles/src/storage/hashmap.rs index 36b591844d..4fadaf3bcb 100644 --- a/crates/precompiles/src/storage/hashmap.rs +++ b/crates/precompiles/src/storage/hashmap.rs @@ -154,6 +154,10 @@ impl PrecompileStorageProvider for HashMapStorageProvider { // No-op } + fn gas_limit(&self) -> u64 { + 0 + } + fn gas_used(&self) -> u64 { 0 } diff --git a/crates/precompiles/src/storage/mod.rs b/crates/precompiles/src/storage/mod.rs index bf2d9a443a..b6bbb70cfc 100644 --- a/crates/precompiles/src/storage/mod.rs +++ b/crates/precompiles/src/storage/mod.rs @@ -82,6 +82,9 @@ pub trait PrecompileStorageProvider { /// Add refund to the refund gas counter. fn refund_gas(&mut self, gas: i64); + /// Returns the gas limit for this precompile call. + fn gas_limit(&self) -> u64; + /// Returns the gas used so far. fn gas_used(&self) -> u64; diff --git a/crates/precompiles/src/storage/thread_local.rs b/crates/precompiles/src/storage/thread_local.rs index 0f4ff25f34..0c8d535093 100644 --- a/crates/precompiles/src/storage/thread_local.rs +++ b/crates/precompiles/src/storage/thread_local.rs @@ -166,6 +166,11 @@ impl StorageCtx { Self::with_storage(|s| s.refund_gas(gas)) } + /// Returns the gas limit for this precompile call. + pub fn gas_limit(&self) -> u64 { + Self::with_storage(|s| s.gas_limit()) + } + /// Returns the gas used so far. pub fn gas_used(&self) -> u64 { Self::with_storage(|s| s.gas_used()) diff --git a/crates/precompiles/src/test_util.rs b/crates/precompiles/src/test_util.rs index 33c3481633..bdc60768f0 100644 --- a/crates/precompiles/src/test_util.rs +++ b/crates/precompiles/src/test_util.rs @@ -12,6 +12,7 @@ use alloy::{ primitives::{Address, B256, U256}, sol_types::SolError, }; +use alloy_evm::precompiles::PrecompileErrorExt; use revm::precompile::PrecompileError; #[cfg(any(test, feature = "test-utils"))] use tempo_contracts::precompiles::TIP20Error; @@ -38,7 +39,7 @@ pub fn check_selector_coverage( // Check if we got "Unknown function selector" error (old format) let is_unsupported_old = matches!(&result, - Err(PrecompileError::Other(msg)) if msg.contains("Unknown function selector") + Err(PrecompileErrorExt { precompile_error: PrecompileError::Other(msg), .. }) if msg.contains("Unknown function selector") ); // Check if we got "Unknown function selector" error (new format - ABI-encoded) diff --git a/crates/precompiles/src/tip20/dispatch.rs b/crates/precompiles/src/tip20/dispatch.rs index 0dbbfb9661..2710117de0 100644 --- a/crates/precompiles/src/tip20/dispatch.rs +++ b/crates/precompiles/src/tip20/dispatch.rs @@ -12,7 +12,8 @@ use alloy::{ primitives::Address, sol_types::{SolCall, SolInterface}, }; -use revm::precompile::{PrecompileError, PrecompileResult}; +use alloy_evm::precompiles::PrecompileResultExt; +use revm::precompile::PrecompileError; use tempo_contracts::precompiles::{IRolesAuth::IRolesAuthCalls, ITIP20::ITIP20Calls, TIP20Error}; /// Decoded call variant — either a TIP-20 token call or a role-management call. @@ -35,7 +36,7 @@ impl TIP20Call { } impl Precompile for TIP20Token { - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; @@ -44,7 +45,7 @@ impl Precompile for TIP20Token { // Note that if the initialization check fails, this is treated as uninitialized if !self.is_initialized().unwrap_or(false) { return TempoPrecompileError::TIP20(TIP20Error::uninitialized()) - .into_precompile_result(self.storage.gas_used()); + .into_precompile_result(self.storage.gas_limit(), self.storage.gas_used()); } dispatch_call(calldata, TIP20Call::decode, |call| match call { @@ -171,13 +172,21 @@ impl Precompile for TIP20Token { TIP20Call::TIP20(ITIP20Calls::permit(call)) => { if !self.storage.spec().is_t2() { - return unknown_selector(ITIP20::permitCall::SELECTOR, self.storage.gas_used()); + return unknown_selector( + ITIP20::permitCall::SELECTOR, + self.storage.gas_limit(), + self.storage.gas_used(), + ); } mutate_void(call, msg_sender, |_s, c| self.permit(c)) } TIP20Call::TIP20(ITIP20Calls::nonces(call)) => { if !self.storage.spec().is_t2() { - return unknown_selector(ITIP20::noncesCall::SELECTOR, self.storage.gas_used()); + return unknown_selector( + ITIP20::noncesCall::SELECTOR, + self.storage.gas_limit(), + self.storage.gas_used(), + ); } view(call, |c| self.nonces(c)) } @@ -185,6 +194,7 @@ impl Precompile for TIP20Token { if !self.storage.spec().is_t2() { return unknown_selector( ITIP20::DOMAIN_SEPARATORCall::SELECTOR, + self.storage.gas_limit(), self.storage.gas_used(), ); } @@ -257,7 +267,9 @@ mod tests { let mut token = TIP20Setup::create("Test", "TST", sender).apply()?; let result = token.call(&Bytes::from([0x12, 0x34]), sender); - assert!(matches!(result, Err(PrecompileError::Other(_)))); + assert!( + matches!(result, Err(f) if matches!(f.precompile_error, PrecompileError::Other(_))) + ); Ok(()) }) @@ -280,7 +292,7 @@ mod tests { let calldata = balance_of_call.abi_encode(); let result = token.call(&calldata, sender)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let decoded = U256::abi_decode(&result.bytes)?; assert_eq!(decoded, test_balance); @@ -311,7 +323,7 @@ mod tests { let calldata = mint_call.abi_encode(); let result = token.call(&calldata, sender)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let final_balance = token.balance_of(ITIP20::balanceOfCall { account: recipient })?; assert_eq!(final_balance, mint_amount); @@ -349,7 +361,7 @@ mod tests { }; let calldata = transfer_call.abi_encode(); let result = token.call(&calldata, sender)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let success = bool::abi_decode(&result.bytes)?; assert!(success); @@ -391,7 +403,7 @@ mod tests { }; let calldata = approve_call.abi_encode(); let result = token.call(&calldata, owner)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let success = bool::abi_decode(&result.bytes)?; assert!(success); @@ -405,7 +417,7 @@ mod tests { }; let calldata = transfer_from_call.abi_encode(); let result = token.call(&calldata, spender)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let success = bool::abi_decode(&result.bytes)?; assert!(success); @@ -444,14 +456,14 @@ mod tests { let pause_call = ITIP20::pauseCall {}; let calldata = pause_call.abi_encode(); let result = token.call(&calldata, pauser)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); assert!(token.paused()?); // Unpause the token let unpause_call = ITIP20::unpauseCall {}; let calldata = unpause_call.abi_encode(); let result = token.call(&calldata, unpauser)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); assert!(!token.paused()?); Ok(()) @@ -485,7 +497,7 @@ mod tests { }; let calldata = burn_call.abi_encode(); let result = token.call(&calldata, burner)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); assert_eq!( token.balance_of(ITIP20::balanceOfCall { account: burner })?, initial_balance - burn_amount @@ -509,7 +521,7 @@ mod tests { let calldata = name_call.abi_encode(); let result = token.call(&calldata, caller)?; // HashMapStorageProvider does not do gas accounting, so we expect 0 here. - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let name = String::abi_decode(&result.bytes)?; assert_eq!(name, "Test Token"); @@ -517,7 +529,7 @@ mod tests { let symbol_call = ITIP20::symbolCall {}; let calldata = symbol_call.abi_encode(); let result = token.call(&calldata, caller)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let symbol = String::abi_decode(&result.bytes)?; assert_eq!(symbol, "TEST"); @@ -525,7 +537,7 @@ mod tests { let decimals_call = ITIP20::decimalsCall {}; let calldata = decimals_call.abi_encode(); let result = token.call(&calldata, caller)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let decimals = ITIP20::decimalsCall::abi_decode_returns(&result.bytes)?; assert_eq!(decimals, 6); @@ -533,7 +545,7 @@ mod tests { let currency_call = ITIP20::currencyCall {}; let calldata = currency_call.abi_encode(); let result = token.call(&calldata, caller)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let currency = String::abi_decode(&result.bytes)?; assert_eq!(currency, "USD"); @@ -542,7 +554,7 @@ mod tests { let calldata = total_supply_call.abi_encode(); let result = token.call(&calldata, caller)?; // HashMapStorageProvider does not do gas accounting, so we expect 0 here. - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let total_supply = U256::abi_decode(&result.bytes)?; assert_eq!(total_supply, U256::ZERO); @@ -567,7 +579,7 @@ mod tests { }; let calldata = set_cap_call.abi_encode(); let result = token.call(&calldata, admin)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let mint_call = ITIP20::mintCall { to: recipient, @@ -603,7 +615,7 @@ mod tests { }; let calldata = has_role_call.abi_encode(); let result = token.call(&calldata, admin)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let has_role = bool::abi_decode(&result.bytes)?; assert!(has_role); @@ -627,7 +639,7 @@ mod tests { assert_eq!(output.bytes, expected); let result = token.call(&calldata, user1)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); Ok(()) }) @@ -655,7 +667,7 @@ mod tests { }; let calldata = transfer_call.abi_encode(); let result = token.call(&calldata, sender)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); assert_eq!( token.balance_of(ITIP20::balanceOfCall { account: sender })?, initial_balance - transfer_amount @@ -695,7 +707,7 @@ mod tests { }; let calldata = change_policy_call.abi_encode(); let result = token.call(&calldata, admin)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); assert_eq!(token.transfer_policy_id()?, new_policy_id); // Create another valid policy for the unauthorized test diff --git a/crates/precompiles/src/tip20_factory/dispatch.rs b/crates/precompiles/src/tip20_factory/dispatch.rs index 1d3c388f89..25dab32337 100644 --- a/crates/precompiles/src/tip20_factory/dispatch.rs +++ b/crates/precompiles/src/tip20_factory/dispatch.rs @@ -2,11 +2,12 @@ use crate::{Precompile, dispatch_call, input_cost, mutate, tip20_factory::TIP20Factory, view}; use alloy::{primitives::Address, sol_types::SolInterface}; -use revm::precompile::{PrecompileError, PrecompileResult}; +use alloy_evm::precompiles::PrecompileResultExt; +use revm::precompile::PrecompileError; use tempo_contracts::precompiles::ITIP20Factory::ITIP20FactoryCalls; impl Precompile for TIP20Factory { - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; diff --git a/crates/precompiles/src/tip403_registry/dispatch.rs b/crates/precompiles/src/tip403_registry/dispatch.rs index 75e6172ad2..c812820ecc 100644 --- a/crates/precompiles/src/tip403_registry/dispatch.rs +++ b/crates/precompiles/src/tip403_registry/dispatch.rs @@ -9,14 +9,15 @@ use alloy::{ primitives::Address, sol_types::{SolCall, SolInterface}, }; -use revm::precompile::{PrecompileError, PrecompileResult}; +use alloy_evm::precompiles::PrecompileResultExt; +use revm::precompile::PrecompileError; use tempo_contracts::precompiles::ITIP403Registry::{ ITIP403RegistryCalls, compoundPolicyDataCall, createCompoundPolicyCall, isAuthorizedMintRecipientCall, isAuthorizedRecipientCall, isAuthorizedSenderCall, }; impl Precompile for TIP403Registry { - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; @@ -38,6 +39,7 @@ impl Precompile for TIP403Registry { if !self.storage.spec().is_t2() { return unknown_selector( isAuthorizedSenderCall::SELECTOR, + self.storage.gas_limit(), self.storage.gas_used(), ); } @@ -49,6 +51,7 @@ impl Precompile for TIP403Registry { if !self.storage.spec().is_t2() { return unknown_selector( isAuthorizedRecipientCall::SELECTOR, + self.storage.gas_limit(), self.storage.gas_used(), ); } @@ -60,6 +63,7 @@ impl Precompile for TIP403Registry { if !self.storage.spec().is_t2() { return unknown_selector( isAuthorizedMintRecipientCall::SELECTOR, + self.storage.gas_limit(), self.storage.gas_used(), ); } @@ -71,6 +75,7 @@ impl Precompile for TIP403Registry { if !self.storage.spec().is_t2() { return unknown_selector( compoundPolicyDataCall::SELECTOR, + self.storage.gas_limit(), self.storage.gas_used(), ); } @@ -98,6 +103,7 @@ impl Precompile for TIP403Registry { if !self.storage.spec().is_t2() { return unknown_selector( createCompoundPolicyCall::SELECTOR, + self.storage.gas_limit(), self.storage.gas_used(), ); } diff --git a/crates/precompiles/src/tip_fee_manager/dispatch.rs b/crates/precompiles/src/tip_fee_manager/dispatch.rs index 0dd937a059..64bb474208 100644 --- a/crates/precompiles/src/tip_fee_manager/dispatch.rs +++ b/crates/precompiles/src/tip_fee_manager/dispatch.rs @@ -10,7 +10,8 @@ use crate::{ view, }; use alloy::{primitives::Address, sol_types::SolInterface}; -use revm::precompile::{PrecompileError, PrecompileResult}; +use alloy_evm::precompiles::PrecompileResultExt; +use revm::precompile::PrecompileError; use tempo_contracts::precompiles::{IFeeManager::IFeeManagerCalls, ITIPFeeAMM::ITIPFeeAMMCalls}; /// Unified calldata discriminant for both `IFeeManager` and `ITIPFeeAMM` selectors. @@ -33,7 +34,7 @@ impl TipFeeManagerCall { } impl Precompile for TipFeeManager { - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; @@ -170,12 +171,12 @@ mod tests { } .abi_encode(); let result = fee_manager.call(&calldata, validator)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); // Verify token was set let calldata = IFeeManager::validatorTokensCall { validator }.abi_encode(); let result = fee_manager.call(&calldata, validator)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let returned_token = Address::abi_decode(&result.bytes)?; assert_eq!(returned_token, token.address()); @@ -215,12 +216,12 @@ mod tests { } .abi_encode(); let result = fee_manager.call(&calldata, user)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); // Verify token was set let calldata = IFeeManager::userTokensCall { user }.abi_encode(); let result = fee_manager.call(&calldata, user)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let returned_token = Address::abi_decode(&result.bytes)?; assert_eq!(returned_token, token.address()); @@ -261,7 +262,7 @@ mod tests { } .abi_encode(); let result = fee_manager.call(&calldata, sender)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); let returned_id = B256::abi_decode(&result.bytes)?; let expected_id = PoolKey::new(token_a, token_b).get_id(); @@ -287,7 +288,7 @@ mod tests { }; let calldata = get_pool_call.abi_encode(); let result = fee_manager.call(&calldata, sender)?; - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); // Decode and verify pool (should be empty initially) let pool = ITIPFeeAMM::Pool::abi_decode(&result.bytes)?; diff --git a/crates/precompiles/src/validator_config/dispatch.rs b/crates/precompiles/src/validator_config/dispatch.rs index f66b1c2360..e7a4685902 100644 --- a/crates/precompiles/src/validator_config/dispatch.rs +++ b/crates/precompiles/src/validator_config/dispatch.rs @@ -9,13 +9,14 @@ use alloy::{ primitives::Address, sol_types::{SolCall, SolInterface}, }; -use revm::precompile::{PrecompileError, PrecompileResult}; +use alloy_evm::precompiles::PrecompileResultExt; +use revm::precompile::PrecompileError; use tempo_contracts::precompiles::IValidatorConfig::{ IValidatorConfigCalls, changeValidatorStatusByIndexCall, }; impl Precompile for ValidatorConfig { - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; @@ -57,6 +58,7 @@ impl Precompile for ValidatorConfig { if !self.storage.spec().is_t1() { return unknown_selector( changeValidatorStatusByIndexCall::SELECTOR, + self.storage.gas_limit(), self.storage.gas_used(), ); } @@ -122,7 +124,9 @@ mod tests { validator_config.initialize(owner)?; let result = validator_config.call(&[0x12, 0x34], sender); - assert!(matches!(result, Err(PrecompileError::Other(_)))); + assert!( + matches!(result, Err(f) if matches!(f.precompile_error, PrecompileError::Other(_))) + ); Ok(()) }) @@ -145,7 +149,7 @@ mod tests { let result = validator_config.call(&calldata, sender)?; // HashMapStorageProvider does not do gas accounting, so we expect 0 here. - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); // Verify we get the correct owner let decoded = Address::abi_decode(&result.bytes)?; @@ -180,7 +184,7 @@ mod tests { let result = validator_config.call(&calldata, owner)?; // HashMapStorageProvider does not have gas accounting, so we expect 0 - assert_eq!(result.gas_used, 0); + assert_eq!(result.gas.remaining(), 0); // Verify validator was added by calling getValidators let validators = validator_config.get_validators()?; diff --git a/crates/precompiles/src/validator_config_v2/dispatch.rs b/crates/precompiles/src/validator_config_v2/dispatch.rs index 44346aa9f7..157205fd0b 100644 --- a/crates/precompiles/src/validator_config_v2/dispatch.rs +++ b/crates/precompiles/src/validator_config_v2/dispatch.rs @@ -3,21 +3,25 @@ use super::*; use crate::{Precompile, dispatch_call, input_cost, mutate, mutate_void, view}; use alloy::{primitives::Address, sol_types::SolInterface}; -use revm::precompile::{PrecompileError, PrecompileOutput, PrecompileResult}; +use alloy_evm::precompiles::{PrecompileOutputExt, PrecompileResultExt}; +use revm::{interpreter::gas::GasTracker, precompile::PrecompileError}; use tempo_contracts::precompiles::IValidatorConfigV2::IValidatorConfigV2Calls; impl Precompile for ValidatorConfigV2 { - fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult { + fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResultExt { self.storage .deduct_gas(input_cost(calldata.len())) .map_err(|_| PrecompileError::OutOfGas)?; // Pre-T2: behave like an empty contract (call succeeds, no execution) if !self.storage.spec().is_t2() { - return Ok(PrecompileOutput::new( - self.storage.gas_used(), - Default::default(), - )); + let gas_limit = self.storage.gas_limit(); + let gas_used = self.storage.gas_used(); + return Ok(PrecompileOutputExt { + gas: GasTracker::new(gas_limit, gas_limit - gas_used, 0), + bytes: Default::default(), + reverted: false, + }); } dispatch_call( diff --git a/crates/primitives/src/reth_compat/header.rs b/crates/primitives/src/reth_compat/header.rs index 483698cefa..fe7307bfd2 100644 --- a/crates/primitives/src/reth_compat/header.rs +++ b/crates/primitives/src/reth_compat/header.rs @@ -52,7 +52,7 @@ mod codec { } } - impl reth_db_api::table::Decompress for TempoHeader { + impl reth_codecs::Decompress for TempoHeader { fn decompress(value: &[u8]) -> Result { let (obj, _) = reth_codecs::Compact::from_compact(value, value.len()); Ok(obj) diff --git a/crates/primitives/src/reth_compat/transaction/envelope.rs b/crates/primitives/src/reth_compat/transaction/envelope.rs index afb3995631..26c236c61a 100644 --- a/crates/primitives/src/reth_compat/transaction/envelope.rs +++ b/crates/primitives/src/reth_compat/transaction/envelope.rs @@ -29,7 +29,7 @@ mod codec { bytes::{self, BufMut}, }; use reth_codecs::{ - Compact, DecompressError, + Compact, alloy::transaction::{CompactEnvelope, Envelope}, txtype::{ COMPACT_EXTENDED_IDENTIFIER_FLAG, COMPACT_IDENTIFIER_EIP1559, @@ -180,8 +180,8 @@ mod codec { } } - impl reth_db_api::table::Decompress for TempoTxEnvelope { - fn decompress(value: &[u8]) -> Result { + impl reth_codecs::Decompress for TempoTxEnvelope { + fn decompress(value: &[u8]) -> Result { let (obj, _) = Compact::from_compact(value, value.len()); Ok(obj) } diff --git a/crates/revm/src/common.rs b/crates/revm/src/common.rs index 36670726bd..9b9ddf8596 100644 --- a/crates/revm/src/common.rs +++ b/crates/revm/src/common.rs @@ -348,6 +348,10 @@ where self.spec } + fn gas_limit(&self) -> u64 { + 0 + } + fn is_static(&self) -> bool { // read-only operations should always be static true diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index 59cfca344c..8088d56beb 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -37,6 +37,11 @@ pub struct TempoEvm { /// /// Additional initial gas cost is added for authorization_key setting in pre execution. pub(crate) initial_gas: u64, + /// TIP-1016: Additional initial state gas accumulated after validate_initial_tx_gas. + /// + /// Tracks state gas from runtime checks in validate_against_state_and_deduct_caller + /// (e.g., 2D nonce + CREATE + caller nonce == 0) that can't be determined upfront. + pub(crate) initial_state_gas: u64, } impl TempoEvm { @@ -70,6 +75,7 @@ impl TempoEvm { logs: Vec::new(), collected_fee: U256::ZERO, initial_gas: 0, + initial_state_gas: 0, } } } @@ -887,7 +893,7 @@ mod tests { let result1 = evm.transact_commit(tx_env1)?; assert!(result1.is_success()); - assert_eq!(result1.gas_used(), 28_671); + assert_eq!(result1.tx_gas_used(), 28_671); let ctx = &mut evm.ctx; let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx); @@ -916,7 +922,7 @@ mod tests { let result2 = evm.transact_commit(tx_env2)?; assert!(result2.is_success()); - assert_eq!(result2.gas_used(), 31_286); + assert_eq!(result2.tx_gas_used(), 31_286); let ctx = &mut evm.ctx; let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx); @@ -1460,10 +1466,10 @@ mod tests { let gas = result.unwrap(); // Verify floor_gas > initial_gas for this calldata (EIP-7623 scenario) assert!( - gas.floor_gas > gas.initial_gas, + gas.floor_gas > gas.initial_total_gas, "Expected floor_gas ({}) > initial_gas ({}) for large calldata", gas.floor_gas, - gas.initial_gas + gas.initial_total_gas ); } @@ -1481,7 +1487,7 @@ mod tests { let gas = result.unwrap(); assert!( - gas.initial_gas >= 21_000, + gas.initial_total_gas >= 21_000, "Initial gas should be at least 21k base" ); } @@ -1556,7 +1562,7 @@ mod tests { assert!(result.is_success()); // With T1 TIP-1000: new account cost (250k) + base intrinsic (21k) + WebAuthn (~3.4k) + calldata - let gas_used = result.gas_used(); + let gas_used = result.tx_gas_used(); assert_eq!( gas_used, 278738, "T1 baseline identity call gas should be exact" @@ -1601,7 +1607,7 @@ mod tests { assert!(result.is_success(), "SSTORE transaction should succeed"); // With TIP-1000: new account (250k) + SSTORE to new slot (250k) + base costs - let gas_used = result.gas_used(); + let gas_used = result.tx_gas_used(); assert_eq!( gas_used, 530863, "T1 SSTORE to new slot gas should be exact" @@ -1655,7 +1661,7 @@ mod tests { // SSTORE to existing non-zero slot (reset) doesn't trigger the 250k new slot cost // But still has new account cost (250k) + cold SLOAD (2100) + warm SSTORE reset (~2900) - let gas_used = result.gas_used(); + let gas_used = result.tx_gas_used(); assert_eq!( gas_used, 283663, "T1 SSTORE to existing slot gas should be exact" @@ -1703,7 +1709,7 @@ mod tests { ); // With TIP-1000: new account (250k) + 2 SSTOREs to new slots (2 * 250k) = 750k + base - let gas_used = result.gas_used(); + let gas_used = result.tx_gas_used(); assert_eq!(gas_used, 783069, "T1 multiple SSTOREs gas should be exact"); Ok(()) @@ -1735,7 +1741,7 @@ mod tests { assert!(result.is_success(), "CREATE transaction should succeed"); // With TIP-1000: CREATE cost (500k) + new account for sender (250k) + base costs - let gas_used = result.gas_used(); + let gas_used = result.tx_gas_used(); assert_eq!(gas_used, 778720, "T1 CREATE contract gas should be exact"); Ok(()) @@ -1784,7 +1790,7 @@ mod tests { // With TIP-1000: CREATE cost (500k) + new account (250k) + 2D nonce sender creation (250k) + base assert_eq!( - result1.gas_used(), + result1.tx_gas_used(), 1028720, "T1 CREATE with 2D nonce (caller.nonce=0) gas should be exact" ); @@ -1812,13 +1818,13 @@ mod tests { // With TIP-1000: CREATE cost (500k) + new account (250k) + base (no extra 250k since caller.nonce != 0) assert_eq!( - result2.gas_used(), + result2.tx_gas_used(), 778720, "T1 CREATE with 2D nonce (caller.nonce=1) gas should be exact" ); // Verify the gas difference is exactly 250,000 (new_account_cost) - let gas_difference = result1.gas_used() - result2.gas_used(); + let gas_difference = result1.tx_gas_used() - result2.tx_gas_used(); assert_eq!( gas_difference, 250_000, "Gas difference should be exactly new_account_cost (250,000), got {gas_difference:?}", @@ -1852,7 +1858,7 @@ mod tests { caller, ))?; assert!(result1.is_success()); - let gas_nonce_zero = result1.gas_used(); + let gas_nonce_zero = result1.tx_gas_used(); // CREATE with caller.nonce == 1 (no extra 250k) let mut evm2 = create_funded_evm_t1_with_timestamp(caller, timestamp); @@ -1875,7 +1881,7 @@ mod tests { caller, ))?; assert!(result2.is_success()); - let gas_nonce_one = result2.gas_used(); + let gas_nonce_one = result2.tx_gas_used(); // The fix adds 250k when caller.nonce == 0 for CREATE with non-zero nonce_key assert_eq!( @@ -1906,7 +1912,7 @@ mod tests { let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller); let result1 = evm1.transact_commit(tx_env1)?; assert!(result1.is_success()); - let gas_single = result1.gas_used(); + let gas_single = result1.tx_gas_used(); // Test 2: Three calls // T1 costs: new account (250k) + 3 calls overhead @@ -1922,7 +1928,7 @@ mod tests { let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller); let result2 = evm2.transact_commit(tx_env2)?; assert!(result2.is_success()); - let gas_triple = result2.gas_used(); + let gas_triple = result2.tx_gas_used(); // Three calls should cost more than single call assert_eq!(gas_single, 278738, "T1 single call gas should be exact"); @@ -1981,7 +1987,7 @@ mod tests { assert!(result.is_success(), "SLOAD transaction should succeed"); // T1 costs: new account (250k) + cold SLOAD (2100) + warm SLOAD (100) + cold account (~2.6k) - let gas_used = result.gas_used(); + let gas_used = result.tx_gas_used(); assert_eq!(gas_used, 280866, "T1 SLOAD cold/warm gas should be exact"); Ok(()) @@ -2132,7 +2138,11 @@ mod tests { // Track whether the nonce was incremented (committed OOG vs validation rejection). let nonce_incremented = match &result_low { Ok(result) => { - assert_eq!(result.gas_used(), 589_000, "Gas used should be gas limit"); + assert_eq!( + result.tx_gas_used(), + 589_000, + "Gas used should be gas limit" + ); assert!( !result.is_success(), "Transaction with insufficient gas should fail" @@ -2426,7 +2436,7 @@ mod tests { let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller); let result = evm.transact_commit(tx_env)?; assert!(result.is_success()); - Ok(result.gas_used()) + Ok(result.tx_gas_used()) } let t1_gas = run_call_with_key_auth(TempoHardfork::T1)?; diff --git a/crates/revm/src/gas_params.rs b/crates/revm/src/gas_params.rs index 558e8d2157..9307a89cf9 100644 --- a/crates/revm/src/gas_params.rs +++ b/crates/revm/src/gas_params.rs @@ -10,6 +10,10 @@ pub trait TempoGasParams { fn tx_tip1000_auth_account_creation_cost(&self) -> u64 { self.gas_params().get(GasId::new(255)) } + + fn tx_tip1000_auth_account_creation_state_gas(&self) -> u64 { + self.gas_params().get(GasId::new(254)) + } } impl TempoGasParams for GasParams { @@ -18,33 +22,172 @@ impl TempoGasParams for GasParams { } } +// TIP-1000 total gas costs (used by both T1 and T3) +const SSTORE_SET_COST: u64 = 250_000; +const CREATE_COST: u64 = 500_000; +const NEW_ACCOUNT_COST: u64 = 250_000; +const CODE_DEPOSIT_COST_T1: u64 = 1_000; +const CODE_DEPOSIT_COST_T3: u64 = 2_500; +const AUTH_ACCOUNT_CREATION_COST: u64 = 250_000; +const EIP7702_PER_EMPTY_ACCOUNT_COST: u64 = 12_500; + +// T3 execution gas (computational overhead only) +const T3_EXEC_GAS: u64 = 5_000; +const T3_CODE_DEPOSIT_EXEC_GAS: u64 = 200; + /// Tempo gas params override. #[inline] pub fn tempo_gas_params(spec: TempoHardfork) -> GasParams { let mut gas_params = GasParams::new_spec(spec.into()); let mut overrides = vec![]; - if spec.is_t1() { + if spec.is_t3() { + // TIP-1016: Split storage creation costs into execution gas + state gas. + // Execution gas (computational overhead) stays in regular params. + // Storage creation gas (permanent storage burden) moves to state gas params. + overrides.extend([ + (GasId::sstore_set_without_load_cost(), T3_EXEC_GAS), + (GasId::sstore_set_state_gas(), SSTORE_SET_COST - T3_EXEC_GAS), + (GasId::tx_create_cost(), T3_EXEC_GAS), + (GasId::create(), T3_EXEC_GAS), + (GasId::create_state_gas(), CREATE_COST - T3_EXEC_GAS), + (GasId::new_account_cost(), T3_EXEC_GAS), + ( + GasId::new_account_state_gas(), + NEW_ACCOUNT_COST - T3_EXEC_GAS, + ), + (GasId::new_account_cost_for_selfdestruct(), T3_EXEC_GAS), + (GasId::code_deposit_cost(), T3_CODE_DEPOSIT_EXEC_GAS), + ( + GasId::code_deposit_state_gas(), + CODE_DEPOSIT_COST_T3 - T3_CODE_DEPOSIT_EXEC_GAS, + ), + ( + GasId::tx_eip7702_per_empty_account_cost(), + EIP7702_PER_EMPTY_ACCOUNT_COST, + ), + (GasId::new(255), T3_EXEC_GAS), + (GasId::new(254), AUTH_ACCOUNT_CREATION_COST - T3_EXEC_GAS), + ]); + } else if spec.is_t1() { + // TIP-1000: All storage creation costs in regular gas (no state gas split). overrides.extend([ - // storage set with SSTORE opcode. - (GasId::sstore_set_without_load_cost(), 250_000), - // Base cost of Create kind transaction. - (GasId::tx_create_cost(), 500_000), - // create cost for CREATE/CREATE2 opcodes. - (GasId::create(), 500_000), - // new account cost for new accounts. - (GasId::new_account_cost(), 250_000), - // Selfdestruct will not be possible to create new account as this can only be - // done when account value is not zero. - (GasId::new_account_cost_for_selfdestruct(), 250_000), - // code deposit cost is 1000 per byte. - (GasId::code_deposit_cost(), 1_000), - // The base cost per authorization is reduced to 12,500 gas - (GasId::tx_eip7702_per_empty_account_cost(), 12500), - // Auth account creation cost. - (GasId::new(255), 250_000), + (GasId::sstore_set_without_load_cost(), SSTORE_SET_COST), + (GasId::tx_create_cost(), CREATE_COST), + (GasId::create(), CREATE_COST), + (GasId::new_account_cost(), NEW_ACCOUNT_COST), + (GasId::new_account_cost_for_selfdestruct(), NEW_ACCOUNT_COST), + (GasId::code_deposit_cost(), CODE_DEPOSIT_COST_T1), + ( + GasId::tx_eip7702_per_empty_account_cost(), + EIP7702_PER_EMPTY_ACCOUNT_COST, + ), + (GasId::new(255), AUTH_ACCOUNT_CREATION_COST), ]); } gas_params.override_gas(overrides); gas_params } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_t1_gas_params_no_state_gas_split() { + let gas_params = tempo_gas_params(TempoHardfork::T1); + + // T1 has full 250k costs in regular gas, no state gas split + assert_eq!( + gas_params.get(GasId::sstore_set_without_load_cost()), + 250_000 + ); + assert_eq!(gas_params.get(GasId::new_account_cost()), 250_000); + assert_eq!(gas_params.get(GasId::tx_create_cost()), 500_000); + assert_eq!(gas_params.get(GasId::create()), 500_000); + assert_eq!(gas_params.get(GasId::code_deposit_cost()), 1_000); + + // State gas params should remain at upstream defaults (not Tempo-bumped) + let upstream = GasParams::new_spec(TempoHardfork::T1.into()); + assert_eq!( + gas_params.get(GasId::sstore_set_state_gas()), + upstream.get(GasId::sstore_set_state_gas()), + "T1 should not override state gas params" + ); + assert_eq!( + gas_params.get(GasId::new_account_state_gas()), + upstream.get(GasId::new_account_state_gas()), + ); + assert_eq!( + gas_params.get(GasId::create_state_gas()), + upstream.get(GasId::create_state_gas()), + ); + } + + #[test] + fn test_t2_gas_params_splits_storage_costs() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + + // T3 execution gas (computational overhead only) + assert_eq!(gas_params.get(GasId::sstore_set_without_load_cost()), 5_000); + assert_eq!(gas_params.get(GasId::new_account_cost()), 5_000); + assert_eq!( + gas_params.get(GasId::new_account_cost_for_selfdestruct()), + 5_000 + ); + assert_eq!(gas_params.get(GasId::tx_create_cost()), 5_000); + assert_eq!(gas_params.get(GasId::create()), 5_000); + assert_eq!(gas_params.get(GasId::code_deposit_cost()), 200); + + // T3 state gas (storage creation burden) + assert_eq!(gas_params.get(GasId::sstore_set_state_gas()), 245_000); + assert_eq!(gas_params.get(GasId::new_account_state_gas()), 245_000); + assert_eq!(gas_params.get(GasId::create_state_gas()), 495_000); + assert_eq!(gas_params.get(GasId::code_deposit_state_gas()), 2_300); + + // Auth account creation also split + assert_eq!(gas_params.get(GasId::new(255)), 5_000); + assert_eq!(gas_params.get(GasId::new(254)), 245_000); + } + + #[test] + fn test_t2_total_gas_matches_t1() { + let t1 = tempo_gas_params(TempoHardfork::T1); + let t2 = tempo_gas_params(TempoHardfork::T3); + + // SSTORE set: exec + state should equal T1 total + assert_eq!( + t2.get(GasId::sstore_set_without_load_cost()) + t2.get(GasId::sstore_set_state_gas()), + t1.get(GasId::sstore_set_without_load_cost()), + "SSTORE set: T2 exec + state must equal T1 total" + ); + + // New account: exec + state should equal T1 total + assert_eq!( + t2.get(GasId::new_account_cost()) + t2.get(GasId::new_account_state_gas()), + t1.get(GasId::new_account_cost()), + "new_account: T2 exec + state must equal T1 total" + ); + + // CREATE: exec + state should equal T1 total + assert_eq!( + t2.get(GasId::create()) + t2.get(GasId::create_state_gas()), + t1.get(GasId::create()), + "CREATE: T2 exec + state must equal T1 total" + ); + + // Code deposit: T2 total is higher (2,500/byte vs 1,000/byte) per TIP-1016 + assert_eq!( + t2.get(GasId::code_deposit_cost()) + t2.get(GasId::code_deposit_state_gas()), + 2_500, + "code_deposit: T2 total should be 2,500/byte per TIP-1016" + ); + + // Auth account creation: exec + state should equal T1 total + assert_eq!( + t2.get(GasId::new(255)) + t2.get(GasId::new(254)), + t1.get(GasId::new(255)), + "auth_account_creation: T2 exec + state must equal T1 total" + ); + } +} diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 99f8172884..7a6a0f55d2 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -176,15 +176,32 @@ fn calculate_key_authorization_gas( /// For pre-T1: Uses `init_and_floor_gas` directly to maintain backward compatibility, /// since pre-T1 doesn't have key_authorization gas tracking and Genesis has special /// handling where nonce_2d_gas is added to init_and_floor_gas but not to evm.initial_gas. +/// +/// `evm_initial_state_gas` captures additional state gas from runtime checks in +/// `validate_against_state_and_deduct_caller` (e.g., 2D nonce + CREATE + caller nonce == 0). #[inline] fn adjusted_initial_gas( spec: tempo_chainspec::hardfork::TempoHardfork, evm_initial_gas: u64, + evm_initial_state_gas: u64, init_and_floor_gas: &InitialAndFloorGas, ) -> InitialAndFloorGas { - if spec.is_t1() { - InitialAndFloorGas::new(evm_initial_gas, init_and_floor_gas.floor_gas) + if spec.is_t1() && init_and_floor_gas.initial_total_gas > 0 { + let state_gas = init_and_floor_gas.initial_state_gas + evm_initial_state_gas; + // evm_initial_gas = init_and_floor_gas.initial_total_gas (possibly + key_auth + // delta). It includes any state gas that revm's initial_tx_gas() folded into + // total, but does NOT include Tempo-specific state gas added afterwards. + // + // We fold Tempo's state gas into initial_total_gas so that revm's subtraction + // (initial_total_gas - initial_state_gas) in execution/first_frame_input + // yields the correct execution-only intrinsic gas. + InitialAndFloorGas::new_with_state_gas( + evm_initial_gas + evm_initial_state_gas + init_and_floor_gas.initial_state_gas, + state_gas, + init_and_floor_gas.floor_gas, + ) } else { + // Pre-T1 or system calls (initial_total_gas == 0): pass through unchanged. *init_and_floor_gas } } @@ -307,10 +324,17 @@ where < as EvmTr>::Frame as FrameTr>::FrameInit, ) -> Result>, { - let gas_limit = evm.ctx().tx().gas_limit() - init_and_floor_gas.initial_gas; + // Deduct only the regular (non-state) portion of initial gas. + // State gas is handled separately by first_frame_input's reservoir logic. + // This matches revm's Handler::execution() which computes: + // regular_initial_gas = initial_total_gas - initial_state_gas + // gas_limit = tx.gas_limit - regular_initial_gas + let regular_initial_gas = + init_and_floor_gas.initial_total_gas - init_and_floor_gas.initial_state_gas; + let gas_limit = evm.ctx().tx().gas_limit() - regular_initial_gas; // Create first frame action - let first_frame_input = self.first_frame_input(evm, gas_limit)?; + let first_frame_input = self.first_frame_input(evm, gas_limit, init_and_floor_gas)?; // Run execution loop (standard or inspector) let mut frame_result = run_loop(self, evm, first_frame_input)?; @@ -365,8 +389,14 @@ where let checkpoint = evm.ctx().journal_mut().checkpoint(); let gas_limit = evm.ctx().tx().gas_limit(); - let mut remaining_gas = gas_limit - init_and_floor_gas.initial_gas; + // Deduct only the regular (non-state) portion of initial gas, matching + // revm's convention: regular_initial_gas = initial_total_gas - initial_state_gas. + // State gas goes to the reservoir and is tracked separately. + let regular_initial_gas = + init_and_floor_gas.initial_total_gas - init_and_floor_gas.initial_state_gas; + let mut remaining_gas = gas_limit - regular_initial_gas; let mut accumulated_gas_refund = 0i64; + let mut accumulated_state_gas_spent = 0u64; // Store original TxEnv values to restore after batch execution let original_kind = evm.ctx().tx().kind(); @@ -433,28 +463,34 @@ where } // Include gas from all previous successful calls + failed call - let gas_spent_by_failed_call = frame_result.gas().spent(); + let gas_spent_by_failed_call = frame_result.gas().total_gas_spent(); let total_gas_spent = (gas_limit - remaining_gas) + gas_spent_by_failed_call; - - // Create new Gas with correct limit, because Gas does not have a set_limit method - // (the frame_result has the limit from just the last call) - let mut corrected_gas = Gas::new(gas_limit); + // State gas only applies to successful calls that create state. + // On revert/halt no new state is created, so the failed call's + // state gas is not charged. + let total_state_gas = accumulated_state_gas_spent; + + // Use flattened gas reconstruction (Gas::new_spent + erase_cost) for robustness + // under the EIP-8037 reservoir model. This avoids ambiguity from Gas::new's + // reservoir initialization. + let mut corrected_gas = Gas::new_spent(gas_limit); if instruction_result.is_revert() { - corrected_gas.set_spent(total_gas_spent); - } else { - corrected_gas.spend_all(); + corrected_gas.erase_cost(gas_limit - total_gas_spent); } corrected_gas.set_refund(0); // No refunds when batch fails and all state is reverted + corrected_gas.set_state_gas_spent(total_state_gas); *frame_result.gas_mut() = corrected_gas; return Ok(frame_result); } - // Call succeeded - accumulate gas usage and refunds - let gas_spent = frame_result.gas().spent(); + // Call succeeded - accumulate gas usage, refunds, and state gas + let gas_spent = frame_result.gas().total_gas_spent(); let gas_refunded = frame_result.gas().refunded(); accumulated_gas_refund = accumulated_gas_refund.saturating_add(gas_refunded); + accumulated_state_gas_spent = + accumulated_state_gas_spent.saturating_add(frame_result.gas().state_gas_spent()); // Subtract only execution gas (intrinsic gas already deducted upfront) remaining_gas = remaining_gas.saturating_sub(gas_spent); @@ -470,11 +506,12 @@ where let total_gas_spent = gas_limit - remaining_gas; - // Create new Gas with correct limit, because Gas does not have a set_limit method - // (the frame_result has the limit from just the last call) - let mut corrected_gas = Gas::new(gas_limit); - corrected_gas.set_spent(total_gas_spent); + // Use flattened gas reconstruction (Gas::new_spent + erase_cost) for robustness + // under the EIP-8037 reservoir model, and preserve accumulated state_gas_spent. + let mut corrected_gas = Gas::new_spent(gas_limit); + corrected_gas.erase_cost(gas_limit - total_gas_spent); corrected_gas.set_refund(accumulated_gas_refund); + corrected_gas.set_state_gas_spent(accumulated_state_gas_spent); *result.gas_mut() = corrected_gas; Ok(result) @@ -553,7 +590,12 @@ where I: Inspector, EthInterpreter>, { let spec = *evm.ctx_ref().cfg().spec(); - let adjusted_gas = adjusted_initial_gas(spec, evm.initial_gas, init_and_floor_gas); + let adjusted_gas = adjusted_initial_gas( + spec, + evm.initial_gas, + evm.initial_state_gas, + init_and_floor_gas, + ); let tx = evm.tx(); @@ -610,7 +652,12 @@ where init_and_floor_gas: &InitialAndFloorGas, ) -> Result { let spec = evm.ctx_ref().cfg().spec(); - let adjusted_gas = adjusted_initial_gas(*spec, evm.initial_gas, init_and_floor_gas); + let adjusted_gas = adjusted_initial_gas( + *spec, + evm.initial_gas, + evm.initial_state_gas, + init_and_floor_gas, + ); let tx = evm.tx(); if let Some(oog) = check_gas_limit(*spec, tx, &adjusted_gas) { @@ -651,7 +698,11 @@ where /// This override extends support to AA transactions (type 0x76) by checking for the presence /// of an aa_authorization_list in the tempo_tx_env. #[inline] - fn apply_eip7702_auth_list(&self, evm: &mut Self::Evm) -> Result { + fn apply_eip7702_auth_list( + &self, + evm: &mut Self::Evm, + init_and_floor_gas: &mut InitialAndFloorGas, + ) -> Result { let ctx = &mut evm.ctx; let spec = ctx.cfg.spec; @@ -680,7 +731,10 @@ where )? } else { // For standard EIP-7702 transactions, use the default implementation - pre_execution::apply_eip7702_auth_list::<_, Self::Error>(evm.ctx())? + pre_execution::apply_eip7702_auth_list::<_, Self::Error>( + evm.ctx_mut(), + init_and_floor_gas, + )? }; // TIP-1000: State Creation Cost Increase @@ -746,6 +800,10 @@ where // This case would create a new account for caller. if !nonce_key.is_zero() && tx.kind().is_create() && caller_account.nonce() == 0 { evm.initial_gas += cfg.gas_params().get(GasId::new_account_cost()); + // TIP-1016: Track state gas for new account creation (T3+ only) + if spec.is_t3() { + evm.initial_state_gas += cfg.gas_params().new_account_state_gas(); + } // do the gas limit check again. if tx.gas_limit() < evm.initial_gas { @@ -1368,14 +1426,22 @@ where // no need for v1 fork check as gas_params would be zero for auth in tx.authorization_list() { if auth.nonce == 0 { - init_gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost(); + init_gas.initial_total_gas += + gas_params.tx_tip1000_auth_account_creation_cost(); + // TIP-1016: Track state gas for auth account creation + init_gas.initial_state_gas += + gas_params.tx_tip1000_auth_account_creation_state_gas(); } } // TIP-1000: Storage pricing updates for launch // Transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas. if spec.is_t1() && tx.nonce == 0 { - init_gas.initial_gas += gas_params.get(GasId::new_account_cost()); + init_gas.initial_total_gas += gas_params.get(GasId::new_account_cost()); + // TIP-1016: Track state gas for new account creation (T3+ only) + if spec.is_t3() { + init_gas.initial_state_gas += gas_params.new_account_state_gas(); + } } if evm.ctx.cfg.is_eip7623_disabled() { @@ -1383,10 +1449,18 @@ where } // Validate gas limit is sufficient for initial gas - if gas_limit < init_gas.initial_gas { + // State gas is only included in the intrinsic check for T3+, since pre-T3 + // transactions were never validated against state gas. + let total_intrinsic = init_gas.initial_total_gas + + if spec.is_t3() { + init_gas.initial_state_gas + } else { + 0 + }; + if gas_limit < total_intrinsic { return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost { gas_limit, - intrinsic_gas: init_gas.initial_gas, + intrinsic_gas: total_intrinsic, } .into()); } @@ -1404,7 +1478,7 @@ where }; // used to calculate key_authorization gas spending limit. - evm.initial_gas = init_gas.initial_gas; + evm.initial_gas = init_gas.initial_total_gas; Ok(init_gas) } @@ -1437,7 +1511,7 @@ where Ok(ExecutionResult::Halt { reason: TempoHaltReason::SubblockTxFeePayment, logs: Default::default(), - gas: ResultGas::default().with_limit(evm.ctx.tx.gas_limit), + gas: ResultGas::default(), }) } else { MainnetHandler::default() @@ -1476,36 +1550,38 @@ pub fn calculate_aa_batch_intrinsic_gas<'a>( let mut gas = InitialAndFloorGas::default(); // 1. Base stipend (21k, once per transaction) - gas.initial_gas += gas_params.tx_base_stipend(); + gas.initial_total_gas += gas_params.tx_base_stipend(); // 2. Signature verification gas - gas.initial_gas += tempo_signature_verification_gas(signature); + gas.initial_total_gas += tempo_signature_verification_gas(signature); let cold_account_cost = gas_params.warm_storage_read_cost() + gas_params.cold_account_additional_cost(); // 3. Per-call overhead: cold account access // if the `to` address has not appeared in the call batch before. - gas.initial_gas += cold_account_cost * calls.len().saturating_sub(1) as u64; + gas.initial_total_gas += cold_account_cost * calls.len().saturating_sub(1) as u64; // 4. Authorization list costs (EIP-7702) - gas.initial_gas += + gas.initial_total_gas += authorization_list.len() as u64 * gas_params.tx_eip7702_per_empty_account_cost(); // Add signature verification costs for each authorization // No need for v1 fork check as gas_params would be zero for auth in authorization_list { - gas.initial_gas += tempo_signature_verification_gas(auth.signature()); + gas.initial_total_gas += tempo_signature_verification_gas(auth.signature()); // TIP-1000: Storage pricing updates for launch // EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas. if auth.nonce == 0 { - gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost(); + gas.initial_total_gas += gas_params.tx_tip1000_auth_account_creation_cost(); + // TIP-1016: Track state gas for auth account creation + gas.initial_state_gas += gas_params.tx_tip1000_auth_account_creation_state_gas(); } } // 5. Key authorization costs (if present) if let Some(key_auth) = key_authorization { - gas.initial_gas += calculate_key_authorization_gas(key_auth, gas_params, spec); + gas.initial_total_gas += calculate_key_authorization_gas(key_auth, gas_params, spec); } // 6. Per-call costs @@ -1519,10 +1595,14 @@ pub fn calculate_aa_batch_intrinsic_gas<'a>( // 4b. CREATE-specific costs if call.to.is_create() { // CREATE costs 500,000 gas in TIP-1000 (T1), 32,000 before - gas.initial_gas += gas_params.create_cost(); + gas.initial_total_gas += gas_params.create_cost(); // EIP-3860: Initcode analysis gas using revm helper - gas.initial_gas += gas_params.tx_initcode_cost(call.input.len()); + gas.initial_total_gas += gas_params.tx_initcode_cost(call.input.len()); + + // TIP-1016: Track predictable state gas for CREATE calls + gas.initial_state_gas += + gas_params.new_account_state_gas() + gas_params.create_state_gas(); } // Note: Transaction value is not allowed in AA transactions as there is no balances in accounts yet. @@ -1534,19 +1614,19 @@ pub fn calculate_aa_batch_intrinsic_gas<'a>( // 4c. Value transfer cost using revm constant // left here for future reference. if !call.value.is_zero() && call.to.is_call() { - gas.initial_gas += gas_params.get(GasId::transfer_value_cost()); // 9000 gas + gas.initial_total_gas += gas_params.get(GasId::transfer_value_cost()); // 9000 gas } } - gas.initial_gas += total_tokens * gas_params.tx_token_cost(); + gas.initial_total_gas += total_tokens * gas_params.tx_token_cost(); // 5. Access list costs using revm constants if let Some(access_list) = access_list { let (accounts, storages) = access_list.fold((0, 0), |(acc_count, storage_count), item| { (acc_count + 1, storage_count + item.storage_slots().count()) }); - gas.initial_gas += accounts * gas_params.tx_access_list_address_cost(); // 2400 per account - gas.initial_gas += storages as u64 * gas_params.tx_access_list_storage_key_cost(); // 1900 per storage + gas.initial_total_gas += accounts * gas_params.tx_access_list_address_cost(); // 2400 per account + gas.initial_total_gas += storages as u64 * gas_params.tx_access_list_storage_key_cost(); // 1900 per storage } // 6. Floor gas using revm helper @@ -1601,15 +1681,19 @@ where // - Expiring nonce (nonce_key == MAX, T1 active): ring buffer + seen mapping operations // - 2D nonce (nonce_key != 0): SLOAD + SSTORE for nonce increment // - Regular nonce (nonce_key == 0): no additional gas - batch_gas.initial_gas += EXPIRING_NONCE_GAS; + batch_gas.initial_total_gas += EXPIRING_NONCE_GAS; } else if tx.nonce == 0 { // TIP-1000: Storage pricing updates for launch // Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas - batch_gas.initial_gas += gas_params.get(GasId::new_account_cost()); + batch_gas.initial_total_gas += gas_params.get(GasId::new_account_cost()); + // TIP-1016: Track state gas for new account creation (T3+ only) + if spec.is_t3() { + batch_gas.initial_state_gas += gas_params.new_account_state_gas(); + } } else if !aa_env.nonce_key.is_zero() { // Existing 2D nonce key usage (nonce > 0) // TIP-1000 Invariant 3: existing state updates must charge +5,000 gas - batch_gas.initial_gas += spec.gas_existing_nonce_key(); + batch_gas.initial_total_gas += spec.gas_existing_nonce_key(); } } else if let Some(aa_env) = &tx.tempo_tx_env && !aa_env.nonce_key.is_zero() @@ -1630,14 +1714,22 @@ where // with gas_limit < intrinsic + nonce_2d_gas to pass validation, but the gas is still // charged during execution via init_and_floor_gas (not evm.initial_gas) if spec.is_t0() { - batch_gas.initial_gas += nonce_2d_gas; + batch_gas.initial_total_gas += nonce_2d_gas; } // Validate gas limit is sufficient for initial gas - if gas_limit < batch_gas.initial_gas { + // State gas is only included in the intrinsic check for T3+, since pre-T3 + // transactions were never validated against state gas. + let total_intrinsic = batch_gas.initial_total_gas + + if spec.is_t3() { + batch_gas.initial_state_gas + } else { + 0 + }; + if gas_limit < total_intrinsic { return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost { gas_limit, - intrinsic_gas: batch_gas.initial_gas, + intrinsic_gas: total_intrinsic, } .into()); } @@ -1645,7 +1737,7 @@ where // For pre-T0 (Genesis), add 2D nonce gas after validation // This gas will be charged via init_and_floor_gas, not evm.initial_gas if !spec.is_t0() { - batch_gas.initial_gas += nonce_2d_gas; + batch_gas.initial_total_gas += nonce_2d_gas; } // Validate floor gas (Prague+) @@ -1709,7 +1801,26 @@ where evm: &mut Self::Evm, init_and_floor_gas: &InitialAndFloorGas, ) -> Result { - self.inspect_execution_with(evm, init_and_floor_gas, Self::inspect_run_exec_loop) + let spec = evm.ctx_ref().cfg().spec(); + let adjusted_gas = adjusted_initial_gas( + *spec, + evm.initial_gas, + evm.initial_state_gas, + init_and_floor_gas, + ); + + let tx = evm.tx(); + + if let Some(oog) = check_gas_limit(*spec, tx, &adjusted_gas) { + return Ok(oog); + } + + if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref() { + let calls = tempo_tx_env.aa_calls.clone(); + self.inspect_execute_multi_call(evm, &adjusted_gas, calls) + } else { + self.inspect_execute_single_call(evm, &adjusted_gas) + } } } @@ -1735,7 +1846,7 @@ fn check_gas_limit( tx: &TempoTxEnv, adjusted_gas: &InitialAndFloorGas, ) -> Option { - if spec.is_t0() && tx.gas_limit() < adjusted_gas.initial_gas { + if spec.is_t0() && tx.gas_limit() < adjusted_gas.initial_total_gas { let kind = *tx .first_call() .expect("we already checked that there is at least one call in aa tx") @@ -1784,7 +1895,10 @@ pub fn validate_time_window( #[cfg(test)] mod tests { use super::*; - use crate::{TempoBlockEnv, TempoTxEnv, evm::TempoEvm, tx::TempoBatchCallEnv}; + use crate::{ + TempoBlockEnv, TempoTxEnv, evm::TempoEvm, gas_params::tempo_gas_params, + tx::TempoBatchCallEnv, + }; use alloy_primitives::{Address, B256, Bytes, TxKind, U256}; use proptest::prelude::*; use revm::{ @@ -1799,7 +1913,7 @@ mod tests { use tempo_contracts::precompiles::DEFAULT_FEE_TOKEN; use tempo_precompiles::{PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS}; use tempo_primitives::transaction::{ - Call, TempoSignature, + Call, RecoveredTempoAuthorization, TempoSignature, TempoSignedAuthorization, tt_signature::{P256SignatureWithPreHash, WebAuthnSignature}, }; @@ -2053,7 +2167,7 @@ mod tests { ); // AA with secp256k1 + single call should match normal tx exactly - assert_eq!(aa_gas.initial_gas, normal_tx_gas.initial_gas); + assert_eq!(aa_gas.initial_total_gas, normal_tx_gas.initial_total_gas); } #[test] @@ -2107,11 +2221,11 @@ mod tests { // For 3 calls: base (21k) + 3*calldata + 2*per-call overhead (calls 2 and 3) // = 21k + 2*(calldata cost) + 2*COLD_ACCOUNT_ACCESS_COST - let expected = base_tx_gas.initial_gas + let expected = base_tx_gas.initial_total_gas + 2 * (calldata.len() as u64 * 16) + 2 * COLD_ACCOUNT_ACCESS_COST; // Should charge per-call overhead for calls beyond the first - assert_eq!(gas.initial_gas, expected,); + assert_eq!(gas.initial_total_gas, expected,); } #[test] @@ -2160,8 +2274,8 @@ mod tests { let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0); // Expected: normal tx + P256_VERIFY_GAS - let expected = base_gas.initial_gas + P256_VERIFY_GAS; - assert_eq!(gas.initial_gas, expected,); + let expected = base_gas.initial_total_gas + P256_VERIFY_GAS; + assert_eq!(gas.initial_total_gas, expected,); } #[test] @@ -2205,7 +2319,7 @@ mod tests { ); // AA CREATE should match normal CREATE exactly - assert_eq!(gas.initial_gas, base_gas.initial_gas,); + assert_eq!(gas.initial_total_gas, base_gas.initial_total_gas,); } #[test] @@ -2284,7 +2398,7 @@ mod tests { let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0); // Expected: normal tx - assert_eq!(gas.initial_gas, base_gas.initial_gas,); + assert_eq!(gas.initial_total_gas, base_gas.initial_total_gas,); } #[test] @@ -2606,7 +2720,7 @@ mod tests { let expected_key_auth_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS; assert_eq!( - gas_with_key_auth.initial_gas - gas_without_key_auth.initial_gas, + gas_with_key_auth.initial_total_gas - gas_without_key_auth.initial_total_gas, expected_key_auth_gas, "Key authorization should add exactly {expected_key_auth_gas} gas to batch", ); @@ -2614,15 +2728,15 @@ mod tests { // Also verify absolute values let spec = tempo_chainspec::hardfork::TempoHardfork::default(); let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0); - let expected_without = base_tx_gas.initial_gas; // no cold access for single call + let expected_without = base_tx_gas.initial_total_gas; // no cold access for single call let expected_with = expected_without + expected_key_auth_gas; assert_eq!( - gas_without_key_auth.initial_gas, expected_without, + gas_without_key_auth.initial_total_gas, expected_without, "Gas without key auth should match expected" ); assert_eq!( - gas_with_key_auth.initial_gas, expected_with, + gas_with_key_auth.initial_total_gas, expected_with, "Gas with key auth should match expected" ); } @@ -2681,7 +2795,7 @@ mod tests { let mut evm = make_evm(5, U256::ZERO); let gas = handler.validate_initial_tx_gas(&mut evm).unwrap(); assert_eq!( - gas.initial_gas, BASE_INTRINSIC_GAS, + gas.initial_total_gas, BASE_INTRINSIC_GAS, "{spec:?}: protocol nonce (nonce_key=0, nonce>0) should have no extra gas" ); } @@ -2698,7 +2812,7 @@ mod tests { let mut evm = make_evm(0, U256::from(42)); let gas = handler.validate_initial_tx_gas(&mut evm).unwrap(); assert_eq!( - gas.initial_gas, expected, + gas.initial_total_gas, expected, "{spec:?}: nonce_key!=0, nonce==0 gas mismatch" ); } @@ -2708,7 +2822,7 @@ mod tests { let mut evm = make_evm(5, U256::from(42)); let gas = handler.validate_initial_tx_gas(&mut evm).unwrap(); assert_eq!( - gas.initial_gas, + gas.initial_total_gas, BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), "{spec:?}: existing 2D nonce key gas mismatch" ); @@ -2737,13 +2851,22 @@ mod tests { } else { spec.gas_new_nonce_key() }; + // TIP-1016: For T2+, state gas must also fit within gas_limit + let nonce_zero_state_gas = if spec.is_t3() { + gas_params.new_account_state_gas() + } else { + 0 + }; + let nonce_zero_total = nonce_zero_gas + nonce_zero_state_gas; let cases = if spec.is_t0() { - vec![ - (BASE_INTRINSIC_GAS + 10_000, 0u64, false), // Insufficient for nonce==0 - (BASE_INTRINSIC_GAS + nonce_zero_gas, 0, true), // Exactly sufficient for nonce==0 + let mut cases = vec![ + (BASE_INTRINSIC_GAS + nonce_zero_total, 0, true), // Exactly sufficient for nonce==0 (exec + state) (BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), // Exactly sufficient for existing key - ] + ]; + // Insufficient: below total required for nonce==0 + cases.push((BASE_INTRINSIC_GAS + nonce_zero_total - 1, 0u64, false)); + cases } else { // Genesis: nonce gas is added AFTER validation, so lower gas_limit still passes vec![ @@ -2885,7 +3008,7 @@ mod tests { let final_gas = result.gas(); assert_eq!( - final_gas.spent(), + final_gas.total_gas_spent(), INTRINSIC_GAS + SPENT.0 + SPENT.1, "Total spent should be intrinsic_gas + sum of all calls' spent values" ); @@ -3047,13 +3170,13 @@ mod tests { let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len2]))); if calldata_len1 <= calldata_len2 { - prop_assert!(gas1.initial_gas <= gas2.initial_gas, + prop_assert!(gas1.initial_total_gas <= gas2.initial_total_gas, "More calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}", - calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas); + calldata_len1, gas1.initial_total_gas, calldata_len2, gas2.initial_total_gas); } else { - prop_assert!(gas1.initial_gas >= gas2.initial_gas, + prop_assert!(gas1.initial_total_gas >= gas2.initial_total_gas, "Less calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}", - calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas); + calldata_len1, gas1.initial_total_gas, calldata_len2, gas2.initial_total_gas); } } @@ -3068,13 +3191,13 @@ mod tests { let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len2]))); if calldata_len1 <= calldata_len2 { - prop_assert!(gas1.initial_gas <= gas2.initial_gas, + prop_assert!(gas1.initial_total_gas <= gas2.initial_total_gas, "More zero-byte calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}", - calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas); + calldata_len1, gas1.initial_total_gas, calldata_len2, gas2.initial_total_gas); } else { - prop_assert!(gas1.initial_gas >= gas2.initial_gas, + prop_assert!(gas1.initial_total_gas >= gas2.initial_total_gas, "Less zero-byte calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}", - calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas); + calldata_len1, gas1.initial_total_gas, calldata_len2, gas2.initial_total_gas); } } @@ -3085,9 +3208,9 @@ mod tests { let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len]))); let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len]))); - prop_assert!(zero_gas.initial_gas < nonzero_gas.initial_gas, + prop_assert!(zero_gas.initial_total_gas < nonzero_gas.initial_total_gas, "Zero-byte calldata should cost less: len={}, zero_gas={}, nonzero_gas={}", - calldata_len, zero_gas.initial_gas, nonzero_gas.initial_gas); + calldata_len, zero_gas.initial_total_gas, nonzero_gas.initial_total_gas); } /// Property: mixed calldata gas is bounded by all-zero and all-nonzero extremes. @@ -3106,12 +3229,12 @@ mod tests { let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len]))); let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len]))); - prop_assert!(mixed_gas.initial_gas >= zero_gas.initial_gas, + prop_assert!(mixed_gas.initial_total_gas >= zero_gas.initial_total_gas, "Mixed calldata gas should be >= all-zero gas: mixed={}, zero={}", - mixed_gas.initial_gas, zero_gas.initial_gas); - prop_assert!(mixed_gas.initial_gas <= nonzero_gas.initial_gas, + mixed_gas.initial_total_gas, zero_gas.initial_total_gas); + prop_assert!(mixed_gas.initial_total_gas <= nonzero_gas.initial_total_gas, "Mixed calldata gas should be <= all-nonzero gas: mixed={}, nonzero={}", - mixed_gas.initial_gas, nonzero_gas.initial_gas); + mixed_gas.initial_total_gas, nonzero_gas.initial_total_gas); } /// Property: gas calculation monotonicity - more calls means more gas @@ -3124,13 +3247,13 @@ mod tests { let gas2 = compute_aa_gas(&make_multi_call_env(num_calls2)); if num_calls1 <= num_calls2 { - prop_assert!(gas1.initial_gas <= gas2.initial_gas, + prop_assert!(gas1.initial_total_gas <= gas2.initial_total_gas, "More calls should mean more gas: calls1={}, gas1={}, calls2={}, gas2={}", - num_calls1, gas1.initial_gas, num_calls2, gas2.initial_gas); + num_calls1, gas1.initial_total_gas, num_calls2, gas2.initial_total_gas); } else { - prop_assert!(gas1.initial_gas >= gas2.initial_gas, + prop_assert!(gas1.initial_total_gas >= gas2.initial_total_gas, "Fewer calls should mean less gas: calls1={}, gas1={}, calls2={}, gas2={}", - num_calls1, gas1.initial_gas, num_calls2, gas2.initial_gas); + num_calls1, gas1.initial_total_gas, num_calls2, gas2.initial_total_gas); } } @@ -3149,9 +3272,9 @@ mod tests { // Expected exactly: 21k base + cold account access for each additional call let expected = 21_000 + COLD_ACCOUNT_ACCESS_COST * (num_calls.saturating_sub(1) as u64); - prop_assert_eq!(gas.initial_gas, expected, + prop_assert_eq!(gas.initial_total_gas, expected, "Gas {} should equal expected {} for {} calls (21k + {}*COLD_ACCOUNT_ACCESS_COST)", - gas.initial_gas, expected, num_calls, num_calls.saturating_sub(1)); + gas.initial_total_gas, expected, num_calls, num_calls.saturating_sub(1)); } /// Property: first_call returns the first call for AA transactions with any number of calls @@ -3415,7 +3538,7 @@ mod tests { // Delta-based assertion: the difference should be new_account_cost - EXISTING_NONCE_KEY_GAS // nonce=0 charges 250k (new account), nonce>0 charges 5k (existing key update) - let gas_delta = gas_nonce_zero.initial_gas - gas_nonce_five.initial_gas; + let gas_delta = gas_nonce_zero.initial_total_gas - gas_nonce_five.initial_total_gas; let expected_delta = new_account_cost - EXISTING_NONCE_KEY_GAS; assert_eq!( gas_delta, expected_delta, @@ -3435,7 +3558,7 @@ mod tests { .unwrap(); assert_eq!( - gas_nonce_zero.initial_gas, gas_regular.initial_gas, + gas_nonce_zero.initial_total_gas, gas_regular.initial_total_gas, "nonce=0 should charge the same regardless of nonce_key (2D vs regular)" ); } @@ -3501,7 +3624,7 @@ mod tests { .unwrap(); assert_eq!( - gas_existing.initial_gas, + gas_existing.initial_total_gas, BASE_INTRINSIC_GAS + EXISTING_NONCE_KEY_GAS, "T1 existing 2D nonce key (nonce>0) should charge BASE + EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})" ); @@ -3511,15 +3634,745 @@ mod tests { let gas_regular = handler.validate_initial_tx_gas(&mut evm_regular).unwrap(); assert_eq!( - gas_regular.initial_gas, BASE_INTRINSIC_GAS, + gas_regular.initial_total_gas, BASE_INTRINSIC_GAS, "T1 regular nonce (nonce_key=0, nonce>0) should only charge BASE intrinsic gas" ); // Verify the delta between 2D and regular nonce is exactly EXISTING_NONCE_KEY_GAS - let gas_delta = gas_existing.initial_gas - gas_regular.initial_gas; + let gas_delta = gas_existing.initial_total_gas - gas_regular.initial_total_gas; assert_eq!( gas_delta, EXISTING_NONCE_KEY_GAS, "Difference between existing 2D nonce and regular nonce should be EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})" ); } + + /// TIP-1016: Standard CREATE tx should populate initial_state_gas with + /// create_state_gas when state gas is enabled (T3+). + /// Note: new_account_state_gas for the caller (nonce==0 with 2D nonce) is added + /// later in validate_against_state_and_deduct_caller, not in upstream initial_tx_gas. + #[test] + fn test_state_gas_standard_create_tx_populates_initial_state_gas() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + let initcode = Bytes::from(vec![0x60, 0x80]); + + let init_gas = gas_params.initial_tx_gas( + &initcode, true, // is_create + 0, 0, 0, + ); + + let expected_state_gas = gas_params.create_state_gas(); + + assert!( + expected_state_gas > 0, + "State gas constants should be non-zero" + ); + assert_eq!( + init_gas.initial_state_gas, + expected_state_gas, + "CREATE tx should have initial_state_gas = create_state_gas ({})", + gas_params.create_state_gas() + ); + } + + /// TIP-1016: Standard CALL tx should have zero initial_state_gas. + #[test] + fn test_state_gas_standard_call_tx_zero_initial_state_gas() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + let calldata = Bytes::from(vec![1, 2, 3]); + + let init_gas = gas_params.initial_tx_gas( + &calldata, false, // not create + 0, 0, 0, + ); + + assert_eq!( + init_gas.initial_state_gas, 0, + "CALL tx should have zero initial_state_gas" + ); + } + + /// TIP-1016: AA CREATE tx should populate initial_state_gas. + #[test] + fn test_state_gas_aa_create_tx_populates_initial_state_gas() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + let initcode = Bytes::from(vec![0x60, 0x80]); + + let call = Call { + to: TxKind::Create, + value: U256::ZERO, + input: initcode, + }; + + let aa_env = TempoBatchCallEnv { + signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + aa_calls: vec![call], + key_authorization: None, + signature_hash: B256::ZERO, + ..Default::default() + }; + + let gas = calculate_aa_batch_intrinsic_gas( + &aa_env, + &gas_params, + None::>, + TempoHardfork::T3, + ) + .unwrap(); + + let expected_state_gas = gas_params.new_account_state_gas() + gas_params.create_state_gas(); + + assert_eq!( + gas.initial_state_gas, expected_state_gas, + "AA CREATE tx should have initial_state_gas = new_account_state_gas + create_state_gas" + ); + } + + /// TIP-1016: AA CALL tx should have zero initial_state_gas. + #[test] + fn test_state_gas_aa_call_tx_zero_initial_state_gas() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + let calldata = Bytes::from(vec![1, 2, 3]); + + let call = Call { + to: TxKind::Call(Address::random()), + value: U256::ZERO, + input: calldata, + }; + + let aa_env = TempoBatchCallEnv { + signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + aa_calls: vec![call], + key_authorization: None, + signature_hash: B256::ZERO, + ..Default::default() + }; + + let gas = calculate_aa_batch_intrinsic_gas( + &aa_env, + &gas_params, + None::>, + TempoHardfork::T3, + ) + .unwrap(); + + assert_eq!( + gas.initial_state_gas, 0, + "AA CALL tx should have zero initial_state_gas" + ); + } + + /// TIP-1016: validate_initial_tx_gas for standard CREATE tx should set + /// initial_state_gas when T2 is active and state gas is enabled. + #[test] + fn test_state_gas_validate_initial_tx_gas_create_t2() { + let mut cfg = CfgEnv::::default(); + cfg.spec = TempoHardfork::T3; + cfg.gas_params = tempo_gas_params(TempoHardfork::T3); + cfg.enable_amsterdam_eip8037 = true; + + let initcode = Bytes::from(vec![0x60, 0x80]); + + let journal = Journal::new(CacheDB::new(EmptyDB::default())); + let tx_env = TempoTxEnv { + inner: revm::context::TxEnv { + gas_limit: 60_000_000, // Above cap to test cap bypass + kind: TxKind::Create, + data: initcode, + ..Default::default() + }, + ..Default::default() + }; + + let ctx = Context::mainnet() + .with_db(CacheDB::new(EmptyDB::default())) + .with_block(TempoBlockEnv::default()) + .with_cfg(cfg.clone()) + .with_tx(tx_env) + .with_new_journal(journal); + let mut evm = TempoEvm::<_, ()>::new(ctx, ()); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::new(); + + let init_gas = handler.validate_initial_tx_gas(&mut evm).unwrap(); + + // create_state_gas (from upstream initial_tx_gas for CREATE) + + // new_account_state_gas (from Tempo's nonce==0 check for the caller) + let expected_state_gas = + cfg.gas_params.create_state_gas() + cfg.gas_params.new_account_state_gas(); + + assert_eq!( + init_gas.initial_state_gas, expected_state_gas, + "T3 CREATE tx with nonce==0 should have create_state_gas + new_account_state_gas" + ); + } + + /// TIP-1016: When enable_amsterdam_eip8037 is true, tx gas limit can exceed the cap + /// (upstream revm validation skips the cap check). + #[test] + fn test_state_gas_tx_gas_limit_above_cap_allowed() { + let mut cfg = CfgEnv::::default(); + cfg.spec = TempoHardfork::T3; + cfg.gas_params = tempo_gas_params(TempoHardfork::T3); + cfg.enable_amsterdam_eip8037 = true; + cfg.tx_gas_limit_cap = Some(30_000_000); + + let calldata = Bytes::from(vec![1, 2, 3]); + + let journal = Journal::new(CacheDB::new(EmptyDB::default())); + let tx_env = TempoTxEnv { + inner: revm::context::TxEnv { + gas_limit: 60_000_000, // Double the cap + kind: TxKind::Call(Address::random()), + data: calldata, + ..Default::default() + }, + ..Default::default() + }; + + let ctx = Context::mainnet() + .with_db(CacheDB::new(EmptyDB::default())) + .with_block(TempoBlockEnv::default()) + .with_cfg(cfg) + .with_tx(tx_env) + .with_new_journal(journal); + let mut evm = TempoEvm::<_, ()>::new(ctx, ()); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::new(); + + // validate_env should pass even though gas_limit > cap + let result = handler.validate_env(&mut evm); + assert!( + result.is_ok(), + "With enable_amsterdam_eip8037=true, tx gas limit above cap should be allowed, got: {:?}", + result.err() + ); + } + + /// TIP-1016: When enable_amsterdam_eip8037 is false (pre-T3), tx gas limit above cap is rejected. + #[test] + fn test_state_gas_tx_gas_limit_above_cap_rejected_pre_t2() { + let mut cfg = CfgEnv::::default(); + cfg.spec = TempoHardfork::T1; + cfg.gas_params = tempo_gas_params(TempoHardfork::T1); + cfg.enable_amsterdam_eip8037 = false; + cfg.tx_gas_limit_cap = Some(30_000_000); + + let calldata = Bytes::from(vec![1, 2, 3]); + + let journal = Journal::new(CacheDB::new(EmptyDB::default())); + let tx_env = TempoTxEnv { + inner: revm::context::TxEnv { + gas_limit: 60_000_000, // Double the cap + kind: TxKind::Call(Address::random()), + data: calldata, + ..Default::default() + }, + ..Default::default() + }; + + let ctx = Context::mainnet() + .with_db(CacheDB::new(EmptyDB::default())) + .with_block(TempoBlockEnv::default()) + .with_cfg(cfg) + .with_tx(tx_env) + .with_new_journal(journal); + let mut evm = TempoEvm::<_, ()>::new(ctx, ()); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::new(); + + // validate_env should reject: gas_limit > cap with state gas disabled + let result = handler.validate_env(&mut evm); + assert!( + result.is_err(), + "With enable_amsterdam_eip8037=false, tx gas limit above cap should be rejected" + ); + } + + /// TIP-1016: Pre-T2 behavior unchanged - initial_state_gas is still populated + /// by upstream revm for CREATE txs (it's a property of gas_params, not gating). + /// But enable_amsterdam_eip8037=false means the reservoir won't be used. + #[test] + fn test_state_gas_backward_compat_t1_no_state_gas_enabled() { + let mut cfg = CfgEnv::::default(); + cfg.spec = TempoHardfork::T1; + cfg.gas_params = tempo_gas_params(TempoHardfork::T1); + + assert!( + !cfg.enable_amsterdam_eip8037, + "Pre-T2 should NOT have enable_amsterdam_eip8037" + ); + + let calldata = Bytes::from(vec![1, 2, 3]); + + let journal = Journal::new(CacheDB::new(EmptyDB::default())); + let tx_env = TempoTxEnv { + inner: revm::context::TxEnv { + gas_limit: 1_000_000, + kind: TxKind::Call(Address::random()), + data: calldata, + ..Default::default() + }, + ..Default::default() + }; + + let ctx = Context::mainnet() + .with_db(CacheDB::new(EmptyDB::default())) + .with_block(TempoBlockEnv::default()) + .with_cfg(cfg) + .with_tx(tx_env) + .with_new_journal(journal); + let mut evm = TempoEvm::<_, ()>::new(ctx, ()); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::new(); + + let init_gas = handler.validate_initial_tx_gas(&mut evm).unwrap(); + + // CALL tx - no state gas in either case + assert_eq!(init_gas.initial_state_gas, 0); + } + + /// TIP-1016: AA batch with multiple calls including CREATE should track + /// state gas for the CREATE call only. + #[test] + fn test_state_gas_aa_mixed_batch_create_and_call() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + let calldata = Bytes::from(vec![1, 2, 3]); + let initcode = Bytes::from(vec![0x60, 0x80]); + + let calls = vec![ + Call { + to: TxKind::Call(Address::random()), + value: U256::ZERO, + input: calldata, + }, + Call { + to: TxKind::Create, + value: U256::ZERO, + input: initcode, + }, + ]; + + let aa_env = TempoBatchCallEnv { + signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + aa_calls: calls, + key_authorization: None, + signature_hash: B256::ZERO, + ..Default::default() + }; + + let gas = calculate_aa_batch_intrinsic_gas( + &aa_env, + &gas_params, + None::>, + TempoHardfork::T3, + ) + .unwrap(); + + // Only the CREATE call contributes state gas + let expected_state_gas = gas_params.new_account_state_gas() + gas_params.create_state_gas(); + + assert_eq!( + gas.initial_state_gas, expected_state_gas, + "Mixed batch should have state gas only from CREATE call" + ); + } + + /// TIP-1016: adjusted_initial_gas must fold state gas into initial_total_gas. + /// + /// Tempo uses an additive convention (initial_total_gas does not include Tempo's + /// state gas), but revm expects initial_total_gas >= initial_state_gas. This function + /// bridges the two by adding init_and_floor_gas.initial_state_gas to initial_total_gas. + #[test] + fn test_state_gas_adjusted_initial_gas_preserves_state_gas() { + // init simulates Tempo output: total=100k (exec + revm-folded state), state=57k + let init = InitialAndFloorGas::new_with_state_gas(100_000, 57_000, 21_000); + + // T3: no runtime state gas delta + // initial_total_gas = evm_initial_gas + init.initial_state_gas = 100k + 57k + let adjusted = adjusted_initial_gas(TempoHardfork::T3, 100_000, 0, &init); + assert_eq!( + adjusted.initial_state_gas, 57_000, + "adjusted_initial_gas must preserve initial_state_gas for T3" + ); + assert_eq!( + adjusted.initial_total_gas, + 100_000 + 57_000, + "initial_total_gas must fold state gas for revm convention" + ); + + // T1: same behavior + let adjusted_t1 = adjusted_initial_gas(TempoHardfork::T1, 100_000, 0, &init); + assert_eq!( + adjusted_t1.initial_state_gas, 57_000, + "adjusted_initial_gas must preserve initial_state_gas for T1" + ); + assert_eq!( + adjusted_t1.initial_total_gas, + 100_000 + 57_000, + "initial_total_gas must fold state gas for revm convention" + ); + + // T3: with runtime state gas delta + // initial_total_gas = evm_initial_gas + evm_initial_state_gas + init.initial_state_gas + let adjusted_with_extra = adjusted_initial_gas(TempoHardfork::T3, 100_000, 245_000, &init); + assert_eq!( + adjusted_with_extra.initial_state_gas, + 57_000 + 245_000, + "adjusted_initial_gas must add evm_initial_state_gas for T3" + ); + assert_eq!( + adjusted_with_extra.initial_total_gas, + 100_000 + 245_000 + 57_000, + "initial_total_gas folds both runtime state gas and init state gas" + ); + } + + /// TIP-1016: AA batch with multiple CREATE calls accumulates state gas. + #[test] + fn test_state_gas_aa_multiple_create_calls() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + let initcode = Bytes::from(vec![0x60, 0x80]); + + let calls = vec![ + Call { + to: TxKind::Create, + value: U256::ZERO, + input: initcode.clone(), + }, + Call { + to: TxKind::Create, + value: U256::ZERO, + input: initcode, + }, + ]; + + let aa_env = TempoBatchCallEnv { + signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + aa_calls: calls, + key_authorization: None, + signature_hash: B256::ZERO, + ..Default::default() + }; + + let gas = calculate_aa_batch_intrinsic_gas( + &aa_env, + &gas_params, + None::>, + TempoHardfork::T3, + ) + .unwrap(); + + // Two CREATE calls should accumulate state gas + let per_create_state_gas = + gas_params.new_account_state_gas() + gas_params.create_state_gas(); + + assert_eq!( + gas.initial_state_gas, + per_create_state_gas * 2, + "Multiple CREATE calls should accumulate initial_state_gas" + ); + } + + /// TIP-1016: In multi-call execution, per-call init gas uses + /// `InitialAndFloorGas::new(0, 0)` so state gas is only deducted once + /// upfront via `calculate_aa_batch_intrinsic_gas`, not per call. + #[test] + fn test_state_gas_multi_call_per_call_init_has_zero_state_gas() { + let zero_init_gas = InitialAndFloorGas::new(0, 0); + assert_eq!( + zero_init_gas.initial_state_gas, 0, + "Per-call init gas in multi-call must have zero initial_state_gas; \ + state gas is deducted once upfront, not per call" + ); + } + + /// TIP-1016: Multi-call corrected gas (success path) must use flattened + /// reconstruction (Gas::new_spent + erase_cost) to be robust under the + /// EIP-8037 reservoir model, and must preserve accumulated state_gas_spent. + #[test] + fn test_state_gas_multi_call_corrected_gas_success_preserves_state_gas() { + let gas_limit: u64 = 1_000_000; + let total_gas_spent: u64 = 400_000; + let accumulated_state_gas: u64 = 150_000; + let accumulated_refund: i64 = 5_000; + + // Simulate flattened gas reconstruction (same pattern as execute_multi_call_with) + let mut corrected_gas = Gas::new_spent(gas_limit); + corrected_gas.erase_cost(gas_limit - total_gas_spent); + corrected_gas.set_refund(accumulated_refund); + corrected_gas.set_state_gas_spent(accumulated_state_gas); + + assert_eq!( + corrected_gas.total_gas_spent(), + total_gas_spent, + "Flattened gas must have correct spent" + ); + assert_eq!( + corrected_gas.used(), + total_gas_spent - accumulated_refund as u64, + "Flattened gas must have correct used (spent - refunded)" + ); + assert_eq!( + corrected_gas.state_gas_spent(), + accumulated_state_gas, + "Corrected gas must preserve accumulated state_gas_spent" + ); + assert_eq!( + corrected_gas.reservoir(), + 0, + "Flattened gas must have zero reservoir" + ); + } + + /// TIP-1016: Multi-call corrected gas (failure path) must preserve + /// state_gas_spent from all calls up to and including the failed one. + #[test] + fn test_state_gas_multi_call_corrected_gas_failure_preserves_state_gas() { + let gas_limit: u64 = 1_000_000; + let total_gas_spent: u64 = 600_000; + let accumulated_state_gas: u64 = 200_000; + + // Simulate flattened gas reconstruction on failure (revert case) + let mut corrected_gas = Gas::new_spent(gas_limit); + corrected_gas.erase_cost(gas_limit - total_gas_spent); + corrected_gas.set_refund(0); // No refunds on batch failure + corrected_gas.set_state_gas_spent(accumulated_state_gas); + + assert_eq!( + corrected_gas.total_gas_spent(), + total_gas_spent, + "Failure path: flattened gas must have correct spent" + ); + assert_eq!( + corrected_gas.state_gas_spent(), + accumulated_state_gas, + "Failure path: corrected gas must preserve state_gas_spent" + ); + } + + /// TIP-1016: AA auth list entries with nonce==0 should track state gas. + #[test] + fn test_state_gas_aa_auth_list_nonce_zero() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + + let aa_env = TempoBatchCallEnv { + signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + aa_calls: vec![Call { + to: TxKind::Call(Address::random()), + value: U256::ZERO, + input: Bytes::from(vec![1, 2, 3]), + }], + tempo_authorization_list: vec![RecoveredTempoAuthorization::new( + TempoSignedAuthorization::new_unchecked( + alloy_eips::eip7702::Authorization { + chain_id: U256::from(1), + address: Address::random(), + nonce: 0, + }, + TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + ), + )], + ..Default::default() + }; + + let gas = calculate_aa_batch_intrinsic_gas( + &aa_env, + &gas_params, + None::>, + TempoHardfork::T3, + ) + .unwrap(); + + assert_eq!( + gas.initial_state_gas, + gas_params.tx_tip1000_auth_account_creation_state_gas(), + "Auth list entry with nonce==0 should track state gas" + ); + } + + /// TIP-1016: AA nonce==0 new account should track state gas in T3. + #[test] + fn test_state_gas_aa_nonce_zero_new_account() { + let gas_params = tempo_gas_params(TempoHardfork::T3); + + let aa_env = TempoBatchCallEnv { + signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + aa_calls: vec![Call { + to: TxKind::Call(Address::random()), + value: U256::ZERO, + input: Bytes::from(vec![1, 2, 3]), + }], + nonce_key: U256::from(1), + ..Default::default() + }; + + let tx_env = TempoTxEnv { + inner: revm::context::TxEnv { + gas_limit: 60_000_000, + nonce: 0, + ..Default::default() + }, + tempo_tx_env: Some(Box::new(aa_env)), + ..Default::default() + }; + + let mut cfg = CfgEnv::::default(); + cfg.spec = TempoHardfork::T3; + cfg.gas_params = gas_params.clone(); + cfg.enable_amsterdam_eip8037 = true; + + let journal = Journal::new(CacheDB::new(EmptyDB::default())); + let ctx = Context::mainnet() + .with_db(CacheDB::new(EmptyDB::default())) + .with_block(TempoBlockEnv::default()) + .with_cfg(cfg) + .with_tx(tx_env) + .with_new_journal(journal); + let mut evm = TempoEvm::<_, ()>::new(ctx, ()); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::new(); + + let init_gas = handler.validate_initial_tx_gas(&mut evm).unwrap(); + + assert_eq!( + init_gas.initial_state_gas, + gas_params.new_account_state_gas(), + "AA tx with nonce==0 should track new_account_state_gas in T3" + ); + } + + /// TIP-1016: Auth list state gas (GasId 254) must be zero on T1. + #[test] + fn test_state_gas_auth_list_zero_on_t1() { + let gas_params = tempo_gas_params(TempoHardfork::T1); + assert_eq!( + gas_params.tx_tip1000_auth_account_creation_state_gas(), + 0, + "Auth account creation state gas must be zero on T1" + ); + + let aa_env = TempoBatchCallEnv { + signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + aa_calls: vec![Call { + to: TxKind::Call(Address::random()), + value: U256::ZERO, + input: Bytes::from(vec![1, 2, 3]), + }], + tempo_authorization_list: vec![RecoveredTempoAuthorization::new( + TempoSignedAuthorization::new_unchecked( + alloy_eips::eip7702::Authorization { + chain_id: U256::from(1), + address: Address::random(), + nonce: 0, + }, + TempoSignature::Primitive(PrimitiveSignature::Secp256k1( + alloy_primitives::Signature::test_signature(), + )), + ), + )], + ..Default::default() + }; + + let gas = calculate_aa_batch_intrinsic_gas( + &aa_env, + &gas_params, + None::>, + TempoHardfork::T1, + ) + .unwrap(); + + assert_eq!( + gas.initial_state_gas, 0, + "T1 auth list nonce==0 should have zero initial_state_gas" + ); + } + + /// TIP-1016: Standard tx with nonce==0 should track state gas on T3 only. + #[test] + fn test_state_gas_standard_tx_nonce_zero_t2() { + let mut cfg = CfgEnv::::default(); + cfg.spec = TempoHardfork::T3; + cfg.gas_params = tempo_gas_params(TempoHardfork::T3); + cfg.enable_amsterdam_eip8037 = true; + + let calldata = Bytes::from(vec![1, 2, 3]); + + let journal = Journal::new(CacheDB::new(EmptyDB::default())); + let tx_env = TempoTxEnv { + inner: revm::context::TxEnv { + gas_limit: 60_000_000, + kind: TxKind::Call(Address::random()), + nonce: 0, + data: calldata, + ..Default::default() + }, + ..Default::default() + }; + + let ctx = Context::mainnet() + .with_db(CacheDB::new(EmptyDB::default())) + .with_block(TempoBlockEnv::default()) + .with_cfg(cfg.clone()) + .with_tx(tx_env) + .with_new_journal(journal); + let mut evm = TempoEvm::<_, ()>::new(ctx, ()); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::new(); + + let init_gas = handler.validate_initial_tx_gas(&mut evm).unwrap(); + + assert_eq!( + init_gas.initial_state_gas, + cfg.gas_params.new_account_state_gas(), + "T3 standard tx with nonce==0 should track new_account_state_gas" + ); + } + + /// TIP-1016: Standard tx with nonce==0 should NOT track state gas on T1. + #[test] + fn test_state_gas_standard_tx_nonce_zero_t1_no_state_gas() { + let mut cfg = CfgEnv::::default(); + cfg.spec = TempoHardfork::T1; + cfg.gas_params = tempo_gas_params(TempoHardfork::T1); + + let calldata = Bytes::from(vec![1, 2, 3]); + + let journal = Journal::new(CacheDB::new(EmptyDB::default())); + let tx_env = TempoTxEnv { + inner: revm::context::TxEnv { + gas_limit: 60_000_000, + kind: TxKind::Call(Address::random()), + nonce: 0, + data: calldata, + ..Default::default() + }, + ..Default::default() + }; + + let ctx = Context::mainnet() + .with_db(CacheDB::new(EmptyDB::default())) + .with_block(TempoBlockEnv::default()) + .with_cfg(cfg) + .with_tx(tx_env) + .with_new_journal(journal); + let mut evm = TempoEvm::<_, ()>::new(ctx, ()); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::new(); + + let init_gas = handler.validate_initial_tx_gas(&mut evm).unwrap(); + + assert_eq!( + init_gas.initial_state_gas, 0, + "T1 standard tx with nonce==0 must NOT track state gas" + ); + } } diff --git a/crates/revm/src/lib.rs b/crates/revm/src/lib.rs index 11acb8ecc5..14419616b6 100644 --- a/crates/revm/src/lib.rs +++ b/crates/revm/src/lib.rs @@ -2,6 +2,7 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::result_large_err)] mod block; // Suppress unused_crate_dependencies warning for tracing diff --git a/crates/transaction-pool/src/test_utils.rs b/crates/transaction-pool/src/test_utils.rs index b5534c1f20..e9b391933e 100644 --- a/crates/transaction-pool/src/test_utils.rs +++ b/crates/transaction-pool/src/test_utils.rs @@ -132,6 +132,12 @@ impl TxBuilder { self } + /// Set the chain ID. + pub(crate) fn chain_id(mut self, chain_id: u64) -> Self { + self.chain_id = chain_id; + self + } + /// Set the max fee per gas. pub(crate) fn max_fee(mut self, fee: u128) -> Self { self.max_fee_per_gas = fee; @@ -189,7 +195,7 @@ impl TxBuilder { }); let tx = TempoTransaction { - chain_id: 1, + chain_id: self.chain_id, max_priority_fee_per_gas: self.max_priority_fee_per_gas, max_fee_per_gas: self.max_fee_per_gas, gas_limit: self.gas_limit, diff --git a/crates/transaction-pool/src/transaction.rs b/crates/transaction-pool/src/transaction.rs index ae80b65ee9..fcc2149c25 100644 --- a/crates/transaction-pool/src/transaction.rs +++ b/crates/transaction-pool/src/transaction.rs @@ -49,6 +49,18 @@ pub struct TempoPooledTransaction { /// Used by `keychain_subject()` so pool maintenance matches against the same token /// that was validated without requiring state access. resolved_fee_token: OnceLock
, + /// Intrinsic state gas (TIP-1016 storage creation gas component). + /// + /// Set during validation for T3+ transactions. The state gas portion does NOT count + /// against protocol limits (block/transaction execution gas caps), only against + /// the user's `gas_limit`. + /// + /// NOTE: This value is hardfork-specific (gas parameters vary by fork). If a + /// transaction survives a hardfork boundary, the cached value may be stale. + /// Future fork transitions that change state gas parameters should add pool + /// maintenance logic (like `evict_underpriced_transactions_for_t1`) to + /// re-validate or evict affected transactions. + intrinsic_state_gas: OnceLock, } impl TempoPooledTransaction { @@ -76,6 +88,7 @@ impl TempoPooledTransaction { tx_env: OnceLock::new(), key_expiry: OnceLock::new(), resolved_fee_token: OnceLock::new(), + intrinsic_state_gas: OnceLock::new(), } } @@ -220,6 +233,27 @@ impl TempoPooledTransaction { let aa_tx = self.inner().as_aa()?; Some(aa_tx.expiring_nonce_hash(self.sender())) } + + /// Sets the intrinsic state gas for this transaction (TIP-1016). + /// + /// Called during validation for T3+ transactions. State gas is the storage creation + /// gas component that does NOT count against protocol limits. + pub fn set_intrinsic_state_gas(&self, state_gas: u64) { + let _ = self.intrinsic_state_gas.set(state_gas); + } + + /// Returns the intrinsic state gas, or 0 if not set (pre-T3 or not yet validated). + pub fn intrinsic_state_gas(&self) -> u64 { + self.intrinsic_state_gas.get().copied().unwrap_or(0) + } + + /// Returns the execution gas limit (gas_limit minus state gas). + /// + /// For T3+ this is the portion that counts against protocol limits. + /// For pre-T3, state gas is 0, so this equals `gas_limit()`. + pub fn execution_gas_limit(&self) -> u64 { + self.gas_limit().saturating_sub(self.intrinsic_state_gas()) + } } #[derive(Debug, Error)] @@ -1033,7 +1067,7 @@ mod tests { .build(); // Test various Transaction trait methods - assert_eq!(tx.chain_id(), Some(1)); + assert_eq!(tx.chain_id(), Some(42431)); assert_eq!(tx.nonce(), 0); assert_eq!(tx.gas_limit(), 1_000_000); assert_eq!(tx.max_fee_per_gas(), 20_000_000_000); @@ -1052,6 +1086,36 @@ mod tests { // PoolTransaction::cost() returns &U256::ZERO for Tempo assert_eq!(*tx.cost(), U256::ZERO); } + + #[test] + fn test_intrinsic_state_gas_defaults_to_zero() { + let tx = TxBuilder::aa(Address::random()) + .gas_limit(1_000_000) + .build(); + + assert_eq!(tx.intrinsic_state_gas(), 0); + assert_eq!(tx.execution_gas_limit(), 1_000_000); + } + + #[test] + fn test_set_intrinsic_state_gas() { + let tx = TxBuilder::aa(Address::random()) + .gas_limit(1_000_000) + .build(); + + tx.set_intrinsic_state_gas(245_000); + assert_eq!(tx.intrinsic_state_gas(), 245_000); + assert_eq!(tx.execution_gas_limit(), 755_000); + } + + #[test] + fn test_execution_gas_limit_saturates() { + let tx = TxBuilder::aa(Address::random()).gas_limit(100).build(); + + // State gas larger than gas_limit should saturate to 0 + tx.set_intrinsic_state_gas(200); + assert_eq!(tx.execution_gas_limit(), 0); + } } // ======================================== diff --git a/crates/transaction-pool/src/validator.rs b/crates/transaction-pool/src/validator.rs index e0010cad2d..d618dde716 100644 --- a/crates/transaction-pool/src/validator.rs +++ b/crates/transaction-pool/src/validator.rs @@ -482,15 +482,18 @@ where if spec.is_t1() { // Expiring nonce transactions if tx.nonce_key == TEMPO_EXPIRING_NONCE_KEY { - init_and_floor_gas.initial_gas += EXPIRING_NONCE_GAS; + init_and_floor_gas.initial_total_gas += EXPIRING_NONCE_GAS; } else if tx.nonce == 0 { // TIP-1000: Storage pricing updates for launch // Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas - init_and_floor_gas.initial_gas += gas_params.get(GasId::new_account_cost()); + init_and_floor_gas.initial_total_gas += gas_params.get(GasId::new_account_cost()); + // TIP-1016: Track state gas for new account creation + init_and_floor_gas.initial_state_gas += + gas_params.get(GasId::new_account_state_gas()); } else if !tx.nonce_key.is_zero() { // Existing 2D nonce key (nonce > 0): cold SLOAD + warm SSTORE reset // TIP-1000 Invariant 3: existing state updates charge 5,000 gas - init_and_floor_gas.initial_gas += spec.gas_existing_nonce_key(); + init_and_floor_gas.initial_total_gas += spec.gas_existing_nonce_key(); } // In CREATE tx with 2d nonce, check if account.nonce is 0, if so, add 250,000 gas. // This covers caller creation of account. @@ -504,27 +507,42 @@ where .unwrap_or_default() == 0 { - init_and_floor_gas.initial_gas += gas_params.get(GasId::new_account_cost()); + init_and_floor_gas.initial_total_gas += gas_params.get(GasId::new_account_cost()); + // TIP-1016: Track state gas for caller account creation + init_and_floor_gas.initial_state_gas += + gas_params.get(GasId::new_account_state_gas()); } } else if !tx.nonce_key.is_zero() { // Pre-T1: Add 2D nonce gas if nonce_key is non-zero if tx.nonce == 0 { // New key - cold SLOAD + SSTORE set (0 -> non-zero) - init_and_floor_gas.initial_gas += spec.gas_new_nonce_key(); + init_and_floor_gas.initial_total_gas += spec.gas_new_nonce_key(); } else { // Existing key - cold SLOAD + warm SSTORE reset - init_and_floor_gas.initial_gas += spec.gas_existing_nonce_key(); + init_and_floor_gas.initial_total_gas += spec.gas_existing_nonce_key(); } } + // TIP-1016: Cache intrinsic state gas for T3+ transactions. + // init_and_floor_gas.initial_state_gas accumulates state gas from: + // - calculate_aa_batch_intrinsic_gas (CREATE calls, auth list) + // - nonce=0 new account creation (above) + // - 2D nonce + CREATE caller account creation (above) + // For pre-T3, initial_state_gas is 0 (state gas GasIds are not overridden). + if spec.is_t3() { + transaction.set_intrinsic_state_gas(init_and_floor_gas.initial_state_gas); + } + let gas_limit = tx.gas_limit; - // Check if gas limit is sufficient for initial gas - if gas_limit < init_and_floor_gas.initial_gas { + // Check if gas limit is sufficient for initial gas (execution + state) + let total_intrinsic = + init_and_floor_gas.initial_total_gas + init_and_floor_gas.initial_state_gas; + if gas_limit < total_intrinsic { return Err( TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { gas_limit, - intrinsic_gas: init_and_floor_gas.initial_gas, + intrinsic_gas: total_intrinsic, }, ); } @@ -746,7 +764,9 @@ where ); } } else { - // validate intrinsic gas with additional TIP-1000 and T1 checks + // validate intrinsic gas with additional TIP-1000 and T1 checks. + // For T3+, this also computes/caches intrinsic state gas and validates that + // gas_limit covers both execution gas and state gas. if let Err(err) = ensure_intrinsic_gas_tempo_tx(&transaction, spec) { return TransactionValidationOutcome::Invalid(transaction, err); } @@ -1108,6 +1128,9 @@ where } /// Ensures that gas limit of the transaction exceeds the intrinsic gas of the transaction. +/// +/// For T3+ transactions, also computes and caches the intrinsic state gas, and validates +/// that gas_limit covers both execution gas and state gas. pub fn ensure_intrinsic_gas_tempo_tx( tx: &TempoPooledTransaction, spec: TempoHardfork, @@ -1129,7 +1152,7 @@ pub fn ensure_intrinsic_gas_tempo_tx( // no need for v1 fork check as gas_params would be zero for auth in tx.authorization_list().unwrap_or_default() { if auth.nonce == 0 { - gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost(); + gas.initial_total_gas += gas_params.tx_tip1000_auth_account_creation_cost(); } } @@ -1139,16 +1162,42 @@ pub fn ensure_intrinsic_gas_tempo_tx( // - Regular/2D nonce with nonce == 0: new_account_cost (250k) for potential account creation if spec.is_t1() && tx.nonce() == 0 { if tx.nonce_key() == Some(TEMPO_EXPIRING_NONCE_KEY) { - gas.initial_gas += EXPIRING_NONCE_GAS; + gas.initial_total_gas += EXPIRING_NONCE_GAS; } else { - gas.initial_gas += gas_params.get(GasId::new_account_cost()); + gas.initial_total_gas += gas_params.get(GasId::new_account_cost()); + } + } + + // TIP-1016: For T3+, compute state gas and validate gas_limit covers both execution + state gas. + // State gas is tracked separately via initial_state_gas and does not count against protocol + // limits, but must still fit within gas_limit. + // For pre-T3, state gas is bundled into initial_total_gas (no split), so we clear + // initial_state_gas to avoid double-counting from upstream defaults in initial_tx_gas. + if !spec.is_t3() { + gas.initial_state_gas = 0; + } else { + // nonce == 0 with non-expiring nonce: potential new account creation + if tx.nonce() == 0 && tx.nonce_key() != Some(TEMPO_EXPIRING_NONCE_KEY) { + gas.initial_state_gas += gas_params.get(GasId::new_account_state_gas()); + } + // EIP-7702 auth list account creation state gas + for auth in tx.authorization_list().unwrap_or_default() { + if auth.nonce == 0 { + gas.initial_state_gas += gas_params.tx_tip1000_auth_account_creation_state_gas(); + } } } let gas_limit = tx.gas_limit(); - if gas_limit < gas.initial_gas || gas_limit < gas.floor_gas { + // TIP-1016: gas_limit must cover both execution gas and state gas + let total_intrinsic = gas.initial_total_gas + gas.initial_state_gas; + if gas_limit < total_intrinsic || gas_limit < gas.floor_gas { Err(InvalidPoolTransactionError::IntrinsicGasTooLow) } else { + // TIP-1016: Cache intrinsic state gas for T3+ transactions + if spec.is_t3() { + tx.set_intrinsic_state_gas(gas.initial_state_gas); + } Ok(()) } } @@ -1166,14 +1215,15 @@ mod tests { PoolTransaction, blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder, }; use std::sync::Arc; - use tempo_chainspec::spec::{MODERATO, TEMPO_T1_TX_GAS_LIMIT_CAP}; + use tempo_chainspec::spec::{DEV, MODERATO, TEMPO_T1_TX_GAS_LIMIT_CAP}; + use tempo_evm::TempoEvmConfig; use tempo_precompiles::{ PATH_USD_ADDRESS, TIP403_REGISTRY_ADDRESS, tip20::{TIP20Token, slots as tip20_slots}, tip403_registry::{ITIP403Registry, PolicyData, TIP403Registry}, }; use tempo_primitives::{ - Block, TempoHeader, TempoPrimitives, TempoTxEnvelope, + Block, TempoHeader, TempoPrimitives, TempoTxEnvelope, TempoTxType, transaction::{ TempoTransaction, envelope::TEMPO_SYSTEM_TX_SIGNATURE, @@ -1274,6 +1324,8 @@ mod tests { let inner = EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet()) .disable_balance_check() + .with_custom_tx_type(TempoTxType::AA as u8) + .no_eip4844() .build(InMemoryBlobStore::default()); let amm_cache = AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache"); @@ -1291,6 +1343,74 @@ mod tests { validator } + /// Like `setup_validator` but uses the DEV chain spec (which has T2 active at timestamp 0). + fn setup_validator_dev( + transaction: &TempoPooledTransaction, + tip_timestamp: u64, + ) -> TempoTransactionValidator> + { + let chain_spec = DEV.clone(); + let provider = + MockEthProvider::::new() + .with_chain_spec(Arc::unwrap_or_clone(chain_spec.clone())); + provider.add_account( + transaction.sender(), + ExtendedAccount::new(transaction.nonce(), alloy_primitives::U256::ZERO), + ); + let block_with_gas = Block { + header: TempoHeader { + inner: alloy_consensus::Header { + gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP, + ..Default::default() + }, + ..Default::default() + }, + body: Default::default(), + }; + provider.add_block(B256::random(), block_with_gas); + + let usd_currency_value = + uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256); + let transfer_policy_id_packed = + uint!(0x0000000000000000000000010000000000000000000000000000000000000000_U256); + let balance_slot = TIP20Token::from_address(PATH_USD_ADDRESS) + .expect("PATH_USD_ADDRESS is a valid TIP20 token") + .balances[transaction.sender()] + .slot(); + let fee_payer_balance = U256::from(1_000_000_000_000u64); + provider.add_account( + PATH_USD_ADDRESS, + ExtendedAccount::new(0, U256::ZERO).extend_storage([ + (tip20_slots::CURRENCY.into(), usd_currency_value), + ( + tip20_slots::TRANSFER_POLICY_ID.into(), + transfer_policy_id_packed, + ), + (balance_slot.into(), fee_payer_balance), + ]), + ); + + let evm_config = TempoEvmConfig::new(chain_spec); + let inner = EthTransactionValidatorBuilder::new(provider.clone(), evm_config) + .disable_balance_check() + .with_custom_tx_type(TempoTxType::AA as u8) + .no_eip4844() + .build(InMemoryBlobStore::default()); + let amm_cache = + AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache"); + let validator = TempoTransactionValidator::new( + inner, + DEFAULT_AA_VALID_AFTER_MAX_SECS, + DEFAULT_MAX_TEMPO_AUTHORIZATIONS, + amm_cache, + ); + + let mock_block = create_mock_block(tip_timestamp); + validator.on_new_head_block(&mock_block); + + validator + } + #[tokio::test] async fn test_some_balance() { let transaction = TxBuilder::eip1559(Address::random()) @@ -5534,9 +5654,9 @@ mod tests { .unwrap_or_default() as u64, ); if spec.is_t1() && tx_probe.nonce() == 0 { - gas.initial_gas += gas_params.get(GasId::new_account_cost()); + gas.initial_total_gas += gas_params.get(GasId::new_account_cost()); } - let intrinsic = std::cmp::max(gas.initial_gas, gas.floor_gas); + let intrinsic = std::cmp::max(gas.initial_total_gas, gas.floor_gas); let tx_exact = TxBuilder::eip1559(Address::random()) .gas_limit(intrinsic) @@ -5558,4 +5678,69 @@ mod tests { "Gas limit one below intrinsic gas should fail, got: {result:?}" ); } + + /// Test that T2 validation caches intrinsic state gas on the pooled transaction. + #[tokio::test] + async fn test_t2_intrinsic_state_gas_cached_on_aa_tx() { + // DEV chain spec has T2 active at timestamp 0, chain_id=1337 + let pooled = TxBuilder::aa(Address::random()) + .chain_id(1337) + .fee_token(PATH_USD_ADDRESS) + .gas_limit(1_000_000) + .build(); + + let validator = setup_validator_dev(&pooled, 100); + let outcome = validator + .validate_transaction(TransactionOrigin::External, pooled) + .await; + + match &outcome { + TransactionValidationOutcome::Valid { transaction, .. } => { + let state_gas = transaction.transaction().intrinsic_state_gas(); + assert_eq!( + state_gas, 245_000, + "state gas should be 245k for nonce=0 T3 tx" + ); + assert_eq!( + transaction.transaction().execution_gas_limit(), + 1_000_000 - 245_000, + "execution gas limit should be gas_limit - state_gas" + ); + } + other => panic!("Expected Valid outcome, got: {other:?}"), + } + } + + /// Test that T1 validation does NOT set intrinsic state gas (remains 0). + #[tokio::test] + async fn test_t1_no_intrinsic_state_gas() { + // MODERATO has T1 active but no T2 + let t1_timestamp = 1770303600u64 + 100; + + let pooled = TxBuilder::aa(Address::random()) + .fee_token(PATH_USD_ADDRESS) + .gas_limit(1_000_000) + .build(); + + let validator = setup_validator(&pooled, t1_timestamp); + let outcome = validator + .validate_transaction(TransactionOrigin::External, pooled) + .await; + + match &outcome { + TransactionValidationOutcome::Valid { transaction, .. } => { + assert_eq!( + transaction.transaction().intrinsic_state_gas(), + 0, + "T1 should not set state gas" + ); + assert_eq!( + transaction.transaction().execution_gas_limit(), + 1_000_000, + "T1 execution_gas_limit should equal gas_limit" + ); + } + other => panic!("Expected Valid outcome, got: {other:?}"), + } + } } diff --git a/deny.toml b/deny.toml index f959c148bd..9ea338e850 100644 --- a/deny.toml +++ b/deny.toml @@ -83,8 +83,11 @@ unknown-registry = "warn" # in the allow list is encountered unknown-git = "deny" allow-git = [ + "https://github.com/alloy-rs/evm", + "https://github.com/bluealloy/revm.git", "https://github.com/commonwarexyz/monorepo", "https://github.com/foundry-rs/foundry", "https://github.com/paradigmxyz/reth", + "https://github.com/paradigmxyz/revm-inspectors", "https://github.com/DaniPopes/slotmap.git", ] diff --git a/tips/ref-impls/test/invariants/BlockGasLimits.t.sol b/tips/ref-impls/test/invariants/BlockGasLimits.t.sol index 0aef4262e3..5846616593 100644 --- a/tips/ref-impls/test/invariants/BlockGasLimits.t.sol +++ b/tips/ref-impls/test/invariants/BlockGasLimits.t.sol @@ -22,6 +22,14 @@ import { LegacyTransaction, LegacyTransactionLib } from "tempo-std/tx/LegacyTran /// - Payment lane minimum: 470,000,000 (TEMPO-BLOCK5) /// - Max deployment fits in tx cap (TEMPO-BLOCK6) /// +/// TIP-1016 state gas changes: +/// - Regular gas counts against tx/block limits; state gas is exempt +/// - tx.gas > max_transaction_gas_limit is VALID when excess is state gas +/// - Block gasUsed reflects regular gas only +/// - Code deposit: 200 regular + 2,300 state per byte +/// - CREATE base: 32,000 regular + 468,000 state +/// - Account creation: 25,000 regular + 225,000 state +/// /// Block-level lane enforcement (BLOCK7, BLOCK12) and shared gas limit /// (BLOCK10) are tested in Rust (crates/consensus/src/lib.rs). contract BlockGasLimitsInvariantTest is InvariantBase { @@ -63,6 +71,34 @@ contract BlockGasLimitsInvariantTest is InvariantBase { /// @dev TIP-1000: Account creation gas uint256 internal constant ACCOUNT_CREATION_GAS = 250_000; + /*////////////////////////////////////////////////////////////// + TIP-1016 CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @dev TIP-1016: SSTORE regular gas component + uint256 internal constant SSTORE_REGULAR_GAS = 20_000; + + /// @dev TIP-1016: SSTORE state gas component + uint256 internal constant SSTORE_STATE_GAS = 230_000; + + /// @dev TIP-1016: Code deposit regular gas per byte + uint256 internal constant CODE_DEPOSIT_REGULAR_PER_BYTE = 200; + + /// @dev TIP-1016: Code deposit state gas per byte + uint256 internal constant CODE_DEPOSIT_STATE_PER_BYTE = 2300; + + /// @dev TIP-1016: CREATE regular gas component + uint256 internal constant CREATE_REGULAR_GAS = 32_000; + + /// @dev TIP-1016: CREATE state gas component + uint256 internal constant CREATE_STATE_GAS = 468_000; + + /// @dev TIP-1016: Account creation regular gas component + uint256 internal constant ACCOUNT_CREATION_REGULAR_GAS = 25_000; + + /// @dev TIP-1016: Account creation state gas component + uint256 internal constant ACCOUNT_CREATION_STATE_GAS = 225_000; + /*////////////////////////////////////////////////////////////// GHOST VARIABLES //////////////////////////////////////////////////////////////*/ @@ -70,14 +106,18 @@ contract BlockGasLimitsInvariantTest is InvariantBase { /// @dev TEMPO-BLOCK3: Tx gas cap enforcement uint256 public ghost_txGasCapTests; uint256 public ghost_txAtCapSucceeded; - uint256 public ghost_txOverCapRejected; - uint256 public ghost_txOverCapViolations; // Over-cap tx was accepted + uint256 public ghost_txOverCapWithStateGasSucceeded; // tx.gas > cap is valid when excess is state gas /// @dev TEMPO-BLOCK6: Deployment fits in cap uint256 public ghost_deploymentTests; uint256 public ghost_maxDeploymentSucceeded; uint256 public ghost_maxDeploymentFailed; // Unexpected - would indicate cap too low + /// @dev TIP-1016: Block gasUsed reflects regular gas only + /// @notice Verified at the block level in Rust (crates/consensus/src/lib.rs); + /// this ghost tracks our Solidity-side accounting for consistency. + uint256 public ghost_blockGasUsedRegularOnly; + /// @dev General tracking uint256 public ghost_validTxExecuted; @@ -105,14 +145,19 @@ contract BlockGasLimitsInvariantTest is InvariantBase { function invariant_globalInvariants() public view { _invariantTxGasCap(); _invariantMaxDeploymentFits(); + _invariantBlockGasRegularOnly(); } - /// @notice TEMPO-BLOCK3: Tx gas cap must be enforced at 30M - /// @dev Violations occur if tx with gas > 30M is accepted + /// @notice TEMPO-BLOCK3 + TIP-1016: Tx gas cap applies to regular gas only + /// @dev Post-TIP-1016, tx.gas > TX_GAS_CAP is valid when excess is state gas. + /// We verify that such transactions succeed rather than being rejected. function _invariantTxGasCap() internal view { - assertEq( - ghost_txOverCapViolations, 0, "TEMPO-BLOCK3: Transaction over 30M gas cap was accepted" - ); + if (ghost_txGasCapTests > 0) { + assertTrue( + ghost_txOverCapWithStateGasSucceeded > 0 || ghost_txAtCapSucceeded > 0, + "TEMPO-BLOCK3: No gas cap tests succeeded" + ); + } } /// @notice TEMPO-BLOCK6: Max contract deployment (24KB) must fit in tx cap @@ -126,14 +171,22 @@ contract BlockGasLimitsInvariantTest is InvariantBase { } } + /// @notice TIP-1016: Block gasUsed must reflect regular gas only + /// @dev State gas is exempt from block gas accounting. This ghost is a + /// Solidity-side placeholder; actual block-level verification is + /// performed in Rust (crates/consensus/src/lib.rs). + function _invariantBlockGasRegularOnly() internal view { + assertEq(ghost_blockGasUsedRegularOnly, 0, "TIP-1016: block.gasUsed included state gas"); + } + /*////////////////////////////////////////////////////////////// HANDLERS //////////////////////////////////////////////////////////////*/ - /// @notice Handler: Test tx gas cap enforcement (TEMPO-BLOCK3) + /// @notice Handler: Test tx gas cap enforcement (TEMPO-BLOCK3 + TIP-1016) /// @param actorSeed Seed for selecting actor - /// @param gasMultiplier Multiplier to test various gas levels - function handler_txGasCapEnforcement(uint256 actorSeed, uint256 gasMultiplier) external { + /// @param stateGasExtra Extra state gas above the cap (1 to 1M) + function handler_txGasCapEnforcement(uint256 actorSeed, uint256 stateGasExtra) external { // Skip when not on Tempo (vmExec.executeTransaction not available) if (!isTempo) return; @@ -163,11 +216,12 @@ contract BlockGasLimitsInvariantTest is InvariantBase { // May fail for other reasons (balance, etc.) - not a violation } - // Test 2: Tx over the cap (should be rejected) + // Test 2: Tx with gas ABOVE the cap where excess is state gas (should succeed) + // Post-TIP-1016, tx.gas > TX_GAS_CAP is valid when the excess goes to + // the state gas reservoir (e.g., an SSTORE needs SSTORE_STATE_GAS). nonce = uint64(vm.getNonce(sender)); - // Gas amount over cap: 30M + 1 to 30M + 10M based on multiplier - uint256 overAmount = bound(gasMultiplier, 1, 10_000_000); + uint256 overAmount = bound(stateGasExtra, 1, 1_000_000); uint64 overCapGas = uint64(TX_GAS_CAP + overAmount); bytes memory overCapTx = TxBuilder.buildLegacyCallWithGas( @@ -175,17 +229,16 @@ contract BlockGasLimitsInvariantTest is InvariantBase { ); try vmExec.executeTransaction(overCapTx) { - // Over-cap tx was accepted - VIOLATION - ghost_txOverCapViolations++; + // Over-cap tx accepted — valid post-TIP-1016 (excess is state gas) + ghost_txOverCapWithStateGasSucceeded++; ghost_protocolNonce[sender]++; - } catch (bytes memory reason) { - if (_isGasCapRevert(reason)) { - ghost_txOverCapRejected++; - } + ghost_validTxExecuted++; + } catch { + // May fail for other reasons (balance, etc.) - not a violation } } - /// @notice Handler: Test max contract deployment fits in cap (TEMPO-BLOCK6) + /// @notice Handler: Test max contract deployment fits in cap (TEMPO-BLOCK6 + TIP-1016) /// @param actorSeed Seed for selecting actor /// @param sizeFraction Fraction of max size to deploy (50-100%) function handler_maxDeploymentFits(uint256 actorSeed, uint256 sizeFraction) external { @@ -206,13 +259,21 @@ contract BlockGasLimitsInvariantTest is InvariantBase { // Simple initcode: PUSH1 0x00 PUSH1 0x00 RETURN + padding bytes memory initcode = _createInitcodeOfSize(targetSize); - // Calculate required gas - uint256 requiredGas = 53_000 // CREATE tx base - + CREATE_BASE_GAS + (initcode.length * CODE_DEPOSIT_PER_BYTE) + ACCOUNT_CREATION_GAS - + 100_000; // Buffer for memory expansion etc. + // TIP-1016: Compute regular and state gas separately. + // A 24KB contract needs ~7M regular gas but ~57M state gas. + // tx.gas = regular + state can exceed TX_GAS_CAP because state gas + // is exempt from the cap (goes to reservoir). + uint256 requiredRegularGas = 53_000 // CREATE tx base + + CREATE_REGULAR_GAS + (initcode.length * CODE_DEPOSIT_REGULAR_PER_BYTE) + + ACCOUNT_CREATION_REGULAR_GAS + 100_000; // Buffer for memory expansion etc. + + uint256 requiredStateGas = CREATE_STATE_GAS + + (initcode.length * CODE_DEPOSIT_STATE_PER_BYTE) + ACCOUNT_CREATION_STATE_GAS; + + uint256 totalGas = requiredRegularGas + requiredStateGas; - // Should fit in TX_GAS_CAP - uint64 gasLimit = uint64(requiredGas > TX_GAS_CAP ? TX_GAS_CAP : requiredGas); + // totalGas can exceed TX_GAS_CAP — state gas is exempt from the cap + uint64 gasLimit = uint64(totalGas); uint64 nonce = uint64(vm.getNonce(sender)); bytes memory createTx = diff --git a/tips/ref-impls/test/invariants/GasPricing.t.sol b/tips/ref-impls/test/invariants/GasPricing.t.sol index f74c065e8f..0d2582b719 100644 --- a/tips/ref-impls/test/invariants/GasPricing.t.sol +++ b/tips/ref-impls/test/invariants/GasPricing.t.sol @@ -11,7 +11,7 @@ import { TxBuilder } from "../helpers/TxBuilder.sol"; import { VmExecuteTransaction, VmRlp } from "tempo-std/StdVm.sol"; import { LegacyTransaction, LegacyTransactionLib } from "tempo-std/tx/LegacyTransactionLib.sol"; -/// @title TIP-1000 Gas Pricing Invariant Tests +/// @title TIP-1000 / TIP-1016 Gas Pricing Invariant Tests /// @notice Fuzz-based invariant tests for Tempo's state creation gas costs /// @dev Tests gas pricing invariants at the EVM opcode level using vmExec.executeTransaction() /// @@ -22,6 +22,11 @@ import { LegacyTransaction, LegacyTransactionLib } from "tempo-std/tx/LegacyTran /// - Account creation: 250,000 gas (part of TEMPO-GAS5) /// - Multiple new slots: 250,000 gas each (TEMPO-GAS8) /// +/// TIP-1016 splits gas into two dimensions: +/// - Regular gas (20k for SSTORE new slot) — counts against tx/block limits +/// - State gas (230k for SSTORE new slot) — exempt from limits but still charged +/// Total gas per SSTORE remains 250k. +/// /// Protocol-level invariants (tx gas cap, intrinsic gas) are tested in Rust. contract GasPricingInvariantTest is InvariantBase { @@ -32,9 +37,19 @@ contract GasPricingInvariantTest is InvariantBase { TIP-1000 CONSTANTS //////////////////////////////////////////////////////////////*/ - /// @dev SSTORE to new (zero) slot costs 250,000 gas + /// @dev SSTORE to new (zero) slot costs 250,000 gas total uint256 internal constant SSTORE_SET_GAS = 250_000; + /*////////////////////////////////////////////////////////////// + TIP-1016 CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @dev Regular gas for SSTORE new slot (counts against tx/block limits) + uint256 internal constant SSTORE_REGULAR_GAS = 20_000; + + /// @dev State gas for SSTORE new slot (exempt from limits, still charged) + uint256 internal constant SSTORE_STATE_GAS = 230_000; + /// @dev CREATE base cost (excludes code deposit and account creation) uint256 internal constant CREATE_BASE_GAS = 500_000; @@ -85,6 +100,10 @@ contract GasPricingInvariantTest is InvariantBase { uint256 public ghost_multiSlotSufficientGasSucceeded; uint256 public ghost_multiSlotViolations; // All slots written with insufficient gas + /// @dev TIP-1016: State gas tracking (block vs receipt delta) + uint256 public ghost_stateGasBlockDelta; + uint256 public ghost_stateGasReceiptDelta; + /*////////////////////////////////////////////////////////////// SETUP //////////////////////////////////////////////////////////////*/ @@ -285,8 +304,8 @@ contract GasPricingInvariantTest is InvariantBase { bytes memory callData = abi.encodeCall(GasTestStorage.storeMultiple, (slots)); uint64 nonce = uint64(vm.getNonce(sender)); - // Test 1: Gas sufficient for ~1 slot only (should fail for N>1) - uint64 lowGas = uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_SET_GAS + GAS_TOLERANCE); + // Test 1: Only enough regular gas for 1 SSTORE (insufficient total for any SSTORE) + uint64 lowGas = uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_REGULAR_GAS + GAS_TOLERANCE); bytes memory lowGasTx = TxBuilder.buildLegacyCallWithGas( vmRlp, vm, address(storageContract), callData, nonce, lowGas, privateKey ); @@ -302,11 +321,10 @@ contract GasPricingInvariantTest is InvariantBase { } } - // Violation: all slots written with gas for only 1 - if (written == numSlots) { + // Violation: any slot written with insufficient gas + if (written > 0) { ghost_multiSlotViolations++; } else { - // Partial write is expected (reverted mid-execution) ghost_multiSlotInsufficientGasFailed++; } ghost_protocolNonce[sender]++; diff --git a/tips/ref-impls/test/invariants/TIP1016.t.sol b/tips/ref-impls/test/invariants/TIP1016.t.sol new file mode 100644 index 0000000000..ca7a67714e --- /dev/null +++ b/tips/ref-impls/test/invariants/TIP1016.t.sol @@ -0,0 +1,628 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.13 <0.9.0; + +import { Test } from "forge-std/Test.sol"; + +import { ITIP20 } from "../../src/interfaces/ITIP20.sol"; +import { InvariantBase } from "../helpers/InvariantBase.sol"; +import { Counter, InitcodeHelper, SimpleStorage } from "../helpers/TestContracts.sol"; +import { TxBuilder } from "../helpers/TxBuilder.sol"; + +import { VmExecuteTransaction, VmRlp } from "tempo-std/StdVm.sol"; +import { LegacyTransaction, LegacyTransactionLib } from "tempo-std/tx/LegacyTransactionLib.sol"; + +/// @title TIP-1016 State Gas Exemption Invariant Tests +/// @notice Fuzz-based invariant tests for TIP-1016's gas dimension split, reservoir model, +/// block accounting, and refund semantics +/// @dev Tests invariants using vmExec.executeTransaction() on a Tempo fork +/// +/// TIP-1016 splits gas into two dimensions: +/// - Regular gas: counts against tx/block limits +/// - State gas: exempt from limits but still charged +/// +/// Invariant groups: +/// - GAS1-GAS3: Gas dimension split (SSTORE, contract deploy) +/// - RES1-RES3: Reservoir model (GAS opcode, conservation, overflow) +/// - BLK1-BLK3: Block accounting (gasUsed, receipts, mixed workload) +/// - REF1-REF2: Refund semantics (SSTORE restoration, refund cap) +contract TIP1016InvariantTest is InvariantBase { + + using TxBuilder for *; + using LegacyTransactionLib for LegacyTransaction; + + /*////////////////////////////////////////////////////////////// + TIP-1016 CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @dev SSTORE to new slot: regular gas component + uint256 internal constant SSTORE_REGULAR_GAS = 20_000; + + /// @dev SSTORE to new slot: state gas component + uint256 internal constant SSTORE_STATE_GAS = 230_000; + + /// @dev SSTORE to new slot: total gas (regular + state) + uint256 internal constant SSTORE_SET_GAS = 250_000; + + /// @dev Hot SSTORE (NZ→NZ): no state gas + uint256 internal constant SSTORE_HOT_GAS = 2900; + + /// @dev SSTORE refund: regular gas refunded on 0→X→0 + uint256 internal constant SSTORE_REFUND_REGULAR = 17_800; + + /// @dev SSTORE refund: state gas refunded on 0→X→0 + uint256 internal constant SSTORE_REFUND_STATE = 230_000; + + /// @dev Contract metadata: regular gas (keccak + nonce) + uint256 internal constant CREATE_REGULAR_GAS = 32_000; + + /// @dev Contract metadata: state gas + uint256 internal constant CREATE_STATE_GAS = 468_000; + + /// @dev Account creation: regular gas + uint256 internal constant ACCOUNT_CREATION_REGULAR_GAS = 25_000; + + /// @dev Account creation: state gas + uint256 internal constant ACCOUNT_CREATION_STATE_GAS = 225_000; + + /// @dev Code deposit: regular gas per byte + uint256 internal constant CODE_DEPOSIT_REGULAR_PER_BYTE = 200; + + /// @dev Code deposit: state gas per byte + uint256 internal constant CODE_DEPOSIT_STATE_PER_BYTE = 2300; + + /// @dev Max transaction gas limit (EIP-7825 / TIP-1010) + uint256 internal constant MAX_TX_GAS_LIMIT = 30_000_000; + + /// @dev Base transaction cost + uint256 internal constant BASE_TX_GAS = 21_000; + + /// @dev Call overhead (cold account + call stipend) + uint256 internal constant CALL_OVERHEAD = 15_000; + + /// @dev Gas tolerance for measurements + uint256 internal constant GAS_TOLERANCE = 50_000; + + /*////////////////////////////////////////////////////////////// + TEST STATE + //////////////////////////////////////////////////////////////*/ + + TIP1016Storage internal storageContract; + GasLeftChecker internal gasLeftChecker; + uint256 internal slotCounter; + + /*////////////////////////////////////////////////////////////// + GHOST VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev TIP1016-GAS1: SSTORE 0→NZ gas dimension split + uint256 public ghost_gas1Tests; + uint256 public ghost_gas1Succeeded; + uint256 public ghost_gas1Violations; + + /// @dev TIP1016-GAS2: SSTORE NZ→NZ (no state gas) + uint256 public ghost_gas2Tests; + uint256 public ghost_gas2Succeeded; + uint256 public ghost_gas2Violations; + + /// @dev TIP1016-GAS3: Contract deploy state gas exemption + uint256 public ghost_gas3Tests; + uint256 public ghost_gas3Succeeded; + uint256 public ghost_gas3Violations; + + /// @dev TIP1016-RES1: GAS opcode returns gas_left only + uint256 public ghost_res1Tests; + uint256 public ghost_res1Succeeded; + uint256 public ghost_res1Violations; + + /// @dev TIP1016-RES2: Gas conservation + uint256 public ghost_res2Checked; + + /// @dev TIP1016-RES3: tx.gas > max_transaction_gas_limit succeeds with state gas + uint256 public ghost_res3Tests; + uint256 public ghost_res3Succeeded; + uint256 public ghost_res3Violations; + + /// @dev TIP1016-BLK1: block.gasUsed ≤ block.gasLimit + uint256 public ghost_blk1Violations; + + /// @dev TIP1016-BLK2: sum(receipt.gasUsed) ≥ block.gasUsed + uint256 public ghost_blk2AccumulatedReceiptGas; + uint256 public ghost_blk2Violations; + + /// @dev TIP1016-BLK3: Mixed workload — state-heavy txs don't crowd out regular txs + uint256 public ghost_blk3Tests; + uint256 public ghost_blk3StateHeavySucceeded; + uint256 public ghost_blk3RegularSucceeded; + uint256 public ghost_blk3Violations; + + /// @dev TIP1016-REF1: SSTORE 0→X→0 refund + uint256 public ghost_ref1Tests; + uint256 public ghost_ref1Succeeded; + uint256 public ghost_ref1Violations; + + /// @dev TIP1016-REF2: Refund cap (20%) + uint256 public ghost_ref2Checked; + + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + function setUp() public override { + super.setUp(); + + targetContract(address(this)); + + storageContract = new TIP1016Storage(); + gasLeftChecker = new GasLeftChecker(); + + bytes4[] memory selectors = new bytes4[](7); + selectors[0] = this.handler_sstoreNewSlot.selector; + selectors[1] = this.handler_sstoreExistingSlot.selector; + selectors[2] = this.handler_createContract.selector; + selectors[3] = this.handler_gasleftCheck.selector; + selectors[4] = this.handler_stateGasOverflow.selector; + selectors[5] = this.handler_mixedWorkload.selector; + selectors[6] = this.handler_sstoreSetAndClear.selector; + targetSelector(FuzzSelector({ addr: address(this), selectors: selectors })); + } + + /*////////////////////////////////////////////////////////////// + INVARIANTS + //////////////////////////////////////////////////////////////*/ + + function invariant_globalInvariants() public view { + _invariantGas1(); + _invariantGas2(); + _invariantGas3(); + _invariantRes1(); + _invariantRes3(); + _invariantBlk1(); + _invariantBlk3(); + _invariantRef1(); + } + + /// @notice TIP1016-GAS1: SSTORE 0→NZ must succeed with total gas (250k) + function _invariantGas1() internal view { + assertEq(ghost_gas1Violations, 0, "TIP1016-GAS1: SSTORE 0->NZ gas dimension violation"); + } + + /// @notice TIP1016-GAS2: SSTORE NZ→NZ must succeed with hot gas only (no state gas) + function _invariantGas2() internal view { + assertEq(ghost_gas2Violations, 0, "TIP1016-GAS2: SSTORE NZ->NZ gas violation"); + } + + /// @notice TIP1016-GAS3: Contract deploy state gas must be exempted from limits + function _invariantGas3() internal view { + assertEq(ghost_gas3Violations, 0, "TIP1016-GAS3: Contract deploy state gas violation"); + } + + /// @notice TIP1016-RES1: GAS opcode must return ≤ max_transaction_gas_limit + function _invariantRes1() internal view { + assertEq( + ghost_res1Violations, 0, "TIP1016-RES1: GAS opcode returned value > max_tx_gas_limit" + ); + } + + /// @notice TIP1016-RES3: tx.gas > max_transaction_gas_limit must succeed when excess is state gas + function _invariantRes3() internal view { + assertEq( + ghost_res3Violations, + 0, + "TIP1016-RES3: tx.gas > max_tx_gas_limit rejected with state gas" + ); + } + + /// @notice TIP1016-BLK1: block.gasUsed must not exceed block.gasLimit + function _invariantBlk1() internal view { + assertEq(ghost_blk1Violations, 0, "TIP1016-BLK1: block.gasUsed > block.gasLimit"); + } + + /// @notice TIP1016-BLK3: State-heavy txs must not crowd out regular txs + function _invariantBlk3() internal view { + assertEq(ghost_blk3Violations, 0, "TIP1016-BLK3: state-heavy txs crowded out regular txs"); + } + + /// @notice TIP1016-REF1: SSTORE 0→X→0 must refund state + regular gas + function _invariantRef1() internal view { + assertEq(ghost_ref1Violations, 0, "TIP1016-REF1: SSTORE 0->X->0 refund violation"); + } + + /*////////////////////////////////////////////////////////////// + HANDLERS + //////////////////////////////////////////////////////////////*/ + + /// @notice Handler: SSTORE 0→NZ gas dimension split (TIP1016-GAS1, RES2) + /// @param actorSeed Seed for selecting actor + /// @param slotSeed Seed for generating unique slot + function handler_sstoreNewSlot(uint256 actorSeed, uint256 slotSeed) external { + if (!isTempo) return; + + ghost_gas1Tests++; + + uint256 senderIdx = actorSeed % actors.length; + address sender = actors[senderIdx]; + uint256 privateKey = actorKeys[senderIdx]; + + bytes32 slot = keccak256(abi.encode(slotSeed, slotCounter++, block.timestamp)); + bytes memory callData = abi.encodeCall(TIP1016Storage.storeValue, (slot, 1)); + uint64 nonce = uint64(vm.getNonce(sender)); + + // Provide total gas for SSTORE (regular + state) + uint64 gasLimit = uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_SET_GAS + GAS_TOLERANCE); + bytes memory signedTx = TxBuilder.buildLegacyCallWithGas( + vmRlp, vm, address(storageContract), callData, nonce, gasLimit, privateKey + ); + + vm.coinbase(validator); + + try vmExec.executeTransaction(signedTx) { + if (storageContract.getValue(slot) != 0) { + ghost_gas1Succeeded++; + } else { + ghost_gas1Violations++; + } + ghost_protocolNonce[sender]++; + ghost_res2Checked++; + ghost_totalTxExecuted++; + } catch { + ghost_gas1Violations++; + ghost_totalTxReverted++; + } + } + + /// @notice Handler: SSTORE NZ→NZ no state gas (TIP1016-GAS2) + /// @param actorSeed Seed for selecting actor + /// @param slotSeed Seed for generating unique slot + function handler_sstoreExistingSlot(uint256 actorSeed, uint256 slotSeed) external { + if (!isTempo) return; + + ghost_gas2Tests++; + + uint256 senderIdx = actorSeed % actors.length; + address sender = actors[senderIdx]; + uint256 privateKey = actorKeys[senderIdx]; + + // First: write a value to create the slot (0→NZ) + bytes32 slot = keccak256(abi.encode(slotSeed, slotCounter++, "existing")); + bytes memory setupData = abi.encodeCall(TIP1016Storage.storeValue, (slot, 1)); + uint64 nonce = uint64(vm.getNonce(sender)); + + uint64 setupGas = uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_SET_GAS + GAS_TOLERANCE); + bytes memory setupTx = TxBuilder.buildLegacyCallWithGas( + vmRlp, vm, address(storageContract), setupData, nonce, setupGas, privateKey + ); + + vm.coinbase(validator); + + try vmExec.executeTransaction(setupTx) { + ghost_protocolNonce[sender]++; + } catch { + return; + } + + // Second: overwrite the slot (NZ→NZ) — should only cost hot SSTORE gas, no state gas + bytes memory overwriteData = abi.encodeCall(TIP1016Storage.storeValue, (slot, 2)); + nonce = uint64(vm.getNonce(sender)); + + // Only need hot SSTORE gas (2,900) + overhead — no state gas + uint64 hotGas = uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_HOT_GAS + GAS_TOLERANCE); + bytes memory hotTx = TxBuilder.buildLegacyCallWithGas( + vmRlp, vm, address(storageContract), overwriteData, nonce, hotGas, privateKey + ); + + try vmExec.executeTransaction(hotTx) { + if (storageContract.getValue(slot) == 2) { + ghost_gas2Succeeded++; + } else { + ghost_gas2Violations++; + } + ghost_protocolNonce[sender]++; + ghost_res2Checked++; + ghost_totalTxExecuted++; + } catch { + ghost_gas2Violations++; + ghost_totalTxReverted++; + } + } + + /// @notice Handler: Contract deploy state gas exemption (TIP1016-GAS3) + /// @param actorSeed Seed for selecting actor + /// @param sizeSeed Seed for contract size (1k-8k range for manageable gas) + function handler_createContract(uint256 actorSeed, uint256 sizeSeed) external { + if (!isTempo) return; + + ghost_gas3Tests++; + + uint256 senderIdx = actorSeed % actors.length; + address sender = actors[senderIdx]; + uint256 privateKey = actorKeys[senderIdx]; + + uint256 targetSize = bound(sizeSeed, 1000, 8000); + bytes memory initcode = _createInitcodeOfSize(targetSize); + + // Compute total gas with TIP-1016 split + uint256 regularGas = 53_000 + CREATE_REGULAR_GAS + + (initcode.length * CODE_DEPOSIT_REGULAR_PER_BYTE) + ACCOUNT_CREATION_REGULAR_GAS + + GAS_TOLERANCE; + + uint256 stateGas = CREATE_STATE_GAS + (initcode.length * CODE_DEPOSIT_STATE_PER_BYTE) + + ACCOUNT_CREATION_STATE_GAS; + + // Expected state gas exempted from limits + uint256 expectedExemptedStateGas = (targetSize * CODE_DEPOSIT_STATE_PER_BYTE) + + CREATE_STATE_GAS + ACCOUNT_CREATION_STATE_GAS; + + uint64 gasLimit = uint64(regularGas + stateGas); + uint64 nonce = uint64(vm.getNonce(sender)); + + bytes memory createTx = + TxBuilder.buildLegacyCreateWithGas(vmRlp, vm, initcode, nonce, gasLimit, privateKey); + + vm.coinbase(validator); + address expectedAddr = TxBuilder.computeCreateAddress(sender, nonce); + + try vmExec.executeTransaction(createTx) { + if (expectedAddr.code.length > 0) { + ghost_gas3Succeeded++; + } else { + ghost_gas3Violations++; + } + ghost_protocolNonce[sender]++; + ghost_res2Checked++; + ghost_totalTxExecuted++; + } catch { + ghost_gas3Violations++; + ghost_totalTxReverted++; + } + } + + /// @notice Handler: GAS opcode returns gas_left only (TIP1016-RES1) + /// @param actorSeed Seed for selecting actor + function handler_gasleftCheck(uint256 actorSeed) external { + if (!isTempo) return; + + ghost_res1Tests++; + + uint256 senderIdx = actorSeed % actors.length; + address sender = actors[senderIdx]; + uint256 privateKey = actorKeys[senderIdx]; + + bytes memory callData = abi.encodeCall(GasLeftChecker.checkGasLeft, ()); + uint64 nonce = uint64(vm.getNonce(sender)); + + // Set tx.gas well above max_transaction_gas_limit so excess goes to reservoir + uint64 gasLimit = uint64(MAX_TX_GAS_LIMIT + 5_000_000); + bytes memory signedTx = TxBuilder.buildLegacyCallWithGas( + vmRlp, vm, address(gasLeftChecker), callData, nonce, gasLimit, privateKey + ); + + vm.coinbase(validator); + + try vmExec.executeTransaction(signedTx) { + uint256 lastGasLeft = gasLeftChecker.lastGasLeft(); + // GAS opcode should return gas_left only, which is ≤ max_transaction_gas_limit + if (lastGasLeft <= MAX_TX_GAS_LIMIT) { + ghost_res1Succeeded++; + } else { + ghost_res1Violations++; + } + ghost_protocolNonce[sender]++; + ghost_totalTxExecuted++; + } catch { + ghost_totalTxReverted++; + } + } + + /// @notice Handler: tx.gas > max_transaction_gas_limit with state gas (TIP1016-RES3) + /// @param actorSeed Seed for selecting actor + /// @param extraGas Extra gas above the limit + function handler_stateGasOverflow(uint256 actorSeed, uint256 extraGas) external { + if (!isTempo) return; + + ghost_res3Tests++; + + uint256 senderIdx = actorSeed % actors.length; + address sender = actors[senderIdx]; + uint256 privateKey = actorKeys[senderIdx]; + + // Execute an SSTORE which needs state gas — excess tx.gas goes to reservoir + bytes32 slot = keccak256(abi.encode(actorSeed, slotCounter++, "overflow")); + bytes memory callData = abi.encodeCall(TIP1016Storage.storeValue, (slot, 1)); + uint64 nonce = uint64(vm.getNonce(sender)); + + // tx.gas above the limit, but the excess covers state gas + extraGas = bound(extraGas, SSTORE_STATE_GAS, SSTORE_STATE_GAS + 1_000_000); + uint64 gasLimit = uint64(MAX_TX_GAS_LIMIT + extraGas); + + bytes memory signedTx = TxBuilder.buildLegacyCallWithGas( + vmRlp, vm, address(storageContract), callData, nonce, gasLimit, privateKey + ); + + vm.coinbase(validator); + + try vmExec.executeTransaction(signedTx) { + if (storageContract.getValue(slot) != 0) { + ghost_res3Succeeded++; + } else { + ghost_res3Violations++; + } + ghost_protocolNonce[sender]++; + ghost_totalTxExecuted++; + } catch { + ghost_res3Violations++; + ghost_totalTxReverted++; + } + } + + /// @notice Handler: Mixed workload — state-heavy + regular txs coexist (TIP1016-BLK3) + /// @param actorSeed Seed for selecting actor + /// @param slotSeed Seed for slot generation + function handler_mixedWorkload(uint256 actorSeed, uint256 slotSeed) external { + if (!isTempo) return; + + ghost_blk3Tests++; + + uint256 senderIdx = actorSeed % actors.length; + address sender = actors[senderIdx]; + uint256 privateKey = actorKeys[senderIdx]; + + // Tx 1: State-heavy (SSTORE 0→NZ) + bytes32 slot = keccak256(abi.encode(slotSeed, slotCounter++, "mixed-state")); + bytes memory stateCallData = abi.encodeCall(TIP1016Storage.storeValue, (slot, 1)); + uint64 nonce = uint64(vm.getNonce(sender)); + + uint64 stateGas = uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_SET_GAS + GAS_TOLERANCE); + bytes memory stateTx = TxBuilder.buildLegacyCallWithGas( + vmRlp, vm, address(storageContract), stateCallData, nonce, stateGas, privateKey + ); + + vm.coinbase(validator); + + bool stateSucceeded = false; + try vmExec.executeTransaction(stateTx) { + if (storageContract.getValue(slot) != 0) { + ghost_blk3StateHeavySucceeded++; + stateSucceeded = true; + } + ghost_protocolNonce[sender]++; + } catch { } + + // Tx 2: Regular (simple transfer — no state gas) + nonce = uint64(vm.getNonce(sender)); + uint256 recipientIdx = (senderIdx + 1) % actors.length; + bytes memory transferData = abi.encodeCall(ITIP20.transfer, (actors[recipientIdx], 1e6)); + + uint64 regularGas = uint64(BASE_TX_GAS + CALL_OVERHEAD + GAS_TOLERANCE); + bytes memory regularTx = TxBuilder.buildLegacyCallWithGas( + vmRlp, vm, address(feeToken), transferData, nonce, regularGas, privateKey + ); + + bool regularSucceeded = false; + try vmExec.executeTransaction(regularTx) { + ghost_blk3RegularSucceeded++; + regularSucceeded = true; + ghost_protocolNonce[sender]++; + ghost_totalTxExecuted++; + } catch { + ghost_totalTxReverted++; + } + + // State-heavy tx should NOT prevent the regular tx from succeeding + if (stateSucceeded && !regularSucceeded) { + ghost_blk3Violations++; + } + } + + /// @notice Handler: SSTORE 0→X→0 refund (TIP1016-REF1, REF2) + /// @param actorSeed Seed for selecting actor + /// @param valueSeed Seed for the intermediate value + function handler_sstoreSetAndClear(uint256 actorSeed, uint256 valueSeed) external { + if (!isTempo) return; + + ghost_ref1Tests++; + + uint256 senderIdx = actorSeed % actors.length; + address sender = actors[senderIdx]; + uint256 privateKey = actorKeys[senderIdx]; + + uint256 value = bound(valueSeed, 1, type(uint256).max); + bytes32 slot = keccak256(abi.encode(actorSeed, slotCounter++, "refund")); + + // storeAndClear: stores value then clears it in the same call (0→X→0) + bytes memory callData = abi.encodeCall(TIP1016Storage.storeAndClear, (slot, value)); + uint64 nonce = uint64(vm.getNonce(sender)); + + // Gas needs: SSTORE 0→NZ (250k) + SSTORE NZ→0 (refund) + overhead + // After refund the net cost should be ~GAS_WARM_ACCESS (100) + // But we need enough upfront for the full SSTORE before the refund + uint64 gasLimit = + uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_SET_GAS + SSTORE_HOT_GAS + GAS_TOLERANCE); + bytes memory signedTx = TxBuilder.buildLegacyCallWithGas( + vmRlp, vm, address(storageContract), callData, nonce, gasLimit, privateKey + ); + + vm.coinbase(validator); + + try vmExec.executeTransaction(signedTx) { + // Slot should be 0 after set-and-clear + if (storageContract.getValue(slot) == 0) { + ghost_ref1Succeeded++; + } else { + ghost_ref1Violations++; + } + ghost_protocolNonce[sender]++; + ghost_ref2Checked++; + ghost_totalTxExecuted++; + } catch { + ghost_ref1Violations++; + ghost_totalTxReverted++; + } + } + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + function _createInitcodeOfSize(uint256 targetSize) internal pure returns (bytes memory) { + bytes memory initcode = new bytes(14 + targetSize); + + initcode[0] = 0x61; // PUSH2 + initcode[1] = bytes1(uint8(targetSize >> 8)); + initcode[2] = bytes1(uint8(targetSize)); + initcode[3] = 0x60; // PUSH1 + initcode[4] = 0x0e; + initcode[5] = 0x60; // PUSH1 + initcode[6] = 0x00; + initcode[7] = 0x39; // CODECOPY + initcode[8] = 0x61; // PUSH2 + initcode[9] = bytes1(uint8(targetSize >> 8)); + initcode[10] = bytes1(uint8(targetSize)); + initcode[11] = 0x60; // PUSH1 + initcode[12] = 0x00; + initcode[13] = 0xf3; // RETURN + + return initcode; + } + +} + +/*////////////////////////////////////////////////////////////// + HELPER CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @title TIP1016Storage - Contract for testing TIP-1016 gas semantics +contract TIP1016Storage { + + mapping(bytes32 => uint256) private _storage; + + function storeValue(bytes32 slot, uint256 value) external { + _storage[slot] = value; + } + + function storeMultiple(bytes32[] calldata slots) external { + for (uint256 i = 0; i < slots.length; i++) { + _storage[slots[i]] = 1; + } + } + + function storeAndClear(bytes32 slot, uint256 value) external { + _storage[slot] = value; + _storage[slot] = 0; + } + + function getValue(bytes32 slot) external view returns (uint256) { + return _storage[slot]; + } + +} + +/// @title GasLeftChecker - Contract that records gasleft() for RES1 testing +contract GasLeftChecker { + + uint256 public lastGasLeft; + + function checkGasLeft() external { + lastGasLeft = gasleft(); + } + +}