diff --git a/packages/testing/src/execution_testing/__init__.py b/packages/testing/src/execution_testing/__init__.py index e8a5b2e3211..a583154b4f4 100644 --- a/packages/testing/src/execution_testing/__init__.py +++ b/packages/testing/src/execution_testing/__init__.py @@ -106,6 +106,7 @@ gas_test, generate_system_contract_deploy_test, generate_system_contract_error_test, + oog_test, ) from .vm import ( Bytecode, @@ -226,4 +227,5 @@ "generate_system_contract_deploy_test", "generate_system_contract_error_test", "keccak256", + "oog_test", ) diff --git a/packages/testing/src/execution_testing/fixtures/pre_alloc_groups.py b/packages/testing/src/execution_testing/fixtures/pre_alloc_groups.py index a34a72781ac..c45f483d72b 100644 --- a/packages/testing/src/execution_testing/fixtures/pre_alloc_groups.py +++ b/packages/testing/src/execution_testing/fixtures/pre_alloc_groups.py @@ -55,7 +55,7 @@ def calculate_genesis(self) -> FixtureHeader: return FixtureHeader.genesis( self.fork.transitions_from(), self.environment, - self.pre.state_root(), + self.pre.state_root(self.fork), ) def add_test_alloc(self, test_id: str, new_pre: Alloc) -> None: @@ -272,11 +272,11 @@ class GroupPreAlloc(Alloc): _cached_state_root: Hash | None = PrivateAttr(None) _model_dump_cache: ModelDumpCache | None = PrivateAttr(None) - def state_root(self) -> Hash: + def state_root(self, fork: Any = None) -> Hash: """On pre-alloc groups, which are normally very big, always cache.""" if self._cached_state_root is not None: return self._cached_state_root - return super().state_root() + return super().state_root(fork) def model_dump( # type: ignore[override] self, mode: Literal["json", "python"], **kwargs: Any diff --git a/packages/testing/src/execution_testing/forks/__init__.py b/packages/testing/src/execution_testing/forks/__init__.py index 6853f2e7697..1041e1157a3 100644 --- a/packages/testing/src/execution_testing/forks/__init__.py +++ b/packages/testing/src/execution_testing/forks/__init__.py @@ -8,6 +8,7 @@ BPO4, BPO5, MONAD_EIGHT, + MONAD_NEXT, MONAD_NINE, Amsterdam, ArrowGlacier, @@ -37,6 +38,7 @@ BPO3ToBPO4AtTime15k, CancunToPragueAtTime15k, MONAD_EIGHTToMONAD_NINEAtTime15k, + MONAD_NINEToMONAD_NEXTAtTime15k, OsakaToBPO1AtTime15k, ParisToShanghaiAtTime15k, PragueToMONAD_EIGHTAtTime15k, @@ -124,6 +126,8 @@ "MONAD_EIGHT", "MONAD_EIGHTToMONAD_NINEAtTime15k", "MONAD_NINE", + "MONAD_NINEToMONAD_NEXTAtTime15k", + "MONAD_NEXT", "BPO1", "BPO1ToBPO2AtTime15k", "BPO2", diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index aa8904c71a7..fd0a848f10d 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1666,6 +1666,94 @@ def c(w: int) -> int: return fn +class MONAD_NEXT(MONAD_NINE, solc_name="cancun"): # noqa: N801 + """MONAD_NEXT fork.""" + + @classmethod + def gas_costs(cls) -> GasCosts: + """Return gas costs with MIP-8 page-based storage constants.""" + return replace( + MONAD_NINE.gas_costs(), + PAGE_BASE_COST=100, + PAGE_LOAD_COST=8_000, + PAGE_WRITE_COST=2_800, + PAGE_STATE_GROWTH_COST=17_000, + ) + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Override SLOAD/SSTORE gas with MIP-8 page-level rules.""" + base_map = super().opcode_gas_map() + gas_costs = cls.gas_costs() + return { + **base_map, + Opcodes.SLOAD: lambda op: ( + gas_costs.PAGE_BASE_COST + if op.metadata["page_load_warm"] or op.metadata["key_warm"] + else gas_costs.PAGE_LOAD_COST + gas_costs.PAGE_BASE_COST + ), + Opcodes.SSTORE: lambda op: cls._calculate_sstore_gas_mip8( + op, gas_costs + ), + } + + @classmethod + def opcode_refund_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """MIP-8 removes SSTORE refunds.""" + return {} + + @classmethod + def _calculate_sstore_gas_mip8( + cls, opcode: OpcodeBase, gas_costs: GasCosts + ) -> int: + """ + Calculate SSTORE gas cost per MIP-8. + + Metadata fields used: + - current_value: value right before this SSTORE (v_original + in spec terms — `None` falls back to original_value) + - new_value: value being written + - page_load_warm: page already in read_accessed_pages + - page_write_warm: page already in write_accessed_pages + - key_warm: any prior SLOAD/SSTORE on this key — implies + page_load_warm, but for prior SSTORE will still misprice + - current_state_growth: per-page counter before this op + - net_state_growth: per-page peak before this op + """ + metadata = opcode.metadata + + v_original = metadata["current_value"] + if v_original is None: + v_original = metadata["original_value"] + v_new = metadata["new_value"] + + gas_cost = gas_costs.PAGE_BASE_COST + + page_load_warm = metadata["page_load_warm"] or metadata["key_warm"] + + if not page_load_warm: + gas_cost += gas_costs.PAGE_LOAD_COST + + if v_original != v_new: + if not metadata["page_write_warm"]: + gas_cost += gas_costs.PAGE_WRITE_COST + + current = metadata["current_state_growth"] + peak = metadata["net_state_growth"] + if v_original == 0 and v_new != 0: + current += 1 + elif v_original != 0 and v_new == 0: + current -= 1 + if current > peak: + gas_cost += gas_costs.PAGE_STATE_GROWTH_COST + + return gas_cost + + class BPO1( Osaka, bpo_fork=True, diff --git a/packages/testing/src/execution_testing/forks/forks/transition.py b/packages/testing/src/execution_testing/forks/forks/transition.py index 534b2dc9f9d..6500b48b788 100644 --- a/packages/testing/src/execution_testing/forks/forks/transition.py +++ b/packages/testing/src/execution_testing/forks/forks/transition.py @@ -7,6 +7,7 @@ BPO3, BPO4, MONAD_EIGHT, + MONAD_NEXT, MONAD_NINE, Amsterdam, Berlin, @@ -71,6 +72,13 @@ class MONAD_EIGHTToMONAD_NINEAtTime15k(TransitionBaseClass): # noqa: N801 pass +@transition_fork(to_fork=MONAD_NEXT, from_fork=MONAD_NINE, at_timestamp=15_000) +class MONAD_NINEToMONAD_NEXTAtTime15k(TransitionBaseClass): # noqa: N801 + """MONAD_NINE to MONAD_NEXT transition at Timestamp 15k.""" + + pass + + @transition_fork(to_fork=BPO1, from_fork=Osaka, at_timestamp=15_000) class OsakaToBPO1AtTime15k(TransitionBaseClass): """Osaka to BPO1 transition at Timestamp 15k.""" diff --git a/packages/testing/src/execution_testing/forks/gas_costs.py b/packages/testing/src/execution_testing/forks/gas_costs.py index fcb8148cca0..6b6dbdef7c1 100644 --- a/packages/testing/src/execution_testing/forks/gas_costs.py +++ b/packages/testing/src/execution_testing/forks/gas_costs.py @@ -146,3 +146,9 @@ class GasCosts: OPCODE_BLOBHASH: int = 0 OPCODE_MCOPY_BASE: int = 0 OPCODE_CLZ: int = 0 + + # MIP-8 page-based storage gas constants + PAGE_BASE_COST: int = 0 + PAGE_LOAD_COST: int = 0 + PAGE_WRITE_COST: int = 0 + PAGE_STATE_GROWTH_COST: int = 0 diff --git a/packages/testing/src/execution_testing/forks/tests/blake3_test_vectors.json b/packages/testing/src/execution_testing/forks/tests/blake3_test_vectors.json new file mode 100644 index 00000000000..f6da91792c6 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/tests/blake3_test_vectors.json @@ -0,0 +1,217 @@ +{ + "_comment": "Each test is an input length and three outputs, one for each of the hash, keyed_hash, and derive_key modes. The input in each case is filled with a repeating sequence of 251 bytes: 0, 1, 2, ..., 249, 250, 0, 1, ..., and so on. The key used with keyed_hash is the 32-byte ASCII string \"whats the Elvish word for friend\", also given in the `key` field below. The context string used with derive_key is the ASCII string \"BLAKE3 2019-12-27 16:29:52 test vectors context\", also given in the `context_string` field below. Outputs are encoded as hexadecimal. Each case is an extended output, and implementations should also check that the first 32 bytes match their default-length output.", + "key": "whats the Elvish word for friend", + "context_string": "BLAKE3 2019-12-27 16:29:52 test vectors context", + "cases": [ + { + "input_len": 0, + "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d", + "keyed_hash": "92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26b18171a2f22a4b94822c701f107153dba24918c4bae4d2945c20ece13387627d3b73cbf97b797d5e59948c7ef788f54372df45e45e4293c7dc18c1d41144a9758be58960856be1eabbe22c2653190de560ca3b2ac4aa692a9210694254c371e851bc8f", + "derive_key": "2cc39783c223154fea8dfb7c1b1660f2ac2dcbd1c1de8277b0b0dd39b7e50d7d905630c8be290dfcf3e6842f13bddd573c098c3f17361f1f206b8cad9d088aa4a3f746752c6b0ce6a83b0da81d59649257cdf8eb3e9f7d4998e41021fac119deefb896224ac99f860011f73609e6e0e4540f93b273e56547dfd3aa1a035ba6689d89a0" + }, + { + "input_len": 1, + "hash": "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213c3a6cb8bf623e20cdb535f8d1a5ffb86342d9c0b64aca3bce1d31f60adfa137b358ad4d79f97b47c3d5e79f179df87a3b9776ef8325f8329886ba42f07fb138bb502f4081cbcec3195c5871e6c23e2cc97d3c69a613eba131e5f1351f3f1da786545e5", + "keyed_hash": "6d7878dfff2f485635d39013278ae14f1454b8c0a3a2d34bc1ab38228a80c95b6568c0490609413006fbd428eb3fd14e7756d90f73a4725fad147f7bf70fd61c4e0cf7074885e92b0e3f125978b4154986d4fb202a3f331a3fb6cf349a3a70e49990f98fe4289761c8602c4e6ab1138d31d3b62218078b2f3ba9a88e1d08d0dd4cea11", + "derive_key": "b3e2e340a117a499c6cf2398a19ee0d29cca2bb7404c73063382693bf66cb06c5827b91bf889b6b97c5477f535361caefca0b5d8c4746441c57617111933158950670f9aa8a05d791daae10ac683cbef8faf897c84e6114a59d2173c3f417023a35d6983f2c7dfa57e7fc559ad751dbfb9ffab39c2ef8c4aafebc9ae973a64f0c76551" + }, + { + "input_len": 2, + "hash": "7b7015bb92cf0b318037702a6cdd81dee41224f734684c2c122cd6359cb1ee63d8386b22e2ddc05836b7c1bb693d92af006deb5ffbc4c70fb44d0195d0c6f252faac61659ef86523aa16517f87cb5f1340e723756ab65efb2f91964e14391de2a432263a6faf1d146937b35a33621c12d00be8223a7f1919cec0acd12097ff3ab00ab1", + "keyed_hash": "5392ddae0e0a69d5f40160462cbd9bd889375082ff224ac9c758802b7a6fd20a9ffbf7efd13e989a6c246f96d3a96b9d279f2c4e63fb0bdff633957acf50ee1a5f658be144bab0f6f16500dee4aa5967fc2c586d85a04caddec90fffb7633f46a60786024353b9e5cebe277fcd9514217fee2267dcda8f7b31697b7c54fab6a939bf8f", + "derive_key": "1f166565a7df0098ee65922d7fea425fb18b9943f19d6161e2d17939356168e6daa59cae19892b2d54f6fc9f475d26031fd1c22ae0a3e8ef7bdb23f452a15e0027629d2e867b1bb1e6ab21c71297377750826c404dfccc2406bd57a83775f89e0b075e59a7732326715ef912078e213944f490ad68037557518b79c0086de6d6f6cdd2" + }, + { + "input_len": 3, + "hash": "e1be4d7a8ab5560aa4199eea339849ba8e293d55ca0a81006726d184519e647f5b49b82f805a538c68915c1ae8035c900fd1d4b13902920fd05e1450822f36de9454b7e9996de4900c8e723512883f93f4345f8a58bfe64ee38d3ad71ab027765d25cdd0e448328a8e7a683b9a6af8b0af94fa09010d9186890b096a08471e4230a134", + "keyed_hash": "39e67b76b5a007d4921969779fe666da67b5213b096084ab674742f0d5ec62b9b9142d0fab08e1b161efdbb28d18afc64d8f72160c958e53a950cdecf91c1a1bbab1a9c0f01def762a77e2e8545d4dec241e98a89b6db2e9a5b070fc110caae2622690bd7b76c02ab60750a3ea75426a6bb8803c370ffe465f07fb57def95df772c39f", + "derive_key": "440aba35cb006b61fc17c0529255de438efc06a8c9ebf3f2ddac3b5a86705797f27e2e914574f4d87ec04c379e12789eccbfbc15892626042707802dbe4e97c3ff59dca80c1e54246b6d055154f7348a39b7d098b2b4824ebe90e104e763b2a447512132cede16243484a55a4e40a85790038bb0dcf762e8c053cabae41bbe22a5bff7" + }, + { + "input_len": 4, + "hash": "f30f5ab28fe047904037f77b6da4fea1e27241c5d132638d8bedce9d40494f328f603ba4564453e06cdcee6cbe728a4519bbe6f0d41e8a14b5b225174a566dbfa61b56afb1e452dc08c804f8c3143c9e2cc4a31bb738bf8c1917b55830c6e65797211701dc0b98daa1faeaa6ee9e56ab606ce03a1a881e8f14e87a4acf4646272cfd12", + "keyed_hash": "7671dde590c95d5ac9616651ff5aa0a27bee5913a348e053b8aa9108917fe070116c0acff3f0d1fa97ab38d813fd46506089118147d83393019b068a55d646251ecf81105f798d76a10ae413f3d925787d6216a7eb444e510fd56916f1d753a5544ecf0072134a146b2615b42f50c179f56b8fae0788008e3e27c67482349e249cb86a", + "derive_key": "f46085c8190d69022369ce1a18880e9b369c135eb93f3c63550d3e7630e91060fbd7d8f4258bec9da4e05044f88b91944f7cab317a2f0c18279629a3867fad0662c9ad4d42c6f27e5b124da17c8c4f3a94a025ba5d1b623686c6099d202a7317a82e3d95dae46a87de0555d727a5df55de44dab799a20dffe239594d6e99ed17950910" + }, + { + "input_len": 5, + "hash": "b40b44dfd97e7a84a996a91af8b85188c66c126940ba7aad2e7ae6b385402aa2ebcfdac6c5d32c31209e1f81a454751280db64942ce395104e1e4eaca62607de1c2ca748251754ea5bbe8c20150e7f47efd57012c63b3c6a6632dc1c7cd15f3e1c999904037d60fac2eb9397f2adbe458d7f264e64f1e73aa927b30988e2aed2f03620", + "keyed_hash": "73ac69eecf286894d8102018a6fc729f4b1f4247d3703f69bdc6a5fe3e0c84616ab199d1f2f3e53bffb17f0a2209fe8b4f7d4c7bae59c2bc7d01f1ff94c67588cc6b38fa6024886f2c078bfe09b5d9e6584cd6c521c3bb52f4de7687b37117a2dbbec0d59e92fa9a8cc3240d4432f91757aabcae03e87431dac003e7d73574bfdd8218", + "derive_key": "1f24eda69dbcb752847ec3ebb5dd42836d86e58500c7c98d906ecd82ed9ae47f6f48a3f67e4e43329c9a89b1ca526b9b35cbf7d25c1e353baffb590fd79be58ddb6c711f1a6b60e98620b851c688670412fcb0435657ba6b638d21f0f2a04f2f6b0bd8834837b10e438d5f4c7c2c71299cf7586ea9144ed09253d51f8f54dd6bff719d" + }, + { + "input_len": 6, + "hash": "06c4e8ffb6872fad96f9aaca5eee1553eb62aed0ad7198cef42e87f6a616c844611a30c4e4f37fe2fe23c0883cde5cf7059d88b657c7ed2087e3d210925ede716435d6d5d82597a1e52b9553919e804f5656278bd739880692c94bff2824d8e0b48cac1d24682699e4883389dc4f2faa2eb3b4db6e39debd5061ff3609916f3e07529a", + "keyed_hash": "82d3199d0013035682cc7f2a399d4c212544376a839aa863a0f4c91220ca7a6dc2ffb3aa05f2631f0fa9ac19b6e97eb7e6669e5ec254799350c8b8d189e8807800842a5383c4d907c932f34490aaf00064de8cdb157357bde37c1504d2960034930887603abc5ccb9f5247f79224baff6120a3c622a46d7b1bcaee02c5025460941256", + "derive_key": "be96b30b37919fe4379dfbe752ae77b4f7e2ab92f7ff27435f76f2f065f6a5f435ae01a1d14bd5a6b3b69d8cbd35f0b01ef2173ff6f9b640ca0bd4748efa398bf9a9c0acd6a66d9332fdc9b47ffe28ba7ab6090c26747b85f4fab22f936b71eb3f64613d8bd9dfabe9bb68da19de78321b481e5297df9e40ec8a3d662f3e1479c65de0" + }, + { + "input_len": 7, + "hash": "3f8770f387faad08faa9d8414e9f449ac68e6ff0417f673f602a646a891419fe66036ef6e6d1a8f54baa9fed1fc11c77cfb9cff65bae915045027046ebe0c01bf5a941f3bb0f73791d3fc0b84370f9f30af0cd5b0fc334dd61f70feb60dad785f070fef1f343ed933b49a5ca0d16a503f599a365a4296739248b28d1a20b0e2cc8975c", + "keyed_hash": "af0a7ec382aedc0cfd626e49e7628bc7a353a4cb108855541a5651bf64fbb28a7c5035ba0f48a9c73dabb2be0533d02e8fd5d0d5639a18b2803ba6bf527e1d145d5fd6406c437b79bcaad6c7bdf1cf4bd56a893c3eb9510335a7a798548c6753f74617bede88bef924ba4b334f8852476d90b26c5dc4c3668a2519266a562c6c8034a6", + "derive_key": "dc3b6485f9d94935329442916b0d059685ba815a1fa2a14107217453a7fc9f0e66266db2ea7c96843f9d8208e600a73f7f45b2f55b9e6d6a7ccf05daae63a3fdd10b25ac0bd2e224ce8291f88c05976d575df998477db86fb2cfbbf91725d62cb57acfeb3c2d973b89b503c2b60dde85a7802b69dc1ac2007d5623cbea8cbfb6b181f5" + }, + { + "input_len": 8, + "hash": "2351207d04fc16ade43ccab08600939c7c1fa70a5c0aaca76063d04c3228eaeb725d6d46ceed8f785ab9f2f9b06acfe398c6699c6129da084cb531177445a682894f9685eaf836999221d17c9a64a3a057000524cd2823986db378b074290a1a9b93a22e135ed2c14c7e20c6d045cd00b903400374126676ea78874d79f2dd7883cf5c", + "keyed_hash": "be2f5495c61cba1bb348a34948c004045e3bd4dae8f0fe82bf44d0da245a060048eb5e68ce6dea1eb0229e144f578b3aa7e9f4f85febd135df8525e6fe40c6f0340d13dd09b255ccd5112a94238f2be3c0b5b7ecde06580426a93e0708555a265305abf86d874e34b4995b788e37a823491f25127a502fe0704baa6bfdf04e76c13276", + "derive_key": "2b166978cef14d9d438046c720519d8b1cad707e199746f1562d0c87fbd32940f0e2545a96693a66654225ebbaac76d093bfa9cd8f525a53acb92a861a98c42e7d1c4ae82e68ab691d510012edd2a728f98cd4794ef757e94d6546961b4f280a51aac339cc95b64a92b83cc3f26d8af8dfb4c091c240acdb4d47728d23e7148720ef04" + }, + { + "input_len": 63, + "hash": "e9bc37a594daad83be9470df7f7b3798297c3d834ce80ba85d6e207627b7db7b1197012b1e7d9af4d7cb7bdd1f3bb49a90a9b5dec3ea2bbc6eaebce77f4e470cbf4687093b5352f04e4a4570fba233164e6acc36900e35d185886a827f7ea9bdc1e5c3ce88b095a200e62c10c043b3e9bc6cb9b6ac4dfa51794b02ace9f98779040755", + "keyed_hash": "bb1eb5d4afa793c1ebdd9fb08def6c36d10096986ae0cfe148cd101170ce37aea05a63d74a840aecd514f654f080e51ac50fd617d22610d91780fe6b07a26b0847abb38291058c97474ef6ddd190d30fc318185c09ca1589d2024f0a6f16d45f11678377483fa5c005b2a107cb9943e5da634e7046855eaa888663de55d6471371d55d", + "derive_key": "b6451e30b953c206e34644c6803724e9d2725e0893039cfc49584f991f451af3b89e8ff572d3da4f4022199b9563b9d70ebb616efff0763e9abec71b550f1371e233319c4c4e74da936ba8e5bbb29a598e007a0bbfa929c99738ca2cc098d59134d11ff300c39f82e2fce9f7f0fa266459503f64ab9913befc65fddc474f6dc1c67669" + }, + { + "input_len": 64, + "hash": "4eed7141ea4a5cd4b788606bd23f46e212af9cacebacdc7d1f4c6dc7f2511b98fc9cc56cb831ffe33ea8e7e1d1df09b26efd2767670066aa82d023b1dfe8ab1b2b7fbb5b97592d46ffe3e05a6a9b592e2949c74160e4674301bc3f97e04903f8c6cf95b863174c33228924cdef7ae47559b10b294acd660666c4538833582b43f82d74", + "keyed_hash": "ba8ced36f327700d213f120b1a207a3b8c04330528586f414d09f2f7d9ccb7e68244c26010afc3f762615bbac552a1ca909e67c83e2fd5478cf46b9e811efccc93f77a21b17a152ebaca1695733fdb086e23cd0eb48c41c034d52523fc21236e5d8c9255306e48d52ba40b4dac24256460d56573d1312319afcf3ed39d72d0bfc69acb", + "derive_key": "a5c4a7053fa86b64746d4bb688d06ad1f02a18fce9afd3e818fefaa7126bf73e9b9493a9befebe0bf0c9509fb3105cfa0e262cde141aa8e3f2c2f77890bb64a4cca96922a21ead111f6338ad5244f2c15c44cb595443ac2ac294231e31be4a4307d0a91e874d36fc9852aeb1265c09b6e0cda7c37ef686fbbcab97e8ff66718be048bb" + }, + { + "input_len": 65, + "hash": "de1e5fa0be70df6d2be8fffd0e99ceaa8eb6e8c93a63f2d8d1c30ecb6b263dee0e16e0a4749d6811dd1d6d1265c29729b1b75a9ac346cf93f0e1d7296dfcfd4313b3a227faaaaf7757cc95b4e87a49be3b8a270a12020233509b1c3632b3485eef309d0abc4a4a696c9decc6e90454b53b000f456a3f10079072baaf7a981653221f2c", + "keyed_hash": "c0a4edefa2d2accb9277c371ac12fcdbb52988a86edc54f0716e1591b4326e72d5e795f46a596b02d3d4bfb43abad1e5d19211152722ec1f20fef2cd413e3c22f2fc5da3d73041275be6ede3517b3b9f0fc67ade5956a672b8b75d96cb43294b9041497de92637ed3f2439225e683910cb3ae923374449ca788fb0f9bea92731bc26ad", + "derive_key": "51fd05c3c1cfbc8ed67d139ad76f5cf8236cd2acd26627a30c104dfd9d3ff8a82b02e8bd36d8498a75ad8c8e9b15eb386970283d6dd42c8ae7911cc592887fdbe26a0a5f0bf821cd92986c60b2502c9be3f98a9c133a7e8045ea867e0828c7252e739321f7c2d65daee4468eb4429efae469a42763f1f94977435d10dccae3e3dce88d" + }, + { + "input_len": 127, + "hash": "d81293fda863f008c09e92fc382a81f5a0b4a1251cba1634016a0f86a6bd640de3137d477156d1fde56b0cf36f8ef18b44b2d79897bece12227539ac9ae0a5119da47644d934d26e74dc316145dcb8bb69ac3f2e05c242dd6ee06484fcb0e956dc44355b452c5e2bbb5e2b66e99f5dd443d0cbcaaafd4beebaed24ae2f8bb672bcef78", + "keyed_hash": "c64200ae7dfaf35577ac5a9521c47863fb71514a3bcad18819218b818de85818ee7a317aaccc1458f78d6f65f3427ec97d9c0adb0d6dacd4471374b621b7b5f35cd54663c64dbe0b9e2d95632f84c611313ea5bd90b71ce97b3cf645776f3adc11e27d135cbadb9875c2bf8d3ae6b02f8a0206aba0c35bfe42574011931c9a255ce6dc", + "derive_key": "c91c090ceee3a3ac81902da31838012625bbcd73fcb92e7d7e56f78deba4f0c3feeb3974306966ccb3e3c69c337ef8a45660ad02526306fd685c88542ad00f759af6dd1adc2e50c2b8aac9f0c5221ff481565cf6455b772515a69463223202e5c371743e35210bbbbabd89651684107fd9fe493c937be16e39cfa7084a36207c99bea3" + }, + { + "input_len": 128, + "hash": "f17e570564b26578c33bb7f44643f539624b05df1a76c81f30acd548c44b45efa69faba091427f9c5c4caa873aa07828651f19c55bad85c47d1368b11c6fd99e47ecba5820a0325984d74fe3e4058494ca12e3f1d3293d0010a9722f7dee64f71246f75e9361f44cc8e214a100650db1313ff76a9f93ec6e84edb7add1cb4a95019b0c", + "keyed_hash": "b04fe15577457267ff3b6f3c947d93be581e7e3a4b018679125eaf86f6a628ecd86bbe0001f10bda47e6077b735016fca8119da11348d93ca302bbd125bde0db2b50edbe728a620bb9d3e6f706286aedea973425c0b9eedf8a38873544cf91badf49ad92a635a93f71ddfcee1eae536c25d1b270956be16588ef1cfef2f1d15f650bd5", + "derive_key": "81720f34452f58a0120a58b6b4608384b5c51d11f39ce97161a0c0e442ca022550e7cd651e312f0b4c6afb3c348ae5dd17d2b29fab3b894d9a0034c7b04fd9190cbd90043ff65d1657bbc05bfdecf2897dd894c7a1b54656d59a50b51190a9da44db426266ad6ce7c173a8c0bbe091b75e734b4dadb59b2861cd2518b4e7591e4b83c9" + }, + { + "input_len": 129, + "hash": "683aaae9f3c5ba37eaaf072aed0f9e30bac0865137bae68b1fde4ca2aebdcb12f96ffa7b36dd78ba321be7e842d364a62a42e3746681c8bace18a4a8a79649285c7127bf8febf125be9de39586d251f0d41da20980b70d35e3dac0eee59e468a894fa7e6a07129aaad09855f6ad4801512a116ba2b7841e6cfc99ad77594a8f2d181a7", + "keyed_hash": "d4a64dae6cdccbac1e5287f54f17c5f985105457c1a2ec1878ebd4b57e20d38f1c9db018541eec241b748f87725665b7b1ace3e0065b29c3bcb232c90e37897fa5aaee7e1e8a2ecfcd9b51463e42238cfdd7fee1aecb3267fa7f2128079176132a412cd8aaf0791276f6b98ff67359bd8652ef3a203976d5ff1cd41885573487bcd683", + "derive_key": "938d2d4435be30eafdbb2b7031f7857c98b04881227391dc40db3c7b21f41fc18d72d0f9c1de5760e1941aebf3100b51d64644cb459eb5d20258e233892805eb98b07570ef2a1787cd48e117c8d6a63a68fd8fc8e59e79dbe63129e88352865721c8d5f0cf183f85e0609860472b0d6087cefdd186d984b21542c1c780684ed6832d8d" + }, + { + "input_len": 1023, + "hash": "10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11a182d27a591b05592b15607500e1e8dd56bc6c7fc063715b7a1d737df5bad3339c56778957d870eb9717b57ea3d9fb68d1b55127bba6a906a4a24bbd5acb2d123a37b28f9e9a81bbaae360d58f85e5fc9d75f7c370a0cc09b6522d9c8d822f2f28f485", + "keyed_hash": "c951ecdf03288d0fcc96ee3413563d8a6d3589547f2c2fb36d9786470f1b9d6e890316d2e6d8b8c25b0a5b2180f94fb1a158ef508c3cde45e2966bd796a696d3e13efd86259d756387d9becf5c8bf1ce2192b87025152907b6d8cc33d17826d8b7b9bc97e38c3c85108ef09f013e01c229c20a83d9e8efac5b37470da28575fd755a10", + "derive_key": "74a16c1c3d44368a86e1ca6df64be6a2f64cce8f09220787450722d85725dea59c413264404661e9e4d955409dfe4ad3aa487871bcd454ed12abfe2c2b1eb7757588cf6cb18d2eccad49e018c0d0fec323bec82bf1644c6325717d13ea712e6840d3e6e730d35553f59eff5377a9c350bcc1556694b924b858f329c44ee64b884ef00d" + }, + { + "input_len": 1024, + "hash": "42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af71cf8107265ecdaf8505b95d8fcec83a98a6a96ea5109d2c179c47a387ffbb404756f6eeae7883b446b70ebb144527c2075ab8ab204c0086bb22b7c93d465efc57f8d917f0b385c6df265e77003b85102967486ed57db5c5ca170ba441427ed9afa684e", + "keyed_hash": "75c46f6f3d9eb4f55ecaaee480db732e6c2105546f1e675003687c31719c7ba4a78bc838c72852d4f49c864acb7adafe2478e824afe51c8919d06168414c265f298a8094b1ad813a9b8614acabac321f24ce61c5a5346eb519520d38ecc43e89b5000236df0597243e4d2493fd626730e2ba17ac4d8824d09d1a4a8f57b8227778e2de", + "derive_key": "7356cd7720d5b66b6d0697eb3177d9f8d73a4a5c5e968896eb6a6896843027066c23b601d3ddfb391e90d5c8eccdef4ae2a264bce9e612ba15e2bc9d654af1481b2e75dbabe615974f1070bba84d56853265a34330b4766f8e75edd1f4a1650476c10802f22b64bd3919d246ba20a17558bc51c199efdec67e80a227251808d8ce5bad" + }, + { + "input_len": 1025, + "hash": "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444f4c4a22b4b399155358a994e52bf255de60035742ec71bd08ac275a1b51cc6bfe332b0ef84b409108cda080e6269ed4b3e2c3f7d722aa4cdc98d16deb554e5627be8f955c98e1d5f9565a9194cad0c4285f93700062d9595adb992ae68ff12800ab67a", + "keyed_hash": "357dc55de0c7e382c900fd6e320acc04146be01db6a8ce7210b7189bd664ea69362396b77fdc0d2634a552970843722066c3c15902ae5097e00ff53f1e116f1cd5352720113a837ab2452cafbde4d54085d9cf5d21ca613071551b25d52e69d6c81123872b6f19cd3bc1333edf0c52b94de23ba772cf82636cff4542540a7738d5b930", + "derive_key": "effaa245f065fbf82ac186839a249707c3bddf6d3fdda22d1b95a3c970379bcb5d31013a167509e9066273ab6e2123bc835b408b067d88f96addb550d96b6852dad38e320b9d940f86db74d398c770f462118b35d2724efa13da97194491d96dd37c3c09cbef665953f2ee85ec83d88b88d11547a6f911c8217cca46defa2751e7f3ad" + }, + { + "input_len": 2048, + "hash": "e776b6028c7cd22a4d0ba182a8bf62205d2ef576467e838ed6f2529b85fba24a9a60bf80001410ec9eea6698cd537939fad4749edd484cb541aced55cd9bf54764d063f23f6f1e32e12958ba5cfeb1bf618ad094266d4fc3c968c2088f677454c288c67ba0dba337b9d91c7e1ba586dc9a5bc2d5e90c14f53a8863ac75655461cea8f9", + "keyed_hash": "879cf1fa2ea0e79126cb1063617a05b6ad9d0b696d0d757cf053439f60a99dd10173b961cd574288194b23ece278c330fbb8585485e74967f31352a8183aa782b2b22f26cdcadb61eed1a5bc144b8198fbb0c13abbf8e3192c145d0a5c21633b0ef86054f42809df823389ee40811a5910dcbd1018af31c3b43aa55201ed4edaac74fe", + "derive_key": "7b2945cb4fef70885cc5d78a87bf6f6207dd901ff239201351ffac04e1088a23e2c11a1ebffcea4d80447867b61badb1383d842d4e79645d48dd82ccba290769caa7af8eaa1bd78a2a5e6e94fbdab78d9c7b74e894879f6a515257ccf6f95056f4e25390f24f6b35ffbb74b766202569b1d797f2d4bd9d17524c720107f985f4ddc583" + }, + { + "input_len": 2049, + "hash": "5f4d72f40d7a5f82b15ca2b2e44b1de3c2ef86c426c95c1af0b687952256303096de31d71d74103403822a2e0bc1eb193e7aecc9643a76b7bbc0c9f9c52e8783aae98764ca468962b5c2ec92f0c74eb5448d519713e09413719431c802f948dd5d90425a4ecdadece9eb178d80f26efccae630734dff63340285adec2aed3b51073ad3", + "keyed_hash": "9f29700902f7c86e514ddc4df1e3049f258b2472b6dd5267f61bf13983b78dd5f9a88abfefdfa1e00b418971f2b39c64ca621e8eb37fceac57fd0c8fc8e117d43b81447be22d5d8186f8f5919ba6bcc6846bd7d50726c06d245672c2ad4f61702c646499ee1173daa061ffe15bf45a631e2946d616a4c345822f1151284712f76b2b0e", + "derive_key": "2ea477c5515cc3dd606512ee72bb3e0e758cfae7232826f35fb98ca1bcbdf27316d8e9e79081a80b046b60f6a263616f33ca464bd78d79fa18200d06c7fc9bffd808cc4755277a7d5e09da0f29ed150f6537ea9bed946227ff184cc66a72a5f8c1e4bd8b04e81cf40fe6dc4427ad5678311a61f4ffc39d195589bdbc670f63ae70f4b6" + }, + { + "input_len": 3072, + "hash": "b98cb0ff3623be03326b373de6b9095218513e64f1ee2edd2525c7ad1e5cffd29a3f6b0b978d6608335c09dc94ccf682f9951cdfc501bfe47b9c9189a6fc7b404d120258506341a6d802857322fbd20d3e5dae05b95c88793fa83db1cb08e7d8008d1599b6209d78336e24839724c191b2a52a80448306e0daa84a3fdb566661a37e11", + "keyed_hash": "044a0e7b172a312dc02a4c9a818c036ffa2776368d7f528268d2e6b5df19177022f302d0529e4174cc507c463671217975e81dab02b8fdeb0d7ccc7568dd22574c783a76be215441b32e91b9a904be8ea81f7a0afd14bad8ee7c8efc305ace5d3dd61b996febe8da4f56ca0919359a7533216e2999fc87ff7d8f176fbecb3d6f34278b", + "derive_key": "050df97f8c2ead654d9bb3ab8c9178edcd902a32f8495949feadcc1e0480c46b3604131bbd6e3ba573b6dd682fa0a63e5b165d39fc43a625d00207607a2bfeb65ff1d29292152e26b298868e3b87be95d6458f6f2ce6118437b632415abe6ad522874bcd79e4030a5e7bad2efa90a7a7c67e93f0a18fb28369d0a9329ab5c24134ccb0" + }, + { + "input_len": 3073, + "hash": "7124b49501012f81cc7f11ca069ec9226cecb8a2c850cfe644e327d22d3e1cd39a27ae3b79d68d89da9bf25bc27139ae65a324918a5f9b7828181e52cf373c84f35b639b7fccbb985b6f2fa56aea0c18f531203497b8bbd3a07ceb5926f1cab74d14bd66486d9a91eba99059a98bd1cd25876b2af5a76c3e9eed554ed72ea952b603bf", + "keyed_hash": "68dede9bef00ba89e43f31a6825f4cf433389fedae75c04ee9f0cf16a427c95a96d6da3fe985054d3478865be9a092250839a697bbda74e279e8a9e69f0025e4cfddd6cfb434b1cd9543aaf97c635d1b451a4386041e4bb100f5e45407cbbc24fa53ea2de3536ccb329e4eb9466ec37093a42cf62b82903c696a93a50b702c80f3c3c5", + "derive_key": "72613c9ec9ff7e40f8f5c173784c532ad852e827dba2bf85b2ab4b76f7079081576288e552647a9d86481c2cae75c2dd4e7c5195fb9ada1ef50e9c5098c249d743929191441301c69e1f48505a4305ec1778450ee48b8e69dc23a25960fe33070ea549119599760a8a2d28aeca06b8c5e9ba58bc19e11fe57b6ee98aa44b2a8e6b14a5" + }, + { + "input_len": 4096, + "hash": "015094013f57a5277b59d8475c0501042c0b642e531b0a1c8f58d2163229e9690289e9409ddb1b99768eafe1623da896faf7e1114bebeadc1be30829b6f8af707d85c298f4f0ff4d9438aef948335612ae921e76d411c3a9111df62d27eaf871959ae0062b5492a0feb98ef3ed4af277f5395172dbe5c311918ea0074ce0036454f620", + "keyed_hash": "befc660aea2f1718884cd8deb9902811d332f4fc4a38cf7c7300d597a081bfc0bbb64a36edb564e01e4b4aaf3b060092a6b838bea44afebd2deb8298fa562b7b597c757b9df4c911c3ca462e2ac89e9a787357aaf74c3b56d5c07bc93ce899568a3eb17d9250c20f6c5f6c1e792ec9a2dcb715398d5a6ec6d5c54f586a00403a1af1de", + "derive_key": "1e0d7f3db8c414c97c6307cbda6cd27ac3b030949da8e23be1a1a924ad2f25b9d78038f7b198596c6cc4a9ccf93223c08722d684f240ff6569075ed81591fd93f9fff1110b3a75bc67e426012e5588959cc5a4c192173a03c00731cf84544f65a2fb9378989f72e9694a6a394a8a30997c2e67f95a504e631cd2c5f55246024761b245" + }, + { + "input_len": 4097, + "hash": "9b4052b38f1c5fc8b1f9ff7ac7b27cd242487b3d890d15c96a1c25b8aa0fb99505f91b0b5600a11251652eacfa9497b31cd3c409ce2e45cfe6c0a016967316c426bd26f619eab5d70af9a418b845c608840390f361630bd497b1ab44019316357c61dbe091ce72fc16dc340ac3d6e009e050b3adac4b5b2c92e722cffdc46501531956", + "keyed_hash": "00df940cd36bb9fa7cbbc3556744e0dbc8191401afe70520ba292ee3ca80abbc606db4976cfdd266ae0abf667d9481831ff12e0caa268e7d3e57260c0824115a54ce595ccc897786d9dcbf495599cfd90157186a46ec800a6763f1c59e36197e9939e900809f7077c102f888caaf864b253bc41eea812656d46742e4ea42769f89b83f", + "derive_key": "aca51029626b55fda7117b42a7c211f8c6e9ba4fe5b7a8ca922f34299500ead8a897f66a400fed9198fd61dd2d58d382458e64e100128075fc54b860934e8de2e84170734b06e1d212a117100820dbc48292d148afa50567b8b84b1ec336ae10d40c8c975a624996e12de31abbe135d9d159375739c333798a80c64ae895e51e22f3ad" + }, + { + "input_len": 5120, + "hash": "9cadc15fed8b5d854562b26a9536d9707cadeda9b143978f319ab34230535833acc61c8fdc114a2010ce8038c853e121e1544985133fccdd0a2d507e8e615e611e9a0ba4f47915f49e53d721816a9198e8b30f12d20ec3689989175f1bf7a300eee0d9321fad8da232ece6efb8e9fd81b42ad161f6b9550a069e66b11b40487a5f5059", + "keyed_hash": "2c493e48e9b9bf31e0553a22b23503c0a3388f035cece68eb438d22fa1943e209b4dc9209cd80ce7c1f7c9a744658e7e288465717ae6e56d5463d4f80cdb2ef56495f6a4f5487f69749af0c34c2cdfa857f3056bf8d807336a14d7b89bf62bef2fb54f9af6a546f818dc1e98b9e07f8a5834da50fa28fb5874af91bf06020d1bf0120e", + "derive_key": "7a7acac8a02adcf3038d74cdd1d34527de8a0fcc0ee3399d1262397ce5817f6055d0cefd84d9d57fe792d65a278fd20384ac6c30fdb340092f1a74a92ace99c482b28f0fc0ef3b923e56ade20c6dba47e49227166251337d80a037e987ad3a7f728b5ab6dfafd6e2ab1bd583a95d9c895ba9c2422c24ea0f62961f0dca45cad47bfa0d" + }, + { + "input_len": 5121, + "hash": "628bd2cb2004694adaab7bbd778a25df25c47b9d4155a55f8fbd79f2fe154cff96adaab0613a6146cdaabe498c3a94e529d3fc1da2bd08edf54ed64d40dcd6777647eac51d8277d70219a9694334a68bc8f0f23e20b0ff70ada6f844542dfa32cd4204ca1846ef76d811cdb296f65e260227f477aa7aa008bac878f72257484f2b6c95", + "keyed_hash": "6ccf1c34753e7a044db80798ecd0782a8f76f33563accaddbfbb2e0ea4b2d0240d07e63f13667a8d1490e5e04f13eb617aea16a8c8a5aaed1ef6fbde1b0515e3c81050b361af6ead126032998290b563e3caddeaebfab592e155f2e161fb7cba939092133f23f9e65245e58ec23457b78a2e8a125588aad6e07d7f11a85b88d375b72d", + "derive_key": "b07f01e518e702f7ccb44a267e9e112d403a7b3f4883a47ffbed4b48339b3c341a0add0ac032ab5aaea1e4e5b004707ec5681ae0fcbe3796974c0b1cf31a194740c14519273eedaabec832e8a784b6e7cfc2c5952677e6c3f2c3914454082d7eb1ce1766ac7d75a4d3001fc89544dd46b5147382240d689bbbaefc359fb6ae30263165" + }, + { + "input_len": 6144, + "hash": "3e2e5b74e048f3add6d21faab3f83aa44d3b2278afb83b80b3c35164ebeca2054d742022da6fdda444ebc384b04a54c3ac5839b49da7d39f6d8a9db03deab32aade156c1c0311e9b3435cde0ddba0dce7b26a376cad121294b689193508dd63151603c6ddb866ad16c2ee41585d1633a2cea093bea714f4c5d6b903522045b20395c83", + "keyed_hash": "3d6b6d21281d0ade5b2b016ae4034c5dec10ca7e475f90f76eac7138e9bc8f1dc35754060091dc5caf3efabe0603c60f45e415bb3407db67e6beb3d11cf8e4f7907561f05dace0c15807f4b5f389c841eb114d81a82c02a00b57206b1d11fa6e803486b048a5ce87105a686dee041207e095323dfe172df73deb8c9532066d88f9da7e", + "derive_key": "2a95beae63ddce523762355cf4b9c1d8f131465780a391286a5d01abb5683a1597099e3c6488aab6c48f3c15dbe1942d21dbcdc12115d19a8b8465fb54e9053323a9178e4275647f1a9927f6439e52b7031a0b465c861a3fc531527f7758b2b888cf2f20582e9e2c593709c0a44f9c6e0f8b963994882ea4168827823eef1f64169fef" + }, + { + "input_len": 6145, + "hash": "f1323a8631446cc50536a9f705ee5cb619424d46887f3c376c695b70e0f0507f18a2cfdd73c6e39dd75ce7c1c6e3ef238fd54465f053b25d21044ccb2093beb015015532b108313b5829c3621ce324b8e14229091b7c93f32db2e4e63126a377d2a63a3597997d4f1cba59309cb4af240ba70cebff9a23d5e3ff0cdae2cfd54e070022", + "keyed_hash": "9ac301e9e39e45e3250a7e3b3df701aa0fb6889fbd80eeecf28dbc6300fbc539f3c184ca2f59780e27a576c1d1fb9772e99fd17881d02ac7dfd39675aca918453283ed8c3169085ef4a466b91c1649cc341dfdee60e32231fc34c9c4e0b9a2ba87ca8f372589c744c15fd6f985eec15e98136f25beeb4b13c4e43dc84abcc79cd4646c", + "derive_key": "379bcc61d0051dd489f686c13de00d5b14c505245103dc040d9e4dd1facab8e5114493d029bdbd295aaa744a59e31f35c7f52dba9c3642f773dd0b4262a9980a2aef811697e1305d37ba9d8b6d850ef07fe41108993180cf779aeece363704c76483458603bbeeb693cffbbe5588d1f3535dcad888893e53d977424bb707201569a8d2" + }, + { + "input_len": 7168, + "hash": "61da957ec2499a95d6b8023e2b0e604ec7f6b50e80a9678b89d2628e99ada77a5707c321c83361793b9af62a40f43b523df1c8633cecb4cd14d00bdc79c78fca5165b863893f6d38b02ff7236c5a9a8ad2dba87d24c547cab046c29fc5bc1ed142e1de4763613bb162a5a538e6ef05ed05199d751f9eb58d332791b8d73fb74e4fce95", + "keyed_hash": "b42835e40e9d4a7f42ad8cc04f85a963a76e18198377ed84adddeaecacc6f3fca2f01d5277d69bb681c70fa8d36094f73ec06e452c80d2ff2257ed82e7ba348400989a65ee8daa7094ae0933e3d2210ac6395c4af24f91c2b590ef87d7788d7066ea3eaebca4c08a4f14b9a27644f99084c3543711b64a070b94f2c9d1d8a90d035d52", + "derive_key": "11c37a112765370c94a51415d0d651190c288566e295d505defdad895dae223730d5a5175a38841693020669c7638f40b9bc1f9f39cf98bda7a5b54ae24218a800a2116b34665aa95d846d97ea988bfcb53dd9c055d588fa21ba78996776ea6c40bc428b53c62b5f3ccf200f647a5aae8067f0ea1976391fcc72af1945100e2a6dcb88" + }, + { + "input_len": 7169, + "hash": "a003fc7a51754a9b3c7fae0367ab3d782dccf28855a03d435f8cfe74605e781798a8b20534be1ca9eb2ae2df3fae2ea60e48c6fb0b850b1385b5de0fe460dbe9d9f9b0d8db4435da75c601156df9d047f4ede008732eb17adc05d96180f8a73548522840779e6062d643b79478a6e8dbce68927f36ebf676ffa7d72d5f68f050b119c8", + "keyed_hash": "ed9b1a922c046fdb3d423ae34e143b05ca1bf28b710432857bf738bcedbfa5113c9e28d72fcbfc020814ce3f5d4fc867f01c8f5b6caf305b3ea8a8ba2da3ab69fabcb438f19ff11f5378ad4484d75c478de425fb8e6ee809b54eec9bdb184315dc856617c09f5340451bf42fd3270a7b0b6566169f242e533777604c118a6358250f54", + "derive_key": "554b0a5efea9ef183f2f9b931b7497995d9eb26f5c5c6dad2b97d62fc5ac31d99b20652c016d88ba2a611bbd761668d5eda3e568e940faae24b0d9991c3bd25a65f770b89fdcadabcb3d1a9c1cb63e69721cacf1ae69fefdcef1e3ef41bc5312ccc17222199e47a26552c6adc460cf47a72319cb5039369d0060eaea59d6c65130f1dd" + }, + { + "input_len": 8192, + "hash": "aae792484c8efe4f19e2ca7d371d8c467ffb10748d8a5a1ae579948f718a2a635fe51a27db045a567c1ad51be5aa34c01c6651c4d9b5b5ac5d0fd58cf18dd61a47778566b797a8c67df7b1d60b97b19288d2d877bb2df417ace009dcb0241ca1257d62712b6a4043b4ff33f690d849da91ea3bf711ed583cb7b7a7da2839ba71309bbf", + "keyed_hash": "dc9637c8845a770b4cbf76b8daec0eebf7dc2eac11498517f08d44c8fc00d58a4834464159dcbc12a0ba0c6d6eb41bac0ed6585cabfe0aca36a375e6c5480c22afdc40785c170f5a6b8a1107dbee282318d00d915ac9ed1143ad40765ec120042ee121cd2baa36250c618adaf9e27260fda2f94dea8fb6f08c04f8f10c78292aa46102", + "derive_key": "ad01d7ae4ad059b0d33baa3c01319dcf8088094d0359e5fd45d6aeaa8b2d0c3d4c9e58958553513b67f84f8eac653aeeb02ae1d5672dcecf91cd9985a0e67f4501910ecba25555395427ccc7241d70dc21c190e2aadee875e5aae6bf1912837e53411dabf7a56cbf8e4fb780432b0d7fe6cec45024a0788cf5874616407757e9e6bef7" + }, + { + "input_len": 8193, + "hash": "bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3bb2282aa69be089359ea1154b9a9286c4a56af4de975a9aa4a5c497654914d279bea60bb6d2cf7225a2fa0ff5ef56bbe4b149f3ed15860f78b4e2ad04e158e375c1e0c0b551cd7dfc82f1b155c11b6b3ed51ec9edb30d133653bb5709d1dbd55f4e1ff6", + "keyed_hash": "954a2a75420c8d6547e3ba5b98d963e6fa6491addc8c023189cc519821b4a1f5f03228648fd983aef045c2fa8290934b0866b615f585149587dda2299039965328835a2b18f1d63b7e300fc76ff260b571839fe44876a4eae66cbac8c67694411ed7e09df51068a22c6e67d6d3dd2cca8ff12e3275384006c80f4db68023f24eebba57", + "derive_key": "af1e0346e389b17c23200270a64aa4e1ead98c61695d917de7d5b00491c9b0f12f20a01d6d622edf3de026a4db4e4526225debb93c1237934d71c7340bb5916158cbdafe9ac3225476b6ab57a12357db3abbad7a26c6e66290e44034fb08a20a8d0ec264f309994d2810c49cfba6989d7abb095897459f5425adb48aba07c5fb3c83c0" + }, + { + "input_len": 16384, + "hash": "f875d6646de28985646f34ee13be9a576fd515f76b5b0a26bb324735041ddde49d764c270176e53e97bdffa58d549073f2c660be0e81293767ed4e4929f9ad34bbb39a529334c57c4a381ffd2a6d4bfdbf1482651b172aa883cc13408fa67758a3e47503f93f87720a3177325f7823251b85275f64636a8f1d599c2e49722f42e93893", + "keyed_hash": "9e9fc4eb7cf081ea7c47d1807790ed211bfec56aa25bb7037784c13c4b707b0df9e601b101e4cf63a404dfe50f2e1865bb12edc8fca166579ce0c70dba5a5c0fc960ad6f3772183416a00bd29d4c6e651ea7620bb100c9449858bf14e1ddc9ecd35725581ca5b9160de04060045993d972571c3e8f71e9d0496bfa744656861b169d65", + "derive_key": "160e18b5878cd0df1c3af85eb25a0db5344d43a6fbd7a8ef4ed98d0714c3f7e160dc0b1f09caa35f2f417b9ef309dfe5ebd67f4c9507995a531374d099cf8ae317542e885ec6f589378864d3ea98716b3bbb65ef4ab5e0ab5bb298a501f19a41ec19af84a5e6b428ecd813b1a47ed91c9657c3fba11c406bc316768b58f6802c9e9b57" + }, + { + "input_len": 31744, + "hash": "62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47860cc51f2b0c28a7b77304bd55fe73af663c02d3f52ea053ba43431ca5bab7bfea2f5e9d7121770d88f70ae9649ea713087d1914f7f312147e247f87eb2d4ffef0ac978bf7b6579d57d533355aa20b8b77b13fd09748728a5cc327a8ec470f4013226f", + "keyed_hash": "efa53b389ab67c593dba624d898d0f7353ab99e4ac9d42302ee64cbf9939a4193a7258db2d9cd32a7a3ecfce46144114b15c2fcb68a618a976bd74515d47be08b628be420b5e830fade7c080e351a076fbc38641ad80c736c8a18fe3c66ce12f95c61c2462a9770d60d0f77115bbcd3782b593016a4e728d4c06cee4505cb0c08a42ec", + "derive_key": "39772aef80e0ebe60596361e45b061e8f417429d529171b6764468c22928e28e9759adeb797a3fbf771b1bcea30150a020e317982bf0d6e7d14dd9f064bc11025c25f31e81bd78a921db0174f03dd481d30e93fd8e90f8b2fee209f849f2d2a52f31719a490fb0ba7aea1e09814ee912eba111a9fde9d5c274185f7bae8ba85d300a2b" + }, + { + "input_len": 102400, + "hash": "bc3e3d41a1146b069abffad3c0d44860cf664390afce4d9661f7902e7943e085e01c59dab908c04c3342b816941a26d69c2605ebee5ec5291cc55e15b76146e6745f0601156c3596cb75065a9c57f35585a52e1ac70f69131c23d611ce11ee4ab1ec2c009012d236648e77be9295dd0426f29b764d65de58eb7d01dd42248204f45f8e", + "keyed_hash": "1c35d1a5811083fd7119f5d5d1ba027b4d01c0c6c49fb6ff2cf75393ea5db4a7f9dbdd3e1d81dcbca3ba241bb18760f207710b751846faaeb9dff8262710999a59b2aa1aca298a032d94eacfadf1aa192418eb54808db23b56e34213266aa08499a16b354f018fc4967d05f8b9d2ad87a7278337be9693fc638a3bfdbe314574ee6fc4", + "derive_key": "4652cff7a3f385a6103b5c260fc1593e13c778dbe608efb092fe7ee69df6e9c6d83a3e041bc3a48df2879f4a0a3ed40e7c961c73eff740f3117a0504c2dff4786d44fb17f1549eb0ba585e40ec29bf7732f0b7e286ff8acddc4cb1e23b87ff5d824a986458dcc6a04ac83969b80637562953df51ed1a7e90a7926924d2763778be8560" + } + ] +} diff --git a/packages/testing/src/execution_testing/forks/tests/test_blake3.py b/packages/testing/src/execution_testing/forks/tests/test_blake3.py new file mode 100644 index 00000000000..3999abbc021 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/tests/test_blake3.py @@ -0,0 +1,43 @@ +""" +Test BLAKE3 implementation against official test vectors. + +Test vectors from: +https://github.com/BLAKE3-team/BLAKE3/blob/master/test_vectors/test_vectors.json + +The input for each test case is generated as bytes(i % 251 for i in range(N)). +Only the ``hash`` field (unkeyed hash mode) is tested here since that is the +mode used by MIP-8. +""" + +import json +from pathlib import Path + +import pytest +from ethereum.crypto.blake3 import blake3_hash + +VECTORS_PATH = Path(__file__).parent / "blake3_test_vectors.json" + +with open(VECTORS_PATH) as f: + _TEST_DATA = json.load(f) + +_CASES = [(c["input_len"], c["hash"][:64]) for c in _TEST_DATA["cases"]] + + +def _make_input(length: int) -> bytes: + return bytes([i % 251 for i in range(length)]) + + +@pytest.mark.parametrize( + "input_len, expected_hex", + _CASES, + ids=[str(c[0]) for c in _CASES], +) +def test_blake3_hash(input_len: int, expected_hex: str) -> None: + """ + Verify unkeyed BLAKE3 hash matches official test vector. + """ + data = _make_input(input_len) + result = blake3_hash(data).hex() + assert result == expected_hex, ( + f"input_len={input_len}: expected {expected_hex}, got {result}" + ) diff --git a/packages/testing/src/execution_testing/forks/tests/test_opcode_gas_costs.py b/packages/testing/src/execution_testing/forks/tests/test_opcode_gas_costs.py index c8559088d5b..1888027f420 100644 --- a/packages/testing/src/execution_testing/forks/tests/test_opcode_gas_costs.py +++ b/packages/testing/src/execution_testing/forks/tests/test_opcode_gas_costs.py @@ -4,7 +4,7 @@ from execution_testing.vm import Bytecode, Op -from ..forks.forks import Homestead, Osaka +from ..forks.forks import MONAD_NEXT, Homestead, Osaka from ..helpers import Fork @@ -609,3 +609,140 @@ def test_bytecode_refunds( # noqa: D103 def test_sstore_gas_costs(fork: Fork, opcode: Op, expected_cost: int) -> None: """Test SSTORE gas costs for all single-SSTORE scenarios.""" assert opcode.gas_cost(fork) == expected_cost + + +# --- MIP-8 page-based storage gas tests (MONAD_NEXT) --- + +_gc = MONAD_NEXT.gas_costs() + + +@pytest.mark.parametrize( + "opcode,expected_cost", + [ + pytest.param( + Op.SLOAD(page_load_warm=True), + _gc.PAGE_BASE_COST, + id="sload_warm_page", + ), + pytest.param( + Op.SLOAD(page_load_warm=False), + _gc.PAGE_LOAD_COST + _gc.PAGE_BASE_COST, + id="sload_cold_page", + ), + ], +) +def test_mip8_sload_gas(opcode: Op, expected_cost: int) -> None: + """Test MIP-8 page-level SLOAD gas.""" + assert opcode.gas_cost(MONAD_NEXT) == expected_cost + + +@pytest.mark.parametrize( + "opcode,expected_cost", + [ + pytest.param( + Op.SSTORE( + page_load_warm=True, + page_write_warm=True, + current_value=5, + new_value=5, + ), + _gc.PAGE_BASE_COST, + id="noop_same_value", + ), + pytest.param( + Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=0, + new_value=1, + ), + ( + _gc.PAGE_LOAD_COST + + _gc.PAGE_WRITE_COST + + _gc.PAGE_BASE_COST + + _gc.PAGE_STATE_GROWTH_COST + ), + id="create_slot_cold_page_new_peak", + ), + pytest.param( + Op.SSTORE( + page_load_warm=True, + page_write_warm=False, + current_value=0, + new_value=1, + ), + ( + _gc.PAGE_WRITE_COST + + _gc.PAGE_BASE_COST + + _gc.PAGE_STATE_GROWTH_COST + ), + id="create_slot_loaded_unwritten_page_new_peak", + ), + pytest.param( + Op.SSTORE( + page_load_warm=True, + page_write_warm=True, + current_value=0, + new_value=1, + ), + _gc.PAGE_BASE_COST + _gc.PAGE_STATE_GROWTH_COST, + id="create_slot_warm_page_new_peak", + ), + pytest.param( + Op.SSTORE( + page_load_warm=True, + page_write_warm=True, + current_value=0, + new_value=1, + current_state_growth=0, + net_state_growth=2, + ), + _gc.PAGE_BASE_COST, + id="create_slot_warm_page_below_peak", + ), + pytest.param( + Op.SSTORE( + page_load_warm=True, + page_write_warm=True, + current_value=3, + new_value=0, + ), + _gc.PAGE_BASE_COST, + id="clear_slot_warm_page", + ), + pytest.param( + Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=3, + new_value=0, + ), + (_gc.PAGE_LOAD_COST + _gc.PAGE_WRITE_COST + _gc.PAGE_BASE_COST), + id="clear_slot_cold_page", + ), + pytest.param( + Op.SSTORE( + page_load_warm=True, + page_write_warm=True, + current_value=3, + new_value=7, + ), + _gc.PAGE_BASE_COST, + id="update_nonzero_to_nonzero", + ), + ], +) +def test_mip8_sstore_gas(opcode: Op, expected_cost: int) -> None: + """Test MIP-8 page-level SSTORE gas.""" + assert opcode.gas_cost(MONAD_NEXT) == expected_cost + + +def test_mip8_sstore_no_refunds() -> None: + """MIP-8 removes all SSTORE refunds.""" + clear_op = Op.SSTORE( + page_load_warm=True, + page_write_warm=True, + current_value=5, + new_value=0, + ) + assert clear_op.refund(MONAD_NEXT) == 0 diff --git a/packages/testing/src/execution_testing/forks/transition_base_fork.py b/packages/testing/src/execution_testing/forks/transition_base_fork.py index 3dca0cb1d31..87c3be37417 100644 --- a/packages/testing/src/execution_testing/forks/transition_base_fork.py +++ b/packages/testing/src/execution_testing/forks/transition_base_fork.py @@ -2,7 +2,10 @@ from typing import Any, Callable, ClassVar, Dict, Type +from execution_testing.vm import OpcodeGasCalculator + from .base_fork import BaseFork +from .gas_costs import GasCosts class TransitionBaseMetaClass(type): @@ -106,6 +109,21 @@ def ruleset(cls) -> Dict[str, int]: """ raise Exception("Not implemented") + @classmethod + def gas_costs(cls) -> GasCosts: + """Return the gas costs of the transitioned-to fork.""" + return cls.transitions_to().gas_costs() + + @classmethod + def opcode_gas_calculator(cls) -> OpcodeGasCalculator: + """ + Return the opcode gas calculator of the transitioned-to fork. + + Transition forks delegate opcode gas to the resulting fork so that + opcode-level gas helpers work when given a transition fork. + """ + return cls.transitions_to().opcode_gas_calculator() + def transition_fork( to_fork: Type[BaseFork], diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 69d61382b30..853d10bb1af 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -791,7 +791,7 @@ def make_genesis( ) if empty_accounts := pre_alloc.empty_accounts(): raise Exception(f"Empty accounts in pre state: {empty_accounts}") - state_root = pre_alloc.state_root() + state_root = pre_alloc.state_root(self.fork.transitions_from()) genesis = FixtureHeader.genesis( self.fork.transitions_from(), env, state_root ) diff --git a/packages/testing/src/execution_testing/test_types/account_types.py b/packages/testing/src/execution_testing/test_types/account_types.py index aba69bed4aa..e3d41f74980 100644 --- a/packages/testing/src/execution_testing/test_types/account_types.py +++ b/packages/testing/src/execution_testing/test_types/account_types.py @@ -102,6 +102,41 @@ def storage_root(state: State, address: Bytes20) -> Bytes32: return EMPTY_TRIE_ROOT +def _resolve_state_module(fork: Any) -> Tuple[Any, Any, Any, Any, Any]: + """ + Pick the (State, set_account, set_storage, state_root, Account) tuple + matching ``fork``'s storage-hashing scheme. Defaults to the local + Frontier slot-based impl. MONAD_NEXT and any later fork switch to the + MIP-8 paged storage impl. + + For transition forks the genesis pre-state uses the FROM fork's + scheme. + """ + if fork is None: + return (State, set_account, set_storage, state_root, FrontierAccount) + + from execution_testing.forks.forks.forks import MONAD_NEXT + from execution_testing.forks.transition_base_fork import ( + TransitionBaseClass, + ) + + resolved = ( + fork.transitions_from() + if isinstance(fork, type) and issubclass(fork, TransitionBaseClass) + else fork + ) + + if isinstance(resolved, type) and issubclass(resolved, MONAD_NEXT): + return ( + State, + set_account, + set_storage, + _state_root_paged, + FrontierAccount, + ) + return (State, set_account, set_storage, state_root, FrontierAccount) + + def state_root(state: State) -> Bytes32: """Calculate the state root.""" assert not state._snapshots @@ -112,6 +147,21 @@ def get_storage_root(address: Bytes20) -> Bytes32: return root(state._main_trie, get_storage_root=get_storage_root) +def _state_root_paged(state: State) -> Bytes32: + """Calculate the state root using MIP-8 paged storage roots.""" + from ethereum.paged_storage_trie import storage_root_paged + + assert not state._snapshots + + def get_storage_root(address: Bytes20) -> Bytes32: + trie = state._storage_tries.get(address) + if trie is None or not trie._data: + return EMPTY_TRIE_ROOT + return Bytes32(storage_root_paged(trie._data)) + + return root(state._main_trie, get_storage_root=get_storage_root) + + class EOA(Address): """ An Externally Owned Account (EOA) is an account controlled by a private @@ -330,16 +380,31 @@ def empty_accounts(self) -> List[Address]: address for address, account in self.root.items() if not account ] - def state_root(self) -> Hash: - """Return state root of the allocation.""" - state = State() + def state_root(self, fork: Any = None) -> Hash: + """ + Return state root of the allocation. + + ``fork`` selects the storage-hashing scheme. ``None`` keeps the + legacy slot-based MPT (used by every fork up to MONAD_NINE). When + ``fork.name() == "MONAD_NEXT"`` the per-fork MIP-8 paged storage + root impl is used so the genesis state_root matches what the C++ + runtime computes for that fork. + """ + ( + StateCls, # noqa N806 + set_account_fn, + set_storage_fn, + state_root_fn, + AccountCls, # noqa N806 + ) = _resolve_state_module(fork) + state = StateCls() for address, account in self.root.items(): if account is None: continue - set_account( + set_account_fn( state=state, address=FrontierAddress(address), - account=FrontierAccount( + account=AccountCls( nonce=Uint(account.nonce) if account.nonce is not None else Uint(0), @@ -353,13 +418,13 @@ def state_root(self) -> Hash: ) if account.storage is not None: for key, value in account.storage.root.items(): - set_storage( + set_storage_fn( state=state, address=FrontierAddress(address), key=Bytes32(Hash(key)), value=U256(value), ) - return Hash(state_root(state)) + return Hash(state_root_fn(state)) def verify_post_alloc(self, got_alloc: "Alloc") -> None: """ diff --git a/packages/testing/src/execution_testing/tools/__init__.py b/packages/testing/src/execution_testing/tools/__init__.py index 0164e2fcccf..b5936ff9e4a 100644 --- a/packages/testing/src/execution_testing/tools/__init__.py +++ b/packages/testing/src/execution_testing/tools/__init__.py @@ -24,6 +24,7 @@ gas_test, generate_system_contract_deploy_test, generate_system_contract_error_test, + oog_test, ) from .utility.pytest import ParameterSet, extend_with_defaults from .utility.versioning import get_current_commit_hash_or_tag @@ -50,4 +51,5 @@ "generate_system_contract_deploy_test", "generate_system_contract_error_test", "get_current_commit_hash_or_tag", + "oog_test", ) diff --git a/packages/testing/src/execution_testing/tools/tools_code/generators.py b/packages/testing/src/execution_testing/tools/tools_code/generators.py index 3e82c18bd0f..0fb8eadda19 100644 --- a/packages/testing/src/execution_testing/tools/tools_code/generators.py +++ b/packages/testing/src/execution_testing/tools/tools_code/generators.py @@ -146,7 +146,7 @@ class CodeGasMeasure(Bytecode): To be considered when subtracting the value of the previous GAS operation, and to be popped at the end of the execution. """ - sstore_key: int | Bytes + sstore_key: int | Bytes | Bytecode """ Storage key to save the gas used. """ @@ -157,7 +157,7 @@ def __new__( code: Bytecode, overhead_cost: int = 0, extra_stack_items: int = 0, - sstore_key: int | Bytes = 0, + sstore_key: int | Bytes | Bytecode = 0, stop: bool = True, ) -> Self: """Assemble the bytecode that measures gas usage.""" diff --git a/packages/testing/src/execution_testing/tools/utility/generators.py b/packages/testing/src/execution_testing/tools/utility/generators.py index ca28a5b9fe1..454c04acd1f 100644 --- a/packages/testing/src/execution_testing/tools/utility/generators.py +++ b/packages/testing/src/execution_testing/tools/utility/generators.py @@ -445,6 +445,7 @@ def wrapper( slot_warm_gas = next(_slot) slot_oog_call_result = next(_slot) slot_sanity_call_result = next(_slot) +slot_setup_sanity_call_result = next(_slot) LEGACY_CALL_FAILURE = 0 LEGACY_CALL_SUCCESS = 1 @@ -463,6 +464,7 @@ def gas_test( warm_gas: int | None = None, subject_address: Address | None = None, subject_balance: int = 0, + subject_storage: dict | None = None, oog_difference: int = 1, out_of_gas_testing: bool = True, prelude_code: Bytecode | None = None, @@ -502,6 +504,7 @@ def gas_test( code_subject, balance=subject_balance, address=subject_address, + storage=subject_storage or {}, ) # Auxiliary instructions charged for at every gas run @@ -632,3 +635,115 @@ def gas_test( ) state_test(pre=pre, tx=tx, post=post) + + +def oog_test( + *, + fork: Fork, + state_test: StateTestFiller, + pre: Alloc, + subject_code: Bytecode, + setup_code: Bytecode | None = None, + expected_gas: int | None = None, + oog_difference: int = 1, + subject_balance: int = 0, + subject_storage: dict | None = None, + prelude_code: Bytecode | None = None, + tx_gas: int | None = None, +) -> None: + """ + Verify `subject_code` runs out of gas correctly. + + Deploys three copies of the tested bytecode and invokes each + from a harness contract via CALL: + - A holds `setup_code + subject_code`, called with `expected_gas` + (expected to succeed). + - B holds `setup_code + subject_code`, called with + `expected_gas - oog_difference` (expected to OOG). + - C holds only `setup_code`, called with + `expected_gas - oog_difference`. + + `expected_gas` defaults to + `(setup_code + subject_code).gas_cost(fork)`. + + `prelude_code` runs once in the harness before the three calls + (e.g. to seed warm state visible to all runs). + + Note: opcodes guarded by the EIP-2200 SSTORE stipend (>2300 + gas required in the call frame) may not OOG cleanly when + `expected_gas` is near the stipend threshold. + """ + if fork < Berlin: + raise ValueError( + "OOG tests before Berlin are not supported due to CALL gas changes" + ) + + if setup_code is None: + setup_code = Bytecode() + + if prelude_code is None: + prelude_code = Bytecode() + + full_code = setup_code + subject_code + if expected_gas is None: + expected_gas = full_code.gas_cost(fork) + + address_subject_a, address_subject_b = ( + pre.deploy_contract( + full_code + Op.STOP, + balance=subject_balance, + storage=subject_storage or {}, + ) + for _ in range(2) + ) + address_setup_only = pre.deploy_contract( + setup_code + Op.STOP, + balance=subject_balance, + storage=subject_storage or {}, + ) + + harness_code = ( + prelude_code + + Op.SSTORE( + slot_sanity_call_result, + Op.CALL(gas=expected_gas, address=address_subject_a), + ) + + Op.SSTORE( + slot_oog_call_result, + Op.CALL( + gas=expected_gas - oog_difference, + address=address_subject_b, + ), + ) + + Op.SSTORE( + slot_setup_sanity_call_result, + Op.CALL( + gas=expected_gas - oog_difference, + address=address_setup_only, + ), + ) + + Op.STOP + ) + address_harness = pre.deploy_contract(harness_code) + + if tx_gas is None: + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", contract_creation=False + ) + tx_gas = ( + intrinsic + harness_code.gas_cost(fork) + 3 * expected_gas + 10_000 + ) + + sender = pre.fund_eoa() + tx = Transaction(to=address_harness, gas_limit=tx_gas, sender=sender) + + post = { + address_harness: Account( + storage={ + slot_sanity_call_result: LEGACY_CALL_SUCCESS, + slot_oog_call_result: LEGACY_CALL_FAILURE, + slot_setup_sanity_call_result: LEGACY_CALL_SUCCESS, + }, + ), + } + state_test(pre=pre, tx=tx, post=post) diff --git a/packages/testing/src/execution_testing/vm/opcodes.py b/packages/testing/src/execution_testing/vm/opcodes.py index 49563887cb7..6c770bca2fd 100644 --- a/packages/testing/src/execution_testing/vm/opcodes.py +++ b/packages/testing/src/execution_testing/vm/opcodes.py @@ -307,6 +307,21 @@ def with_metadata(self, **metadata: Any) -> "Opcode": ) # Create a new opcode instance with updated metadata + merged = {**self.metadata, **metadata} + # key_warm=True implies the page is at least load-warm. Auto-set + # page_load_warm when only key_warm was passed (so pre-MIP-8 tests + # using key_warm still price correctly under MIP-8). Reject + # explicit conflicts (key_warm=True alongside page_load_warm=False + # in the same call). + if merged.get("key_warm") and "page_load_warm" in merged: + if "page_load_warm" in metadata and not metadata["page_load_warm"]: + raise ValueError( + "key_warm=True implies page_load_warm=True; got " + f"page_load_warm={metadata['page_load_warm']} in the " + "same call" + ) + if "page_load_warm" not in metadata: + merged["page_load_warm"] = True new_opcode = Opcode( bytes(self), popped_stack_items=self.popped_stack_items, @@ -319,8 +334,7 @@ def with_metadata(self, **metadata: Any) -> "Opcode": terminating=self.terminating, kwargs=self.kwargs, kwargs_defaults=self.kwargs_defaults, - # Merge defaults, existing metadata, and new metadata - metadata={**self.metadata, **metadata}, + metadata=merged, original_opcode=self, ) new_opcode.opcode_list = [new_opcode] @@ -2556,7 +2570,7 @@ class Opcodes(Opcode, Enum): popped_stack_items=1, pushed_stack_items=1, kwargs=["key"], - metadata={"key_warm": False}, + metadata={"key_warm": False, "page_load_warm": False}, ) """ SLOAD(key) = value @@ -2581,12 +2595,25 @@ class Opcodes(Opcode, Enum): Gas ---- + NOTE: MIP-8 (MONAD_NEXT) changes the gas cost, see spec. - static_gas = 0 - dynamic_gas = 100 if warm_address, 2600 if cold_address + MIP-8 (MONAD_NEXT) replaces per-key warming with per-page + warming: `GAS_PAGE_BASE_COST` when the page is already in + `read_accessed_pages`, else `GAS_PAGE_LOAD_COST + + GAS_PAGE_BASE_COST`. + Metadata ---- - - key_warm: whether the storage key is already warm (default: False) + - key_warm: whether the storage key is already warm (default: False). + Used by pre-MIP-8 gas calculations. Under MIP-8 a True + value implies `page_load_warm=True` (any prior SLOAD/SSTORE + on the key necessarily loaded its page) so pre-MIP-8 tests + flagged only with `key_warm` still price correctly. + - page_load_warm: MIP-8 only. Whether the page hosting `key` is + already in `read_accessed_pages` at the time of the SLOAD + (default: False). Source: [evm.codes/#54](https://www.evm.codes/#54) """ @@ -2600,6 +2627,10 @@ class Opcodes(Opcode, Enum): "original_value": 0, "current_value": None, "new_value": 1, + "page_load_warm": False, + "page_write_warm": False, + "current_state_growth": 0, + "net_state_growth": 0, }, ) """ @@ -2625,6 +2656,7 @@ class Opcodes(Opcode, Enum): Gas ---- + NOTE: MIP-8 (MONAD_NEXT) changes the gas cost, see spec. ``` static_gas = 0 @@ -2648,12 +2680,26 @@ class Opcodes(Opcode, Enum): Metadata ---- - key_warm: whether the key had already been accessed during the - transaction, either by SLOAD or SSTORE (default: False) + transaction, either by SLOAD or SSTORE (default: False). + Under MIP-8 a True value implies `page_load_warm=True`. - original_value: value the storage key had at the beginning of the transaction (default: 0) - current_value: value the storage key holds at the execution of the opcode (default: None, which means same as original_value) - new_value: value being set by the opcode (default: 1) + - page_load_warm: MIP-8 only. Whether the page hosting `key` is + already in `read_accessed_pages` at the time of the SSTORE + (default: False). + - page_write_warm: MIP-8 only. Whether the page hosting `key` is + already in `write_accessed_pages` at the time of the SSTORE + (default: False). + - current_state_growth: MIP-8 only. The page's current + state-growth counter (count of nonzero slots) prior to this + SSTORE (default: 0). + - net_state_growth: MIP-8 only. The page's peak state-growth + counter so far in the transaction prior to this SSTORE + (default: 0). A state-growth charge applies when this SSTORE + pushes `current_state_growth` strictly above `net_state_growth`. Source: [evm.codes/#55](https://www.evm.codes/#55) """ diff --git a/src/ethereum/crypto/blake3.py b/src/ethereum/crypto/blake3.py new file mode 100644 index 00000000000..a13d323c4fa --- /dev/null +++ b/src/ethereum/crypto/blake3.py @@ -0,0 +1,258 @@ +""" +BLAKE3 Cryptographic Hash Function. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of BLAKE3 compression function and hash, used for +page commitments and storage trie hashing in MIP-8. +""" + +import struct +from typing import List + +from ethereum_types.bytes import Bytes + +IV = [ + 0x6A09E667, + 0xBB67AE85, + 0x3C6EF372, + 0xA54FF53A, + 0x510E527F, + 0x9B05688C, + 0x1F83D9AB, + 0x5BE0CD19, +] + +MSG_PERMUTATION = [ + 2, + 6, + 3, + 10, + 7, + 0, + 4, + 13, + 1, + 11, + 12, + 5, + 9, + 14, + 15, + 8, +] + +CHUNK_START = 1 +CHUNK_END = 2 +PARENT = 4 +ROOT = 8 +DERIVE_KEY_MATERIAL = 64 + +BLOCK_LEN = 64 +CHUNK_LEN = 1024 +MASK_32 = 0xFFFFFFFF + + +def _rotate_right(x: int, n: int) -> int: + return ((x >> n) | (x << (32 - n))) & MASK_32 + + +def _g( + state: List[int], + a: int, + b: int, + c: int, + d: int, + mx: int, + my: int, +) -> None: + state[a] = (state[a] + state[b] + mx) & MASK_32 + state[d] = _rotate_right(state[d] ^ state[a], 16) + state[c] = (state[c] + state[d]) & MASK_32 + state[b] = _rotate_right(state[b] ^ state[c], 12) + state[a] = (state[a] + state[b] + my) & MASK_32 + state[d] = _rotate_right(state[d] ^ state[a], 8) + state[c] = (state[c] + state[d]) & MASK_32 + state[b] = _rotate_right(state[b] ^ state[c], 7) + + +def _round(state: List[int], m: List[int]) -> None: + _g(state, 0, 4, 8, 12, m[0], m[1]) + _g(state, 1, 5, 9, 13, m[2], m[3]) + _g(state, 2, 6, 10, 14, m[4], m[5]) + _g(state, 3, 7, 11, 15, m[6], m[7]) + _g(state, 0, 5, 10, 15, m[8], m[9]) + _g(state, 1, 6, 11, 12, m[10], m[11]) + _g(state, 2, 7, 8, 13, m[12], m[13]) + _g(state, 3, 4, 9, 14, m[14], m[15]) + + +def _permute(m: List[int]) -> List[int]: + return [m[i] for i in MSG_PERMUTATION] + + +def compress( + chaining_value: List[int], + block_bytes: bytes, + block_len: int, + counter: int, + flags: int, +) -> List[int]: + """ + Run the BLAKE3 compression function. + + Parameters + ---------- + chaining_value : + 8 x 32-bit words of chaining value. + block_bytes : + 64 bytes of message block data. + block_len : + Number of valid bytes in the block (0-64). + counter : + 64-bit block counter. + flags : + Flag bits for this compression. + + Returns + ------- + output : List[int] + 16 x 32-bit words. + + """ + assert len(block_bytes) == BLOCK_LEN + m = list(struct.unpack("<16I", block_bytes)) + + state = [ + chaining_value[0], + chaining_value[1], + chaining_value[2], + chaining_value[3], + chaining_value[4], + chaining_value[5], + chaining_value[6], + chaining_value[7], + IV[0], + IV[1], + IV[2], + IV[3], + counter & MASK_32, + (counter >> 32) & MASK_32, + block_len & MASK_32, + flags & MASK_32, + ] + + for _ in range(7): + _round(state, m) + m = _permute(m) + + for i in range(8): + state[i] ^= state[i + 8] + state[i + 8] ^= chaining_value[i] + + return state + + +def words_to_bytes(words: List[int]) -> bytes: + """Serialize 32-bit words to little-endian bytes.""" + return struct.pack("<%dI" % len(words), *words) + + +def bytes_to_words(data: bytes) -> List[int]: + """Deserialize little-endian bytes to 32-bit words.""" + assert len(data) % 4 == 0 + return list(struct.unpack("<%dI" % (len(data) // 4), data)) + + +def _compress_chunk(chunk: bytes, counter: int, extra_flags: int) -> List[int]: + """ + Compress a single chunk (up to 1024 bytes) into 8 chaining + value words. + """ + cv = list(IV) + num_blocks = max(1, (len(chunk) + BLOCK_LEN - 1) // BLOCK_LEN) + + for i in range(num_blocks): + start = i * BLOCK_LEN + block = chunk[start : start + BLOCK_LEN] + blen = len(block) + if blen < BLOCK_LEN: + block = block + b"\x00" * (BLOCK_LEN - blen) + + flags = 0 + if i == 0: + flags |= CHUNK_START + if i == num_blocks - 1: + flags |= CHUNK_END | extra_flags + + output = compress(cv, block, blen, counter, flags) + cv = output[:8] + + return cv + + +def _parent_cv( + left: List[int], right: List[int], extra_flags: int +) -> List[int]: + """Merge two child chaining values into a parent.""" + block = words_to_bytes(left) + words_to_bytes(right) + output = compress(IV, block, BLOCK_LEN, 0, PARENT | extra_flags) + return output[:8] + + +def blake3_hash(data: Bytes | bytes) -> bytes: + """ + Compute the BLAKE3 hash of input data. + + Parameters + ---------- + data : + Input bytes to hash. + + Returns + ------- + digest : bytes + 32-byte BLAKE3 digest. + + """ + data = bytes(data) + + if len(data) == 0: + output = compress( + IV, + b"\x00" * BLOCK_LEN, + 0, + 0, + CHUNK_START | CHUNK_END | ROOT, + ) + return words_to_bytes(output[:8]) + + if len(data) <= CHUNK_LEN: + cv = _compress_chunk(data, 0, ROOT) + return words_to_bytes(cv) + + cvs: List[List[int]] = [] + offset = 0 + counter = 0 + while offset < len(data): + chunk = data[offset : offset + CHUNK_LEN] + offset += CHUNK_LEN + cvs.append(_compress_chunk(chunk, counter, 0)) + counter += 1 + + while len(cvs) > 2: + next_level: List[List[int]] = [] + for i in range(0, len(cvs), 2): + if i + 1 < len(cvs): + next_level.append(_parent_cv(cvs[i], cvs[i + 1], 0)) + else: + next_level.append(cvs[i]) + cvs = next_level + + root = _parent_cv(cvs[0], cvs[1], ROOT) + return words_to_bytes(root) diff --git a/src/ethereum/forks/monad_next/__init__.py b/src/ethereum/forks/monad_next/__init__.py new file mode 100644 index 00000000000..db47697c567 --- /dev/null +++ b/src/ethereum/forks/monad_next/__init__.py @@ -0,0 +1,8 @@ +""" +MONAD_TEN fork introduces Monad specific changes to the Ethereum protocol. +""" + +from ethereum.fork_criteria import ByTimestamp, ForkCriteria + +# TODO: just a bit after MONAD_NINE +FORK_CRITERIA: ForkCriteria = ByTimestamp(1774898552) diff --git a/src/ethereum/forks/monad_next/blocks.py b/src/ethereum/forks/monad_next/blocks.py new file mode 100644 index 00000000000..f6745f79de3 --- /dev/null +++ b/src/ethereum/forks/monad_next/blocks.py @@ -0,0 +1,414 @@ +""" +A `Block` is a single link in the chain that is Ethereum. Each `Block` contains +a `Header` and zero or more transactions. Each `Header` contains associated +metadata like the block number, parent block hash, and how much gas was +consumed by its transactions. + +Together, these blocks form a cryptographically secure journal recording the +history of all state transitions that have happened since the genesis of the +chain. +""" + +from dataclasses import dataclass +from typing import Tuple, final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes8, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32 +from ethereum.state import Address, Root + +from .fork_types import Bloom +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, +) + + +@final +@slotted_freezable +@dataclass +class Withdrawal: + """ + Withdrawals represent a transfer of ETH from the consensus layer (beacon + chain) to the execution layer, as validated by the consensus layer. Each + withdrawal is listed in the block's list of withdrawals. See [`block`]. + + [`block`]: ref:ethereum.forks.monad_next.blocks.Block.withdrawals + """ + + index: U64 + """ + The unique index of the withdrawal, incremented for each withdrawal + processed. + """ + + validator_index: U64 + """ + The index of the validator on the consensus layer that is withdrawing. + """ + + address: Address + """ + The execution-layer address receiving the withdrawn ETH. + """ + + amount: U256 + """ + The amount of ETH being withdrawn. + """ + + +@final +@slotted_freezable +@dataclass +class Header: + """ + Header portion of a block on the chain, containing metadata and + cryptographic commitments to the block's contents. + """ + + parent_hash: Hash32 + """ + Hash ([`keccak256`]) of the parent block's header, encoded with [RLP]. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [RLP]: https://ethereum.github.io/ethereum-rlp/src/ethereum_rlp/rlp.py.html + """ + + ommers_hash: Hash32 + """ + Hash ([`keccak256`]) of the ommers (uncle blocks) in this block, encoded + with [RLP]. However, in post merge forks `ommers_hash` is always + [`EMPTY_OMMER_HASH`]. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [RLP]: https://ethereum.github.io/ethereum-rlp/src/ethereum_rlp/rlp.py.html + [`EMPTY_OMMER_HASH`]: ref:ethereum.forks.monad_next.fork.EMPTY_OMMER_HASH + """ + + coinbase: Address + """ + Address of the miner (or validator) who mined this block. + + The coinbase address receives the block reward and the priority fees (tips) + from included transactions. Base fees (introduced in [EIP-1559]) are burned + and do not go to the coinbase. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + state_root: Root + """ + Root hash ([`keccak256`]) of the state trie after executing all + transactions in this block. It represents the state of the Ethereum Virtual + Machine (EVM) after all transactions in this block have been processed. It + is computed using [`compute_state_root_and_trie_changes()`][changes], + which computes the root of the Merkle-Patricia [Trie] representing the + Ethereum world state after applying the block's state changes. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [Trie]: ref:ethereum.merkle_patricia_trie.Trie + """ # noqa: E501 + + transactions_root: Root + """ + Root hash ([`keccak256`]) of the transactions trie, which contains all + transactions included in this block in their original order. It is computed + using the [`root()`] function over the Merkle-Patricia [trie] of + transactions as the parameter. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [`root()`]: ref:ethereum.merkle_patricia_trie.root + [Trie]: ref:ethereum.merkle_patricia_trie.Trie + """ + + receipt_root: Root + """ + Root hash ([`keccak256`]) of the receipts trie, which contains all receipts + for transactions in this block. It is computed using the [`root()`] + function over the Merkle-Patricia [trie] constructed from the receipts. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [`root()`]: ref:ethereum.merkle_patricia_trie.root + [Trie]: ref:ethereum.merkle_patricia_trie.Trie + """ + + bloom: Bloom + """ + Bloom filter for logs generated by transactions in this block. + Constructed from all logs in the block using the [logs bloom] mechanism. + + [logs bloom]: ref:ethereum.forks.monad_next.bloom.logs_bloom + """ + + difficulty: Uint + """ + Difficulty of the block (pre-PoS), or a constant in PoS. + """ + + number: Uint + """ + Block number (height) in the chain. + """ + + gas_limit: Uint + """ + Maximum gas allowed in this block. Pre [EIP-1559], this was the maximum + gas that could be consumed by all transactions in the block. Post + [EIP-1559], this is still the maximum gas limit, but the base fee per gas + is adjusted so that effective block gas utilization targets 50% of + that limit. The gas_limit is a voted parameter that can be + [adjusted by a factor of 1/1024] from the previous block's limit by the + block proposer, allowing the network to coordinate on capacity + increases (e.g. the 60M limit proposed in EIP-7935). + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + [adjusted by a factor of 1/1024]: + https://ethereum.org/en/developers/docs/blocks/ + """ + + gas_used: Uint + """ + Total gas used by all transactions in this block. + """ + + timestamp: U256 + """ + Timestamp of when the block was mined, in seconds since the unix epoch. + """ + + extra_data: Bytes + """ + Arbitrary data included by the miner. + """ + + prev_randao: Bytes32 + """ + Output of the RANDAO beacon for random validator selection. + """ + + nonce: Bytes8 + """ + Nonce used in the mining process (pre-PoS), set to zero in PoS. + """ + + base_fee_per_gas: Uint + """ + Base fee per gas for transactions in this block, introduced in + [EIP-1559]. This is the minimum fee per gas that must be paid for a + transaction to be included in this block. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + withdrawals_root: Root + """ + Root hash of the withdrawals trie, which contains all withdrawals in this + block. + """ + + blob_gas_used: U64 + """ + Total blob gas consumed by the transactions within this block. Introduced + in [EIP-4844]. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + excess_blob_gas: U64 + """ + Running total of blob gas consumed in excess of the target, prior to this + block. Blocks with above-target blob gas consumption increase this value, + while blocks with below-target blob gas consumption decrease it (to a + minimum of zero). Introduced in [EIP-4844]. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + parent_beacon_block_root: Root + """ + Root hash of the corresponding beacon chain block. + """ + + requests_hash: Hash32 + """ + [SHA2-256] hash of all the collected requests in this block. Introduced in + [EIP-7685]. See [`compute_requests_hash`][crh] for more details. + + [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 + [crh]: ref:ethereum.forks.monad_next.requests.compute_requests_hash + [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 + """ + + +@final +@slotted_freezable +@dataclass +class Block: + """ + A complete block on Ethereum, which is composed of a block [`header`], + a list of transactions, a list of ommers (deprecated), and a list of + validator [withdrawals]. + + The block [`header`] includes fields relevant to the Proof-of-Stake + consensus, with deprecated Proof-of-Work fields such as `difficulty`, + `nonce`, and `ommersHash` set to constants. The `coinbase` field + denotes the address receiving priority fees from the block. + + The header also contains commitments to the current state (`stateRoot`), + the transactions (`transactionsRoot`), the transaction receipts + (`receiptsRoot`), and `withdrawalsRoot` committing to the validator + withdrawals included in this block. It also includes a bloom filter which + summarizes log data from the transactions. + + Withdrawals represent ETH transfers from validators to their recipients, + introduced by the consensus layer. Ommers remain deprecated and empty. + + [`header`]: ref:ethereum.forks.monad_next.blocks.Header + [withdrawals]: ref:ethereum.forks.monad_next.blocks.Withdrawal + """ + + header: Header + """ + The block header containing metadata and cryptographic commitments. Refer + to [headers] for more details on the fields included in the header. + + [headers]: ref:ethereum.forks.monad_next.blocks.Header + """ + + transactions: Tuple[Bytes | LegacyTransaction, ...] + """ + A tuple of transactions included in this block. Each transaction can be + any of a legacy transaction, an access list transaction, a fee market + transaction, a blob transaction, or a set code transaction. + """ + + ommers: Tuple[Header, ...] + """ + A tuple of ommers (uncle blocks) included in this block. Always empty in + Proof-of-Stake forks. + """ + + withdrawals: Tuple[Withdrawal, ...] + """ + A tuple of withdrawals processed in this block. + """ + + +@final +@slotted_freezable +@dataclass +class Log: + """ + Data record produced during the execution of a transaction. Logs are used + by smart contracts to emit events (using the EVM log opcodes ([`LOG0`], + [`LOG1`], [`LOG2`], [`LOG3`] and [`LOG4`]), which can be efficiently + searched using the bloom filter in the block header. + + [`LOG0`]: ref:ethereum.forks.monad_next.vm.instructions.log.log0 + [`LOG1`]: ref:ethereum.forks.monad_next.vm.instructions.log.log1 + [`LOG2`]: ref:ethereum.forks.monad_next.vm.instructions.log.log2 + [`LOG3`]: ref:ethereum.forks.monad_next.vm.instructions.log.log3 + [`LOG4`]: ref:ethereum.forks.monad_next.vm.instructions.log.log4 + """ + + address: Address + """ + The address of the contract that emitted the log. + """ + + topics: Tuple[Hash32, ...] + """ + A tuple of up to four topics associated with the log, used for filtering. + """ + + data: Bytes + """ + The data payload of the log, which can contain any arbitrary data. + """ + + +@final +@slotted_freezable +@dataclass +class Receipt: + """ + Result of a transaction execution. Receipts are included in the receipts + trie. + """ + + succeeded: bool + """ + Whether the transaction execution was successful. + """ + + cumulative_gas_used: Uint + """ + Total gas used in the block up to and including this transaction. + """ + + bloom: Bloom + """ + Bloom filter for logs generated by this transaction. This is a 2048-byte + bit array that allows for efficient filtering of logs. + """ + + logs: Tuple[Log, ...] + """ + A tuple of logs generated by this transaction. Each log contains the + address of the contract that emitted it, a tuple of topics, and the data + payload. + """ + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Bytes | Receipt: + r""" + Encodes a transaction receipt based on the transaction type. + + The encoding follows the same format as transactions encoding, where: + - AccessListTransaction receipts are prefixed with `b"\x01"`. + - FeeMarketTransaction receipts are prefixed with `b"\x02"`. + - BlobTransaction receipts are prefixed with `b"\x03"`. + - SetCodeTransaction receipts are prefixed with `b"\x04"`. + - LegacyTransaction receipts are returned as is. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(receipt) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Bytes | Receipt) -> Receipt: + r""" + Decodes a receipt from its serialized form. + + The decoding follows the same format as transactions decoding, where: + - Receipts prefixed with `b"\x01"` are decoded as AccessListTransaction + receipts. + - Receipts prefixed with `b"\x02"` are decoded as FeeMarketTransaction + receipts. + - Receipts prefixed with `b"\x03"` are decoded as BlobTransaction + receipts. + - Receipts prefixed with `b"\x04"` are decoded as SetCodeTransaction + receipts. + - LegacyTransaction receipts are returned as is. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2, 3, 4) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/forks/monad_next/bloom.py b/src/ethereum/forks/monad_next/bloom.py new file mode 100644 index 00000000000..0e079df6b39 --- /dev/null +++ b/src/ethereum/forks/monad_next/bloom.py @@ -0,0 +1,87 @@ +""" +Ethereum Logs Bloom. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +This module defines functions for calculating bloom filters of logs. For the +general theory of bloom filters see e.g. `Wikipedia +`_. Bloom filters are used to allow +for efficient searching of logs by address and/or topic, by rapidly +eliminating blocks and receipts from their search. +""" + +from typing import Tuple + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import keccak256 + +from .blocks import Log +from .fork_types import Bloom + + +def add_to_bloom(bloom: bytearray, bloom_entry: Bytes) -> None: + """ + Add a bloom entry to the bloom filter (`bloom`). + + The number of hash functions used is 3. They are calculated by taking the + least significant 11 bits from the first 3 16-bit words of the + `keccak_256()` hash of `bloom_entry`. + + Parameters + ---------- + bloom : + The bloom filter. + bloom_entry : + An entry which is to be added to bloom filter. + + """ + hashed = keccak256(bloom_entry) + + for idx in (0, 2, 4): + # Obtain the least significant 11 bits from the pair of bytes + # (16 bits), and set this bit in bloom bytearray. + # The obtained bit is 0-indexed in the bloom filter from the least + # significant bit to the most significant bit. + bit_to_set = Uint.from_be_bytes(hashed[idx : idx + 2]) & Uint(0x07FF) + # Below is the index of the bit in the bytearray (where 0-indexed + # byte is the most significant byte) + bit_index = 0x07FF - int(bit_to_set) + + byte_index = bit_index // 8 + bit_value = 1 << (7 - (bit_index % 8)) + bloom[byte_index] = bloom[byte_index] | bit_value + + +def logs_bloom(logs: Tuple[Log, ...]) -> Bloom: + """ + Obtain the logs bloom from a list of log entries. + + The address and each topic of a log are added to the bloom filter. + + Parameters + ---------- + logs : + List of logs for which the logs bloom is to be obtained. + + Returns + ------- + logs_bloom : `Bloom` + The logs bloom obtained which is 256 bytes with some bits set as per + the caller address and the log topics. + + """ + bloom: bytearray = bytearray(b"\x00" * 256) + + for log in logs: + add_to_bloom(bloom, log.address) + for topic in log.topics: + add_to_bloom(bloom, topic) + + return Bloom(bloom) diff --git a/src/ethereum/forks/monad_next/exceptions.py b/src/ethereum/forks/monad_next/exceptions.py new file mode 100644 index 00000000000..3074a1f738f --- /dev/null +++ b/src/ethereum/forks/monad_next/exceptions.py @@ -0,0 +1,131 @@ +""" +Exceptions specific to this fork. +""" + +from typing import TYPE_CHECKING, Final + +from ethereum_types.numeric import Uint + +from ethereum.exceptions import InvalidTransaction + +if TYPE_CHECKING: + from .transactions import Transaction + + +class TransactionTypeError(InvalidTransaction): + """ + Unknown [EIP-2718] transaction type byte. + + [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + """ + + transaction_type: Final[int] + """ + The type byte of the transaction that caused the error. + """ + + def __init__(self, transaction_type: int): + super().__init__(f"unknown transaction type `{transaction_type}`") + self.transaction_type = transaction_type + + +class TransactionTypeContractCreationError(InvalidTransaction): + """ + Contract creation is not allowed for a transaction type. + """ + + transaction: "Transaction" + """ + The transaction that caused the error. + """ + + def __init__(self, transaction: "Transaction"): + super().__init__( + f"transaction type `{type(transaction).__name__}` not allowed to " + "create contracts" + ) + self.transaction = transaction + + +class BlobGasLimitExceededError(InvalidTransaction): + """ + The blob gas limit for the transaction exceeds the maximum allowed. + """ + + +class InsufficientMaxFeePerBlobGasError(InvalidTransaction): + """ + The maximum fee per blob gas is insufficient for the transaction. + """ + + +class InsufficientMaxFeePerGasError(InvalidTransaction): + """ + The maximum fee per gas is insufficient for the transaction. + """ + + transaction_max_fee_per_gas: Final[Uint] + """ + The maximum fee per gas specified in the transaction. + """ + + block_base_fee_per_gas: Final[Uint] + """ + The base fee per gas of the block in which the transaction is included. + """ + + def __init__( + self, transaction_max_fee_per_gas: Uint, block_base_fee_per_gas: Uint + ): + super().__init__( + f"Insufficient max fee per gas " + f"({transaction_max_fee_per_gas} < {block_base_fee_per_gas})" + ) + self.transaction_max_fee_per_gas = transaction_max_fee_per_gas + self.block_base_fee_per_gas = block_base_fee_per_gas + + +class InvalidBlobVersionedHashError(InvalidTransaction): + """ + The versioned hash of the blob is invalid. + """ + + +class NoBlobDataError(InvalidTransaction): + """ + The transaction does not contain any blob data. + """ + + +class BlobCountExceededError(InvalidTransaction): + """ + The transaction has more blobs than the limit. + """ + + +class PriorityFeeGreaterThanMaxFeeError(InvalidTransaction): + """ + The priority fee is greater than the maximum fee per gas. + """ + + +class EmptyAuthorizationListError(InvalidTransaction): + """ + The authorization list in the transaction is empty. + """ + + +class InitCodeTooLargeError(InvalidTransaction): + """ + The init code of the transaction is too large. + """ + + +class TransactionGasLimitExceededError(InvalidTransaction): + """ + The transaction has specified a gas limit that is greater than the allowed + maximum. + + Note that this is _not_ the exception thrown when bytecode execution runs + out of gas. + """ diff --git a/src/ethereum/forks/monad_next/fork.py b/src/ethereum/forks/monad_next/fork.py new file mode 100644 index 00000000000..d5c2b4bffac --- /dev/null +++ b/src/ethereum/forks/monad_next/fork.py @@ -0,0 +1,1097 @@ +""" +Ethereum Specification. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Entry point for the Ethereum specification. +""" + +from dataclasses import dataclass +from typing import Dict, Final, List, Optional, Tuple, final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import ( + EthereumException, + GasUsedExceedsLimitError, + InsufficientBalanceError, + InvalidBlock, + InvalidSenderError, + NonceMismatchError, +) +from ethereum.merkle_patricia_trie import ( + EMPTY_TRIE_ROOT, + Trie, + copy_trie, + root, + trie_set, +) +from ethereum.paged_storage_trie import storage_root_paged +from ethereum.state import ( + EMPTY_CODE_HASH, + Account, + Address, + State, + apply_changes_to_state, +) + +from . import vm +from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt +from .bloom import logs_bloom +from .exceptions import ( + BlobCountExceededError, + BlobGasLimitExceededError, + EmptyAuthorizationListError, + InsufficientMaxFeePerBlobGasError, + InsufficientMaxFeePerGasError, + InvalidBlobVersionedHashError, + NoBlobDataError, + PriorityFeeGreaterThanMaxFeeError, + TransactionTypeContractCreationError, +) +from .fork_types import Authorization, VersionedHash +from .requests import ( + CONSOLIDATION_REQUEST_TYPE, + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + compute_requests_hash, + parse_deposit_requests, +) +from .state_tracker import ( + BlockState, + TransactionState, + add_sender_authority, + create_ether, + destroy_account, + extract_block_diff, + forget_senders_authorities, + get_account, + get_code, + incorporate_tx_into_block, + increment_nonce, + set_account_balance, +) +from .transactions import ( + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, + decode_transaction, + encode_transaction, + get_transaction_hash, + has_access_list, + recover_sender, + validate_transaction, +) +from .utils.hexadecimal import hex_to_address +from .utils.message import prepare_message +from .vm import Message +from .vm.eoa_delegation import is_valid_delegation +from .vm.gas import ( + GasCosts, + calculate_blob_gas_price, + calculate_data_fee, + calculate_excess_blob_gas, + calculate_total_blob_gas, +) +from .vm.interpreter import MessageCallOutput, process_message_call + +BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) +ELASTICITY_MULTIPLIER = Uint(2) +EMPTY_OMMER_HASH = keccak256(rlp.encode([])) +SYSTEM_ADDRESS = hex_to_address("0xfffffffffffffffffffffffffffffffffffffffe") +BEACON_ROOTS_ADDRESS = hex_to_address( + "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" +) +SYSTEM_TRANSACTION_GAS = Uint(30000000) +MAX_BLOB_GAS_PER_BLOCK: Final[U64] = ( + GasCosts.BLOB_SCHEDULE_MAX * GasCosts.PER_BLOB +) +VERSIONED_HASH_VERSION_KZG = b"\x01" + +WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x00000961Ef480Eb55e80D19ad83579A64c007002" +) +CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x0000BBdDc7CE488642fb579F8B00f3a590007251" +) +HISTORY_STORAGE_ADDRESS = hex_to_address( + "0x0000F90827F1C53a10cb7A02335B175320002935" +) +MAX_BLOCK_SIZE = 10_485_760 +SAFETY_MARGIN = 2_097_152 +MAX_RLP_BLOCK_SIZE = MAX_BLOCK_SIZE - SAFETY_MARGIN +BLOB_COUNT_LIMIT = 6 + + +def compute_paged_state_root( + state: State, + account_changes: Dict[Address, Optional[Account]], + storage_changes: Dict[Address, Dict[Bytes32, U256]], +) -> Hash32: + """ + Compute the state root after applying the block's account and storage + changes to ``state``, using MIP-8 paged storage roots. + """ + main_trie = copy_trie(state._main_trie) + storage_tries = {k: copy_trie(v) for k, v in state._storage_tries.items()} + + for address, account in account_changes.items(): + trie_set(main_trie, address, account) + + for address, slots in storage_changes.items(): + trie = storage_tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + storage_tries[address] = trie + for key, value in slots.items(): + trie_set(trie, key, value) + if trie._data == {}: + del storage_tries[address] + + def get_storage_root(addr: Address) -> Hash32: + if addr in storage_tries and storage_tries[addr]._data: + return storage_root_paged(storage_tries[addr]._data) + return EMPTY_TRIE_ROOT + + return root(main_trie, get_storage_root=get_storage_root) + + +@final +@dataclass +class BlockChain: + """ + History and current state of the block chain. + """ + + blocks: List[Block] + state: State + chain_id: U64 + + +def apply_fork(old: BlockChain) -> BlockChain: + """ + Transforms the state from the previous hard fork (`old`) into the block + chain object for this hard fork and returns it. + + When forks need to implement an irregular state transition, this function + is used to handle the irregularity. See the :ref:`DAO Fork ` for + an example. + + Parameters + ---------- + old : + Previous block chain object. + + Returns + ------- + new : `BlockChain` + Upgraded block chain object for this hard fork. + + """ + return old + + +def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: + """ + Obtain the list of hashes of the previous 256 blocks in order of + increasing block number. + + This function will return less hashes for the first 256 blocks. + + The ``BLOCKHASH`` opcode needs to access the latest hashes on the chain, + therefore this function retrieves them. + + Parameters + ---------- + chain : + History and current state. + + Returns + ------- + recent_block_hashes : `List[Hash32]` + Hashes of the recent 256 blocks in order of increasing block number. + + """ + recent_blocks = chain.blocks[-255:] + # TODO: This function has not been tested rigorously + if len(recent_blocks) == 0: + return [] + + recent_block_hashes = [] + + for block in recent_blocks: + prev_block_hash = block.header.parent_hash + recent_block_hashes.append(prev_block_hash) + + # We are computing the hash only for the most recent block and not for + # the rest of the blocks as they have successors which have the hash of + # the current block as parent hash. + most_recent_block_hash = keccak256(rlp.encode(recent_blocks[-1].header)) + recent_block_hashes.append(most_recent_block_hash) + + return recent_block_hashes + + +def state_transition(chain: BlockChain, block: Block) -> None: + """ + Attempts to apply a block to an existing block chain. + + All parts of the block's contents need to be verified before being added + to the chain. Blocks are verified by ensuring that the contents of the + block make logical sense with the contents of the parent block. The + information in the block's header must also match the corresponding + information in the block. + + To implement Ethereum, in theory clients are only required to store the + most recent 255 blocks of the chain since as far as execution is + concerned, only those blocks are accessed. Practically, however, clients + should store more blocks to handle reorgs. + + Parameters + ---------- + chain : + History and current state. + block : + Block to apply to `chain`. + + """ + if len(rlp.encode(block)) > MAX_RLP_BLOCK_SIZE: + raise InvalidBlock("Block rlp size exceeds MAX_RLP_BLOCK_SIZE") + + validate_header(chain, block.header) + if block.ommers != (): + raise InvalidBlock + + block_state = BlockState(pre_state=chain.state) + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=block_state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + prev_randao=block.header.prev_randao, + excess_blob_gas=block.header.excess_blob_gas, + parent_beacon_block_root=block.header.parent_beacon_block_root, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + withdrawals=block.withdrawals, + ) + block_diff = extract_block_diff(block_state) + block_state_root = compute_paged_state_root( + chain.state, + block_diff.account_changes, + block_diff.storage_changes, + ) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + withdrawals_root = root(block_output.withdrawals_trie) + requests_hash = compute_requests_hash(block_output.requests) + + if block_output.block_gas_used != block.header.gas_used: + raise InvalidBlock( + f"{block_output.block_gas_used} != {block.header.gas_used}" + ) + if transactions_root != block.header.transactions_root: + raise InvalidBlock + if block_state_root != block.header.state_root: + raise InvalidBlock + if receipt_root != block.header.receipt_root: + raise InvalidBlock + if block_logs_bloom != block.header.bloom: + raise InvalidBlock + if withdrawals_root != block.header.withdrawals_root: + raise InvalidBlock + if block_output.blob_gas_used != block.header.blob_gas_used: + raise InvalidBlock + if requests_hash != block.header.requests_hash: + raise InvalidBlock + + apply_changes_to_state(chain.state, block_diff) + chain.blocks.append(block) + if len(chain.blocks) > 255: + # Real clients have to store more blocks to deal with reorgs, but the + # protocol only requires the last 255 + chain.blocks = chain.blocks[-255:] + + +def calculate_base_fee_per_gas( + block_gas_limit: Uint, + parent_gas_limit: Uint, + parent_gas_used: Uint, + parent_base_fee_per_gas: Uint, +) -> Uint: + """ + Calculates the base fee per gas for the block. + + Parameters + ---------- + block_gas_limit : + Gas limit of the block for which the base fee is being calculated. + parent_gas_limit : + Gas limit of the parent block. + parent_gas_used : + Gas used in the parent block. + parent_base_fee_per_gas : + Base fee per gas of the parent block. + + Returns + ------- + base_fee_per_gas : `Uint` + Base fee per gas for the block. + + """ + parent_gas_target = parent_gas_limit // ELASTICITY_MULTIPLIER + if not check_gas_limit(block_gas_limit, parent_gas_limit): + raise InvalidBlock + + if parent_gas_used == parent_gas_target: + expected_base_fee_per_gas = parent_base_fee_per_gas + elif parent_gas_used > parent_gas_target: + gas_used_delta = parent_gas_used - parent_gas_target + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = max( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR, + Uint(1), + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas + base_fee_per_gas_delta + ) + else: + gas_used_delta = parent_gas_target - parent_gas_used + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = ( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas - base_fee_per_gas_delta + ) + + return Uint(expected_base_fee_per_gas) + + +def validate_header(chain: BlockChain, header: Header) -> None: + """ + Verifies a block header. + + In order to consider a block's header valid, the logic for the + quantities in the header should match the logic for the block itself. + For example the header timestamp should be greater than the block's parent + timestamp because the block was created *after* the parent block. + Additionally, the block's number should be directly following the parent + block's number since it is the next block in the sequence. + + Parameters + ---------- + chain : + History and current state. + header : + Header to check for correctness. + + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_header = chain.blocks[-1].header + + excess_blob_gas = calculate_excess_blob_gas(parent_header) + if header.excess_blob_gas != excess_blob_gas: + raise InvalidBlock + + if header.gas_used > header.gas_limit: + raise InvalidBlock + + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_header.base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: + raise InvalidBlock + if header.number != parent_header.number + Uint(1): + raise InvalidBlock + if len(header.extra_data) > 32: + raise InvalidBlock + if header.difficulty != 0: + raise InvalidBlock + if header.nonce != b"\x00\x00\x00\x00\x00\x00\x00\x00": + raise InvalidBlock + if header.ommers_hash != EMPTY_OMMER_HASH: + raise InvalidBlock + + block_parent_hash = keccak256(rlp.encode(parent_header)) + if header.parent_hash != block_parent_hash: + raise InvalidBlock + + +def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + tx_state: TransactionState, +) -> Tuple[Address, Uint, Tuple[VersionedHash, ...], U64]: + """ + Check if the transaction is includable in the block. + + Parameters + ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. + tx : + The transaction. + tx_state : + The transaction state tracker. + + Returns + ------- + sender_address : + The sender of the transaction. + effective_gas_price : + The price to charge for gas when the transaction is executed. + blob_versioned_hashes : + The blob versioned hashes of the transaction. + tx_blob_gas_used: + The blob gas used by the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not includable. + GasUsedExceedsLimitError : + If the gas used by the transaction exceeds the block's gas limit. + NonceMismatchError : + If the nonce of the transaction is not equal to the sender's nonce. + InsufficientBalanceError : + If the sender's balance is not enough to pay for the transaction. + InvalidSenderError : + If the transaction is from an address that does not exist anymore. + PriorityFeeGreaterThanMaxFeeError : + If the priority fee is greater than the maximum fee per gas. + InsufficientMaxFeePerGasError : + If the maximum fee per gas is insufficient for the transaction. + InsufficientMaxFeePerBlobGasError : + If the maximum fee per blob gas is insufficient for the transaction. + BlobGasLimitExceededError : + If the blob gas used by the transaction exceeds the block's blob gas + limit. + InvalidBlobVersionedHashError : + If the transaction contains a blob versioned hash with an invalid + version. + NoBlobDataError : + If the transaction is a type 3 but has no blobs. + BlobCountExceededError : + If the transaction is a type 3 and has more blobs than the limit. + TransactionTypeContractCreationError: + If the transaction type is not allowed to create contracts. + EmptyAuthorizationListError : + If the transaction is a SetCodeTransaction and the authorization list + is empty. + + """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used + blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used + + if tx.gas > gas_available: + raise GasUsedExceedsLimitError("gas used exceeds limit") + + tx_blob_gas_used = calculate_total_blob_gas(tx) + if tx_blob_gas_used > blob_gas_available: + raise BlobGasLimitExceededError("blob gas limit exceeded") + + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(tx_state, sender_address) + + if isinstance( + tx, (FeeMarketTransaction, BlobTransaction, SetCodeTransaction) + ): + if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: + raise PriorityFeeGreaterThanMaxFeeError( + "priority fee greater than max fee" + ) + if tx.max_fee_per_gas < block_env.base_fee_per_gas: + raise InsufficientMaxFeePerGasError( + tx.max_fee_per_gas, block_env.base_fee_per_gas + ) + + priority_fee_per_gas = min( + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, + ) + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas + else: + if tx.gas_price < block_env.base_fee_per_gas: + raise InvalidBlock + effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if isinstance(tx, BlobTransaction): + blob_count = len(tx.blob_versioned_hashes) + if blob_count == 0: + raise NoBlobDataError("no blob data in transaction") + if blob_count > BLOB_COUNT_LIMIT: + raise BlobCountExceededError( + f"Tx has {blob_count} blobs. Max allowed: {BLOB_COUNT_LIMIT}" + ) + for blob_versioned_hash in tx.blob_versioned_hashes: + if blob_versioned_hash[0:1] != VERSIONED_HASH_VERSION_KZG: + raise InvalidBlobVersionedHashError( + "invalid blob versioned hash" + ) + + blob_gas_price = calculate_blob_gas_price(block_env.excess_blob_gas) + if Uint(tx.max_fee_per_blob_gas) < blob_gas_price: + raise InsufficientMaxFeePerBlobGasError( + "insufficient max fee per blob gas" + ) + + max_gas_fee += Uint(calculate_total_blob_gas(tx)) * Uint( + tx.max_fee_per_blob_gas + ) + blob_versioned_hashes = tx.blob_versioned_hashes + else: + blob_versioned_hashes = () + + if isinstance(tx, (BlobTransaction, SetCodeTransaction)): + if not isinstance(tx.to, Address): + raise TransactionTypeContractCreationError(tx) + + if isinstance(tx, SetCodeTransaction): + if not any(tx.authorizations): + raise EmptyAuthorizationListError("empty authorization list") + + if sender_account.nonce > Uint(tx.nonce): + raise NonceMismatchError("nonce too low") + elif sender_account.nonce < Uint(tx.nonce): + raise NonceMismatchError("nonce too high") + + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InsufficientBalanceError("insufficient sender balance") + sender_code = get_code(tx_state, sender_account.code_hash) + if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( + sender_code + ): + raise InvalidSenderError("not EOA") + + return ( + sender_address, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) + + +def make_receipt( + tx: Transaction, + error: Optional[EthereumException], + cumulative_gas_used: Uint, + logs: Tuple[Log, ...], +) -> Bytes | Receipt: + """ + Make the receipt for a transaction that was executed. + + Parameters + ---------- + tx : + The executed transaction. + error : + Error in the top level frame of the transaction, if any. + cumulative_gas_used : + The total gas used so far in the block after the transaction was + executed. + logs : + The logs produced by the transaction. + + Returns + ------- + receipt : + The receipt for the transaction. + + """ + receipt = Receipt( + succeeded=error is None, + cumulative_gas_used=cumulative_gas_used, + bloom=logs_bloom(logs), + logs=logs, + ) + + return encode_receipt(tx, receipt) + + +def process_checked_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + data: Bytes, +) -> MessageCallOutput: + """ + Process a system transaction and raise an error if the contract does not + contain code or if the transaction fails. + + Parameters + ---------- + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + data : + Data to pass to the contract. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + + """ + # Pre-check that the system contract has code. We use a throwaway + # TransactionState here that is *never* propagated back to BlockState + # (no incorporate_tx_into_block call); the same get_account / get_code + # lookups are performed and properly tracked by + # process_unchecked_system_transaction below, which this function + # always calls. Reading via a TransactionState (rather than directly + # against pre_state) lets us see system contracts deployed earlier in + # the same block — see EIP-7002 and EIP-7251 for this edge case. + untracked_state = TransactionState(parent=block_env.state) + system_contract_code = get_code( + untracked_state, + get_account(untracked_state, target_address).code_hash, + ) + + if len(system_contract_code) == 0: + raise InvalidBlock( + f"System contract address {target_address.hex()} does not " + "contain code" + ) + + system_tx_output = process_unchecked_system_transaction( + block_env, + target_address, + data, + ) + + if system_tx_output.error: + raise InvalidBlock( + f"System contract ({target_address.hex()}) call failed: " + f"{system_tx_output.error}" + ) + + return system_tx_output + + +def process_unchecked_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + data: Bytes, +) -> MessageCallOutput: + """ + Process a system transaction without checking if the contract contains + code or if the transaction fails. + + Parameters + ---------- + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + data : + Data to pass to the contract. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + + """ + system_tx_state = TransactionState(parent=block_env.state) + system_contract_code = get_code( + system_tx_state, + get_account(system_tx_state, target_address).code_hash, + ) + + tx_env = vm.TransactionEnvironment( + origin=SYSTEM_ADDRESS, + gas_price=block_env.base_fee_per_gas, + gas=SYSTEM_TRANSACTION_GAS, + tx_gas_limit=SYSTEM_TRANSACTION_GAS, + access_list_addresses=set(), + access_list_storage_keys=set(), + state=system_tx_state, + blob_versioned_hashes=(), + authorizations=(), + index_in_block=None, + tx_hash=None, + ) + + system_tx_message = Message( + block_env=block_env, + tx_env=tx_env, + caller=SYSTEM_ADDRESS, + target=target_address, + gas=SYSTEM_TRANSACTION_GAS, + value=U256(0), + data=data, + code=system_contract_code, + depth=Uint(0), + current_target=target_address, + code_address=target_address, + should_transfer_value=False, + is_static=False, + accessed_addresses=set(), + accessed_storage_keys=set(), + disable_precompiles=False, + parent_evm=None, + disable_create_opcodes=False, + ) + + system_tx_output = process_message_call(system_tx_message) + + incorporate_tx_into_block(system_tx_state) + + return system_tx_output + + +def apply_body( + block_env: vm.BlockEnvironment, + transactions: Tuple[LegacyTransaction | Bytes, ...], + withdrawals: Tuple[Withdrawal, ...], +) -> vm.BlockOutput: + """ + Executes a block. + + Many of the contents of a block are stored in data structures called + tries. There is a transactions trie which is similar to a ledger of the + transactions stored in the current block. There is also a receipts trie + which stores the results of executing a transaction, like the post state + and gas used. This function creates and executes the block that is to be + added to the chain. + + Parameters + ---------- + block_env : + The block scoped environment. + transactions : + Transactions included in the block. + withdrawals : + Withdrawals to be processed in the current block. + + Returns + ------- + block_output : + The block output for the current block. + + """ + block_output = vm.BlockOutput() + forget_senders_authorities(block_env.state, block_env.number) + + process_unchecked_system_transaction( + block_env=block_env, + target_address=BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, + ) + + process_unchecked_system_transaction( + block_env=block_env, + target_address=HISTORY_STORAGE_ADDRESS, + data=block_env.block_hashes[-1], # The parent hash + ) + + for i, tx in enumerate(map(decode_transaction, transactions)): + process_transaction(block_env, block_output, tx, Uint(i)) + + process_withdrawals(block_env, block_output, withdrawals) + + process_general_purpose_requests( + block_env=block_env, + block_output=block_output, + ) + + return block_output + + +def process_general_purpose_requests( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, +) -> None: + """ + Process all the requests in the block. + + Parameters + ---------- + block_env : + The execution environment for the Block. + block_output : + The block output for the current block. + + """ + # Requests are to be in ascending order of request type + deposit_requests = parse_deposit_requests(block_output) + requests_from_execution = block_output.requests + if len(deposit_requests) > 0: + requests_from_execution.append(DEPOSIT_REQUEST_TYPE + deposit_requests) + + system_withdrawal_tx_output = process_checked_system_transaction( + block_env=block_env, + target_address=WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + data=b"", + ) + + if len(system_withdrawal_tx_output.return_data) > 0: + requests_from_execution.append( + WITHDRAWAL_REQUEST_TYPE + system_withdrawal_tx_output.return_data + ) + + system_consolidation_tx_output = process_checked_system_transaction( + block_env=block_env, + target_address=CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + data=b"", + ) + + if len(system_consolidation_tx_output.return_data) > 0: + requests_from_execution.append( + CONSOLIDATION_REQUEST_TYPE + + system_consolidation_tx_output.return_data + ) + + +def process_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: + """ + Execute a transaction against the provided environment. + + This function processes the actions needed to execute a transaction. + It decrements the sender's account balance after calculating the gas fee + and refunds them the proper amount after execution. Calling contracts, + deploying code, and incrementing nonces are all examples of actions that + happen within this function or from a call made within this function. + + Accounts that are marked for deletion are processed and destroyed after + execution. + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. + tx : + Transaction to execute. + index: + Index of the transaction in the block. + + """ + tx_state = TransactionState(parent=block_env.state) + + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + + intrinsic_gas, calldata_floor_gas_cost = validate_transaction(tx) + + ( + sender, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + tx_state=tx_state, + ) + + sender_account = get_account(tx_state, sender) + + if isinstance(tx, BlobTransaction): + blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) + else: + blob_gas_fee = Uint(0) + + effective_gas_fee = tx.gas * effective_gas_price + + gas = tx.gas - intrinsic_gas + increment_nonce(tx_state, sender) + + sender_balance_after_gas_fee = ( + Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee + ) + set_account_balance(tx_state, sender, U256(sender_balance_after_gas_fee)) + + access_list_addresses = set() + access_list_storage_keys = set() + access_list_addresses.add(block_env.coinbase) + if has_access_list(tx): + for access in tx.access_list: + access_list_addresses.add(access.account) + for slot in access.slots: + access_list_storage_keys.add((access.account, slot)) + + authorizations: Tuple[Authorization, ...] = () + if isinstance(tx, SetCodeTransaction): + authorizations = tx.authorizations + + tx_env = vm.TransactionEnvironment( + origin=sender, + gas_price=effective_gas_price, + gas=gas, + tx_gas_limit=tx.gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + state=tx_state, + blob_versioned_hashes=blob_versioned_hashes, + authorizations=authorizations, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) + + # For EIP-7623 we first calculate the execution_gas_used, which includes + # the execution gas refund. + # tx_gas_used_before_refund = tx.gas - tx_output.gas_left + # tx_gas_refund = min( + # tx_gas_used_before_refund // Uint(5), Uint(tx_output.refund_counter) + # ) + # tx_gas_used_after_refund = tx_gas_used_before_refund - tx_gas_refund + + # Transactions with less execution_gas_used than the floor pay at the + # floor cost. + # tx_gas_used_after_refund = max( + # tx_gas_used_after_refund, calldata_floor_gas_cost + # ) + + # tx_gas_left = tx.gas - tx_gas_used_after_refund + # gas_refund_amount = tx_gas_left * effective_gas_price + + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx.gas * priority_fee_per_gas + + # Monad: gas is not refunded to the sender (note absence of + # create_ether(tx_state, sender, U256(gas_refund_amount)) here). + add_sender_authority(block_env.state, block_env.number, sender) + + # transfer miner fees + create_ether(tx_state, block_env.coinbase, U256(transaction_fee)) + + for address in tx_output.accounts_to_delete: + destroy_account(tx_state, address) + + # block_output.block_gas_used += tx_gas_used_after_refund + block_output.block_gas_used += tx.gas + block_output.blob_gas_used += tx_blob_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_gas_used, tx_output.logs + ) + + receipt_key = rlp.encode(Uint(index)) + block_output.receipt_keys += (receipt_key,) + + trie_set( + block_output.receipts_trie, + receipt_key, + receipt, + ) + + block_output.block_logs += tx_output.logs + + incorporate_tx_into_block(tx_state) + + +def process_withdrawals( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + withdrawals: Tuple[Withdrawal, ...], +) -> None: + """ + Increase the balance of the withdrawing account. + """ + wd_state = TransactionState(parent=block_env.state) + + for i, wd in enumerate(withdrawals): + trie_set( + block_output.withdrawals_trie, + rlp.encode(Uint(i)), + rlp.encode(wd), + ) + + create_ether(wd_state, wd.address, wd.amount * U256(10**9)) + + incorporate_tx_into_block(wd_state) + + +def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: + """ + Validates the gas limit for a block. + + The bounds of the gas limit, ``max_adjustment_delta``, is set as the + quotient of the parent block's gas limit and the + ``LIMIT_ADJUSTMENT_FACTOR``. Therefore, if the gas limit that is passed + through as a parameter is greater than or equal to the *sum* of the + parent's gas and the adjustment delta then the limit for gas is too high + and fails this function's check. Similarly, if the limit is less than or + equal to the *difference* of the parent's gas and the adjustment delta *or* + the predefined ``LIMIT_MINIMUM`` then this function's check fails because + the gas limit doesn't allow for a sufficient or reasonable amount of gas to + be used on a block. + + Parameters + ---------- + gas_limit : + Gas limit to validate. + + parent_gas_limit : + Gas limit of the parent block. + + Returns + ------- + check : `bool` + True if gas limit constraints are satisfied, False otherwise. + + """ + max_adjustment_delta = parent_gas_limit // GasCosts.LIMIT_ADJUSTMENT_FACTOR + if gas_limit >= parent_gas_limit + max_adjustment_delta: + return False + if gas_limit <= parent_gas_limit - max_adjustment_delta: + return False + if gas_limit < GasCosts.LIMIT_MINIMUM: + return False + + return True diff --git a/src/ethereum/forks/monad_next/fork_types.py b/src/ethereum/forks/monad_next/fork_types.py new file mode 100644 index 00000000000..4bec34810db --- /dev/null +++ b/src/ethereum/forks/monad_next/fork_types.py @@ -0,0 +1,60 @@ +""" +Ethereum Types. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Types reused throughout the specification, which are specific to Ethereum. +""" + +from dataclasses import dataclass +from typing import final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes256 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U8, U64, U256 + +from ethereum.crypto.hash import Hash32 +from ethereum.state import Account, Address + +VersionedHash = Hash32 + +Bloom = Bytes256 + + +def encode_account(raw_account_data: Account, storage_root: Bytes) -> Bytes: + """ + Encode `Account` dataclass. + + Storage is not stored in the `Account` dataclass, so `Accounts` cannot be + encoded without providing a storage root. + """ + return rlp.encode( + ( + raw_account_data.nonce, + raw_account_data.balance, + storage_root, + raw_account_data.code_hash, + ) + ) + + +@final +@slotted_freezable +@dataclass +class Authorization: + """ + The authorization for a set code transaction. + """ + + chain_id: U256 + address: Address + nonce: U64 + y_parity: U8 + r: U256 + s: U256 diff --git a/src/ethereum/forks/monad_next/requests.py b/src/ethereum/forks/monad_next/requests.py new file mode 100644 index 00000000000..df2c2901ab4 --- /dev/null +++ b/src/ethereum/forks/monad_next/requests.py @@ -0,0 +1,308 @@ +""" +[EIP-7685] generalizes how the execution layer communicates validator actions +to the consensus layer. Rather than adding a dedicated header field for each +new action type (as [EIP-4895] did for withdrawals), the execution header +commits to a single [`requests_hash`][rh] that aggregates an ordered list of +typed requests. + +Each request is a type byte (see [`DEPOSIT_REQUEST_TYPE`][dt], +[`WITHDRAWAL_REQUEST_TYPE`][wt], and [`CONSOLIDATION_REQUEST_TYPE`][ct]) +followed by an opaque payload. Deposit requests are discovered by scanning +transaction receipts for logs emitted by the deposit contract; withdrawal +and consolidation requests are produced by the corresponding system +contracts during block processing. + +See [`parse_deposit_requests`][pd] for how deposit logs become request data, +[`compute_requests_hash`][crh] for how the list is hashed for inclusion in the +header, and [`process_general_purpose_requests`][pgpr] for how the requests are +processed. + +[EIP-4895]: https://eips.ethereum.org/EIPS/eip-4895 +[EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 +[rh]: ref:ethereum.forks.monad_next.blocks.Header.requests_hash +[dt]: ref:ethereum.forks.monad_next.requests.DEPOSIT_REQUEST_TYPE +[wt]: ref:ethereum.forks.monad_next.requests.WITHDRAWAL_REQUEST_TYPE +[ct]: ref:ethereum.forks.monad_next.requests.CONSOLIDATION_REQUEST_TYPE +[pd]: ref:ethereum.forks.monad_next.requests.parse_deposit_requests +[crh]: ref:ethereum.forks.monad_next.requests.compute_requests_hash +[pgpr]: ref:ethereum.forks.monad_next.fork.process_general_purpose_requests +""" + +from hashlib import sha256 +from typing import List + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint, ulen + +from ethereum.exceptions import InvalidBlock +from ethereum.merkle_patricia_trie import trie_get +from ethereum.utils.hexadecimal import hex_to_bytes32 + +from .blocks import decode_receipt +from .utils.hexadecimal import hex_to_address +from .vm import BlockOutput + +DEPOSIT_CONTRACT_ADDRESS = hex_to_address( + "0x00000000219ab540356cbb839cbe05303d7705fa" +) +""" +Mainnet address of the beacon chain deposit contract. Scanning block +receipts for logs emitted by this address is how the execution layer +discovers validator deposits, per [EIP-6110]. + +[EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110 +""" + +DEPOSIT_EVENT_SIGNATURE_HASH = hex_to_bytes32( + "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5" +) +""" +First [log topic] of the deposit contract's `DepositEvent`, equal to the +keccak256 of its Solidity event signature. Logs whose first topic does not +match this are ignored when collecting deposit requests. + +[log topic]: https://docs.soliditylang.org/en/latest/abi-spec.html#events +""" + +DEPOSIT_REQUEST_TYPE = b"\x00" +""" +Request type byte identifying a deposit request, per [EIP-6110]. + +[EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110 +""" + +WITHDRAWAL_REQUEST_TYPE = b"\x01" +""" +Request type byte identifying an execution-triggered withdrawal request, +per [EIP-7002]. + +[EIP-7002]: https://eips.ethereum.org/EIPS/eip-7002 +""" + +CONSOLIDATION_REQUEST_TYPE = b"\x02" +""" +Request type byte identifying a consolidation request, per [EIP-7251]. + +[EIP-7251]: https://eips.ethereum.org/EIPS/eip-7251 +""" + + +DEPOSIT_EVENT_LENGTH = Uint(576) +""" +Total length in bytes of the ABI-encoded `DepositEvent` data payload. Every +well-formed event has this exact length. +""" + +PUBKEY_OFFSET = Uint(160) +""" +Position within the event payload of the validator public key's length +prefix, as emitted by the Solidity ABI encoder. +""" + +WITHDRAWAL_CREDENTIALS_OFFSET = Uint(256) +""" +Position within the event payload of the withdrawal credentials' length +prefix. +""" + +AMOUNT_OFFSET = Uint(320) +""" +Position within the event payload of the deposit amount's length prefix. +""" + +SIGNATURE_OFFSET = Uint(384) +""" +Position within the event payload of the deposit signature's length prefix. +""" + +INDEX_OFFSET = Uint(512) +""" +Position within the event payload of the deposit index's length prefix. +""" + +PUBKEY_SIZE = Uint(48) +""" +Length of the BLS12-381 public key that identifies the validator receiving +the deposit. +""" + +WITHDRAWAL_CREDENTIALS_SIZE = Uint(32) +""" +Length of the withdrawal credentials, which determine where the staked +ether may eventually be withdrawn. +""" + +AMOUNT_SIZE = Uint(8) +""" +Length of the little-endian Gwei amount being deposited. +""" + +SIGNATURE_SIZE = Uint(96) +""" +Length of the BLS12-381 signature over the deposit message. +""" + +INDEX_SIZE = Uint(8) +""" +Length of the monotonically-increasing deposit index assigned by the +deposit contract when it emits the event. +""" + + +def extract_deposit_data(data: Bytes) -> Bytes: + """ + Strip the Solidity ABI framing from a `DepositEvent` payload and return + the concatenated raw fields in the order consumed by the consensus + layer: public key, withdrawal credentials, amount, signature, and + deposit index. + + Because each field has a fixed length, every well-formed event has an + identical byte layout. Any deviation indicates a misbehaving or + compromised deposit contract, so this function raises [`InvalidBlock`] + rather than silently accepting unexpected data. + + [`InvalidBlock`]: ref:ethereum.exceptions.InvalidBlock + """ + if ulen(data) != DEPOSIT_EVENT_LENGTH: + raise InvalidBlock("Invalid deposit event data length") + + # Check that all the offsets are in order + pubkey_offset = Uint.from_be_bytes(data[0:32]) + if pubkey_offset != PUBKEY_OFFSET: + raise InvalidBlock("Invalid pubkey offset in deposit log") + + withdrawal_credentials_offset = Uint.from_be_bytes(data[32:64]) + if withdrawal_credentials_offset != WITHDRAWAL_CREDENTIALS_OFFSET: + raise InvalidBlock( + "Invalid withdrawal credentials offset in deposit log" + ) + + amount_offset = Uint.from_be_bytes(data[64:96]) + if amount_offset != AMOUNT_OFFSET: + raise InvalidBlock("Invalid amount offset in deposit log") + + signature_offset = Uint.from_be_bytes(data[96:128]) + if signature_offset != SIGNATURE_OFFSET: + raise InvalidBlock("Invalid signature offset in deposit log") + + index_offset = Uint.from_be_bytes(data[128:160]) + if index_offset != INDEX_OFFSET: + raise InvalidBlock("Invalid index offset in deposit log") + + # Check that all the sizes are in order + pubkey_size = Uint.from_be_bytes( + data[pubkey_offset : pubkey_offset + Uint(32)] + ) + if pubkey_size != PUBKEY_SIZE: + raise InvalidBlock("Invalid pubkey size in deposit log") + + pubkey = data[ + pubkey_offset + Uint(32) : pubkey_offset + Uint(32) + PUBKEY_SIZE + ] + + withdrawal_credentials_size = Uint.from_be_bytes( + data[ + withdrawal_credentials_offset : withdrawal_credentials_offset + + Uint(32) + ], + ) + if withdrawal_credentials_size != WITHDRAWAL_CREDENTIALS_SIZE: + raise InvalidBlock( + "Invalid withdrawal credentials size in deposit log" + ) + + withdrawal_credentials = data[ + withdrawal_credentials_offset + + Uint(32) : withdrawal_credentials_offset + + Uint(32) + + WITHDRAWAL_CREDENTIALS_SIZE + ] + + amount_size = Uint.from_be_bytes( + data[amount_offset : amount_offset + Uint(32)] + ) + if amount_size != AMOUNT_SIZE: + raise InvalidBlock("Invalid amount size in deposit log") + + amount = data[ + amount_offset + Uint(32) : amount_offset + Uint(32) + AMOUNT_SIZE + ] + + signature_size = Uint.from_be_bytes( + data[signature_offset : signature_offset + Uint(32)] + ) + if signature_size != SIGNATURE_SIZE: + raise InvalidBlock("Invalid signature size in deposit log") + + signature = data[ + signature_offset + Uint(32) : signature_offset + + Uint(32) + + SIGNATURE_SIZE + ] + + index_size = Uint.from_be_bytes( + data[index_offset : index_offset + Uint(32)] + ) + if index_size != INDEX_SIZE: + raise InvalidBlock("Invalid index size in deposit log") + + index = data[ + index_offset + Uint(32) : index_offset + Uint(32) + INDEX_SIZE + ] + + return pubkey + withdrawal_credentials + amount + signature + index + + +def parse_deposit_requests(block_output: BlockOutput) -> Bytes: + """ + Walk the receipts produced during block execution, concatenating the + raw payload of every valid deposit event into a single byte string. + + A log is considered a deposit when it originates from + [`DEPOSIT_CONTRACT_ADDRESS`][addr] and its first topic matches + [`DEPOSIT_EVENT_SIGNATURE_HASH`][sig]. The returned bytes are the + direct concatenation of the unframed deposit fields, ready to be + prefixed with [`DEPOSIT_REQUEST_TYPE`][dt] before being appended to + the block's request list. + + [addr]: ref:ethereum.forks.monad_next.requests.DEPOSIT_CONTRACT_ADDRESS + [sig]: ref:ethereum.forks.monad_next.requests.DEPOSIT_EVENT_SIGNATURE_HASH + [dt]: ref:ethereum.forks.monad_next.requests.DEPOSIT_REQUEST_TYPE + """ + deposit_requests: Bytes = b"" + for key in block_output.receipt_keys: + receipt = trie_get(block_output.receipts_trie, key) + assert receipt is not None + decoded_receipt = decode_receipt(receipt) + for log in decoded_receipt.logs: + if log.address == DEPOSIT_CONTRACT_ADDRESS: + if ( + len(log.topics) > 0 + and log.topics[0] == DEPOSIT_EVENT_SIGNATURE_HASH + ): + request = extract_deposit_data(log.data) + deposit_requests += request + + return deposit_requests + + +def compute_requests_hash(requests: List[Bytes]) -> Bytes: + """ + Compute the [SHA2-256] commitment over an ordered list of + type-prefixed requests, as defined by [EIP-7685]. + + The commitment is the SHA2-256 hash of the concatenation of the + SHA2-256 hashes of each individual request. This is what the + execution header's [`requests_hash`][rh] stores, and what the + consensus layer re-derives to validate that both layers observed the + same set of requests. + + [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 + [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 + [rh]: ref:ethereum.forks.monad_next.blocks.Header.requests_hash + """ + m = sha256() + for request in requests: + m.update(sha256(request).digest()) + + return m.digest() diff --git a/src/ethereum/forks/monad_next/state_tracker.py b/src/ethereum/forks/monad_next/state_tracker.py new file mode 100644 index 00000000000..5b3d4be0bd2 --- /dev/null +++ b/src/ethereum/forks/monad_next/state_tracker.py @@ -0,0 +1,841 @@ +""" +State Tracking for Block Execution. + +Track state changes on top of a read-only ``PreState``. At block end, +accumulated diffs feed into +``PreState.compute_state_root_and_trie_changes()``. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Replace the mutable ``State`` class with lightweight state trackers that +record diffs. ``BlockState`` accumulates committed transaction +changes across a block. ``TransactionState`` tracks in-flight changes +within a single transaction and supports copy-on-write rollback. +""" + +from dataclasses import dataclass, field +from typing import Callable, Dict, Optional, Set, Tuple, final + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.frozen import modify +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.state import ( + EMPTY_ACCOUNT, + EMPTY_CODE_HASH, + Account, + Address, + BlockDiff, + PreState, +) + + +@final +@dataclass +class BlockState: + """ + Accumulate committed transaction-level changes across a block. + + Read chain: block writes -> pre_state. + """ + + pre_state: PreState + account_writes: Dict[Address, Optional[Account]] = field( + default_factory=dict + ) + storage_writes: Dict[Address, Dict[Bytes32, U256]] = field( + default_factory=dict + ) + code_writes: Dict[Hash32, Bytes] = field(default_factory=dict) + # Monad: per-block-number set of EOAs that sent or were authorized in + # that block. Carried across blocks via t8n; used by the reserve + # balance exception window (RESERVE_BALANCE_DELAY_BLOCKS lookback). + _senders_authorities: Dict[Uint, Set[Address]] = field( + default_factory=dict + ) + + +@final +@dataclass +class TransactionState: + """ + Track in-flight state changes within a single transaction. + + Read chain: tx writes -> block writes -> pre_state. + """ + + parent: BlockState + account_writes: Dict[Address, Optional[Account]] = field( + default_factory=dict + ) + storage_writes: Dict[Address, Dict[Bytes32, U256]] = field( + default_factory=dict + ) + code_writes: Dict[Hash32, Bytes] = field(default_factory=dict) + created_accounts: Set[Address] = field(default_factory=set) + transient_storage: Dict[Tuple[Address, Bytes32], U256] = field( + default_factory=dict + ) + + +def get_account_optional( + tx_state: TransactionState, address: Address +) -> Optional[Account]: + """ + Get the ``Account`` object at an address. Return ``None`` (rather than + ``EMPTY_ACCOUNT``) if there is no account at the address. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to look up. + + Returns + ------- + account : ``Optional[Account]`` + Account at address. + + """ + if address in tx_state.account_writes: + return tx_state.account_writes[address] + if address in tx_state.parent.account_writes: + return tx_state.parent.account_writes[address] + return tx_state.parent.pre_state.get_account_optional(address) + + +def get_account(tx_state: TransactionState, address: Address) -> Account: + """ + Get the ``Account`` object at an address. Return ``EMPTY_ACCOUNT`` + if there is no account at the address. + + Use ``get_account_optional()`` if you care about the difference + between a non-existent account and ``EMPTY_ACCOUNT``. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to look up. + + Returns + ------- + account : ``Account`` + Account at address. + + """ + account = get_account_optional(tx_state, address) + if account is None: + return EMPTY_ACCOUNT + else: + return account + + +def get_code(tx_state: TransactionState, code_hash: Hash32) -> Bytes: + """ + Get the bytecode for a given code hash. + + Read chain: tx code_writes -> block code_writes -> pre_state. + + Parameters + ---------- + tx_state : + The transaction state. + code_hash : + Hash of the code to look up. + + Returns + ------- + code : ``Bytes`` + The bytecode. + + """ + if code_hash == EMPTY_CODE_HASH: + return b"" + if code_hash in tx_state.code_writes: + return tx_state.code_writes[code_hash] + if code_hash in tx_state.parent.code_writes: + return tx_state.parent.code_writes[code_hash] + return tx_state.parent.pre_state.get_code(code_hash) + + +def get_storage( + tx_state: TransactionState, address: Address, key: Bytes32 +) -> U256: + """ + Get a value at a storage key on an account. Return ``U256(0)`` if + the storage key has not been set previously. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account. + key : + Key to look up. + + Returns + ------- + value : ``U256`` + Value at the key. + + """ + if address in tx_state.storage_writes: + if key in tx_state.storage_writes[address]: + return tx_state.storage_writes[address][key] + if address in tx_state.parent.storage_writes: + if key in tx_state.parent.storage_writes[address]: + return tx_state.parent.storage_writes[address][key] + return tx_state.parent.pre_state.get_storage(address, key) + + +def get_storage_original( + tx_state: TransactionState, address: Address, key: Bytes32 +) -> U256: + """ + Get the original value in a storage slot i.e. the value before the + current transaction began. Read from block-level writes, then + pre_state. Return ``U256(0)`` for accounts created in the current + transaction. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account to read the value from. + key : + Key of the storage slot. + + """ + if address in tx_state.created_accounts: + return U256(0) + if address in tx_state.parent.storage_writes: + if key in tx_state.parent.storage_writes[address]: + return tx_state.parent.storage_writes[address][key] + return tx_state.parent.pre_state.get_storage(address, key) + + +def get_transient_storage( + tx_state: TransactionState, address: Address, key: Bytes32 +) -> U256: + """ + Get a value at a storage key on an account from transient storage. + Return ``U256(0)`` if the storage key has not been set previously. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account. + key : + Key to look up. + + Returns + ------- + value : ``U256`` + Value at the key. + + """ + return tx_state.transient_storage.get((address, key), U256(0)) + + +def account_exists(tx_state: TransactionState, address: Address) -> bool: + """ + Check if an account exists in the state trie. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + account_exists : ``bool`` + True if account exists in the state trie, False otherwise. + + """ + return get_account_optional(tx_state, address) is not None + + +def account_has_code_or_nonce( + tx_state: TransactionState, address: Address +) -> bool: + """ + Check if an account has non-zero nonce or non-empty code. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + has_code_or_nonce : ``bool`` + True if the account has non-zero nonce or non-empty code, + False otherwise. + + """ + account = get_account(tx_state, address) + return account.nonce != Uint(0) or account.code_hash != EMPTY_CODE_HASH + + +def account_has_storage(tx_state: TransactionState, address: Address) -> bool: + """ + Check if an account has storage. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + has_storage : ``bool`` + True if the account has storage, False otherwise. + + """ + if tx_state.storage_writes.get(address): + return True + if tx_state.parent.storage_writes.get(address): + return True + return tx_state.parent.pre_state.account_has_storage(address) + + +def account_exists_and_is_empty( + tx_state: TransactionState, address: Address +) -> bool: + """ + Check if an account exists and has zero nonce, empty code and zero + balance. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + exists_and_is_empty : ``bool`` + True if an account exists and has zero nonce, empty code and + zero balance, False otherwise. + + """ + account = get_account_optional(tx_state, address) + return ( + account is not None + and account.nonce == Uint(0) + and account.code_hash == EMPTY_CODE_HASH + and account.balance == 0 + ) + + +def is_account_alive(tx_state: TransactionState, address: Address) -> bool: + """ + Check whether an account is both in the state and non-empty. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + is_alive : ``bool`` + True if the account is alive. + + """ + account = get_account_optional(tx_state, address) + return account is not None and account != EMPTY_ACCOUNT + + +def set_account( + tx_state: TransactionState, + address: Address, + account: Optional[Account], +) -> None: + """ + Set the ``Account`` object at an address. Setting to ``None`` + deletes the account (but not its storage, see + ``destroy_account()``). + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to set. + account : + Account to set at address. + + """ + tx_state.account_writes[address] = account + + +def set_storage( + tx_state: TransactionState, + address: Address, + key: Bytes32, + value: U256, +) -> None: + """ + Set a value at a storage key on an account. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account. + key : + Key to set. + value : + Value to set at the key. + + """ + assert get_account_optional(tx_state, address) is not None + if address not in tx_state.storage_writes: + tx_state.storage_writes[address] = {} + tx_state.storage_writes[address][key] = value + + +def destroy_account(tx_state: TransactionState, address: Address) -> None: + """ + Completely remove the account at ``address`` and all of its storage. + + This function is made available exclusively for the ``SELFDESTRUCT`` + opcode. It is expected that ``SELFDESTRUCT`` will be disabled in a + future hardfork and this function will be removed. Only supports same + transaction destruction. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of account to destroy. + + """ + destroy_storage(tx_state, address) + set_account(tx_state, address, None) + + +def destroy_storage(tx_state: TransactionState, address: Address) -> None: + """ + Completely remove the storage at ``address``. + + Only supports same transaction destruction. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of account whose storage is to be deleted. + + """ + if address in tx_state.storage_writes: + del tx_state.storage_writes[address] + + +def mark_account_created(tx_state: TransactionState, address: Address) -> None: + """ + Mark an account as having been created in the current transaction. + This information is used by ``get_storage_original()`` to handle an + obscure edgecase, and to respect the constraints added to + SELFDESTRUCT by EIP-6780. + + The marker is not removed even if the account creation reverts. + Since the account cannot have had code prior to its creation and + can't call ``get_storage_original()``, this is harmless. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that has been created. + + """ + tx_state.created_accounts.add(address) + + +def set_transient_storage( + tx_state: TransactionState, + address: Address, + key: Bytes32, + value: U256, +) -> None: + """ + Set a value at a storage key on an account in transient storage. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account. + key : + Key to set. + value : + Value to set at the key. + + """ + if value == U256(0): + tx_state.transient_storage.pop((address, key), None) + else: + tx_state.transient_storage[(address, key)] = value + + +def modify_state( + tx_state: TransactionState, + address: Address, + f: Callable[[Account], None], +) -> None: + """ + Modify an ``Account`` in the state. If, after modification, the + account exists and has zero nonce, empty code, and zero balance, it + is destroyed. + """ + set_account(tx_state, address, modify(get_account(tx_state, address), f)) + if account_exists_and_is_empty(tx_state, address): + destroy_account(tx_state, address) + + +def move_ether( + tx_state: TransactionState, + sender_address: Address, + recipient_address: Address, + amount: U256, +) -> None: + """ + Move funds between accounts. + + Parameters + ---------- + tx_state : + The transaction state. + sender_address : + Address of the sender. + recipient_address : + Address of the recipient. + amount : + The amount to transfer. + + """ + + def reduce_sender_balance(sender: Account) -> None: + if sender.balance < amount: + raise AssertionError + sender.balance -= amount + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += amount + + modify_state(tx_state, sender_address, reduce_sender_balance) + modify_state(tx_state, recipient_address, increase_recipient_balance) + + +def create_ether( + tx_state: TransactionState, address: Address, amount: U256 +) -> None: + """ + Add newly created ether to an account. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account to which ether is added. + amount : + The amount of ether to be added to the account of interest. + + """ + + def increase_balance(account: Account) -> None: + account.balance += amount + + modify_state(tx_state, address, increase_balance) + + +def set_account_balance( + tx_state: TransactionState, address: Address, amount: U256 +) -> None: + """ + Set the balance of an account. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account whose balance needs to be set. + amount : + The amount that needs to be set in the balance. + + """ + + def set_balance(account: Account) -> None: + account.balance = amount + + modify_state(tx_state, address, set_balance) + + +def increment_nonce(tx_state: TransactionState, address: Address) -> None: + """ + Increment the nonce of an account. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account whose nonce needs to be incremented. + + """ + + def increase_nonce(sender: Account) -> None: + sender.nonce += Uint(1) + + modify_state(tx_state, address, increase_nonce) + + +def set_code( + tx_state: TransactionState, address: Address, code: Bytes +) -> None: + """ + Set Account code. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account whose code needs to be updated. + code : + The bytecode that needs to be set. + + """ + code_hash = keccak256(code) + if code_hash != EMPTY_CODE_HASH: + tx_state.code_writes[code_hash] = code + + def write_code_hash(sender: Account) -> None: + sender.code_hash = code_hash + + modify_state(tx_state, address, write_code_hash) + + +# -- Snapshot / Rollback --------------------------------------------------- + + +def copy_tx_state(tx_state: TransactionState) -> TransactionState: + """ + Create a snapshot of the transaction state for rollback. + + Deep-copy writes and transient storage. The parent reference and + ``created_accounts`` are shared (not rolled back). + + Parameters + ---------- + tx_state : + The transaction state to snapshot. + + Returns + ------- + snapshot : ``TransactionState`` + A copy of the transaction state. + + """ + return TransactionState( + parent=tx_state.parent, + account_writes=dict(tx_state.account_writes), + storage_writes={ + addr: dict(slots) + for addr, slots in tx_state.storage_writes.items() + }, + code_writes=dict(tx_state.code_writes), + created_accounts=tx_state.created_accounts, + transient_storage=dict(tx_state.transient_storage), + ) + + +def restore_tx_state( + tx_state: TransactionState, snapshot: TransactionState +) -> None: + """ + Restore transaction state from a snapshot (rollback on failure). + + Parameters + ---------- + tx_state : + The transaction state to restore. + snapshot : + The snapshot to restore from. + + """ + tx_state.account_writes = snapshot.account_writes + tx_state.storage_writes = snapshot.storage_writes + tx_state.code_writes = snapshot.code_writes + tx_state.transient_storage = snapshot.transient_storage + + +# -- Lifecycle -------------------------------------------------------------- + + +def incorporate_tx_into_block(tx_state: TransactionState) -> None: + """ + Merge transaction writes into the block state and clear for reuse. + + Parameters + ---------- + tx_state : + The transaction state to commit. + + """ + block = tx_state.parent + + for address, account in tx_state.account_writes.items(): + block.account_writes[address] = account + + for address, slots in tx_state.storage_writes.items(): + if address not in block.storage_writes: + block.storage_writes[address] = {} + block.storage_writes[address].update(slots) + + block.code_writes.update(tx_state.code_writes) + + tx_state.account_writes.clear() + tx_state.storage_writes.clear() + tx_state.code_writes.clear() + tx_state.created_accounts.clear() + tx_state.transient_storage.clear() + + +def extract_block_diff(block_state: BlockState) -> BlockDiff: + """ + Extract account, storage, and code diff from the block state. + + Parameters + ---------- + block_state : + The block state. + + Returns + ------- + diff : `BlockDiff` + Account, storage, and code changes accumulated during block execution. + + """ + return BlockDiff( + account_changes=block_state.account_writes, + storage_changes=block_state.storage_writes, + code_changes=block_state.code_writes, + ) + + +# Monad-specific: reserve balance exception window. +RESERVE_BALANCE_DELAY_BLOCKS = Uint(3) + + +def add_sender_authority( + block_state: BlockState, + block_number: Uint, + address: Address, +) -> None: + """ + Record `address` as a sender or authority in the given block number. + + Used by the reserve balance "emptying transaction" exception. + """ + block_state._senders_authorities.setdefault(block_number, set()).add( + address + ) + + +def is_sender_authority( + block_state: BlockState, + address: Address, +) -> bool: + """ + Return True if `address` was a sender or authority in any of the + block numbers currently retained on `block_state`. + """ + for ( + senders_authorities_at_block + ) in block_state._senders_authorities.values(): + if address in senders_authorities_at_block: + return True + return False + + +def forget_senders_authorities( + block_state: BlockState, + current_block_number: Uint, +) -> None: + """ + Prune senders/authorities older than the reserve balance delay window. + """ + if current_block_number >= RESERVE_BALANCE_DELAY_BLOCKS: + number = current_block_number - RESERVE_BALANCE_DELAY_BLOCKS + block_state._senders_authorities.pop(number, None) + + +def get_balance_original(snapshot: TransactionState, address: Address) -> U256: + """ + Get the balance of `address` as it was at the start of the + top-level EVM execution (i.e. after pre-execution gas/nonce + deduction but before any EVM-driven writes). + + Reads from a ``TransactionState`` captured at top-level EVM begin + (the same ``snapshot`` used for rollback in the interpreter). Read + chain: snapshot.account_writes -> block writes -> pre_state. + """ + account = get_account_optional(snapshot, address) + if account is None: + return U256(0) + return account.balance + + +def iter_all_addresses(tx_state: TransactionState) -> Set[Address]: + """ + Return every address touched by pre-state, block, or current tx. + + Union of pre-state addresses, block-level writes, and current-tx + writes. Reproduces the old ``state._main_trie._data.keys()`` view + used by Monad's end-of-tx reserve-balance sweep. Assumes the + concrete ``State`` from ``ethereum.state`` (t8n loads its alloc + into one) so ``_main_trie._data`` is enumerable. + """ + pre_state = tx_state.parent.pre_state + pre_addrs: Set[Address] + if hasattr(pre_state, "_main_trie"): + pre_addrs = set(pre_state._main_trie._data.keys()) + else: + pre_addrs = set() + return ( + pre_addrs + | set(tx_state.parent.account_writes.keys()) + | set(tx_state.account_writes.keys()) + ) diff --git a/src/ethereum/forks/monad_next/transactions.py b/src/ethereum/forks/monad_next/transactions.py new file mode 100644 index 00000000000..04c4b77388c --- /dev/null +++ b/src/ethereum/forks/monad_next/transactions.py @@ -0,0 +1,879 @@ +""" +Transactions are atomic units of work created externally to Ethereum and +submitted to be executed. If Ethereum is viewed as a state machine, +transactions are the events that move between states. +""" + +from dataclasses import dataclass +from typing import Tuple, TypeGuard, final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes0, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint, ulen + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import ( + InsufficientTransactionGasError, + InvalidSignatureError, + NonceOverflowError, +) +from ethereum.state import Address + +from .exceptions import ( + InitCodeTooLargeError, + TransactionGasLimitExceededError, + TransactionTypeError, +) +from .fork_types import Authorization, VersionedHash + +TX_MAX_GAS_LIMIT = Uint(30_000_000) + + +@final +@slotted_freezable +@dataclass +class LegacyTransaction: + """ + Atomic operation performed on the block chain. This represents the original + transaction format used before [EIP-1559], [EIP-2930], [EIP-4844], + and [EIP-7702]. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + gas_price: Uint + """ + The price of gas for this transaction, in wei. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + v: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@final +@slotted_freezable +@dataclass +class Access: + """ + A mapping from account address to storage slots that are pre-warmed as part + of a transaction. + """ + + account: Address + """ + The address of the account that is accessed. + """ + + slots: Tuple[Bytes32, ...] + """ + A tuple of storage slots that are accessed in the account. + """ + + +@final +@slotted_freezable +@dataclass +class AccessListTransaction: + """ + The transaction type added in [EIP-2930] to support access lists. + + This transaction type extends the legacy transaction with an access list + and chain ID. The access list specifies which addresses and storage slots + the transaction will access. + + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + gas_price: Uint + """ + The price of gas for this transaction. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@final +@slotted_freezable +@dataclass +class FeeMarketTransaction: + """ + The transaction type added in [EIP-1559]. + + This transaction type introduces a new fee market mechanism with two gas + price parameters: max_priority_fee_per_gas and max_fee_per_gas. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@final +@slotted_freezable +@dataclass +class BlobTransaction: + """ + The transaction type added in [EIP-4844]. + + This transaction type extends the fee market transaction to support + blob-carrying transactions. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + max_fee_per_blob_gas: U256 + """ + The maximum fee per blob gas that the sender is willing to pay. + """ + + blob_versioned_hashes: Tuple[VersionedHash, ...] + """ + A tuple of objects that represent the versioned hashes of the blobs + included in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@final +@slotted_freezable +@dataclass +class SetCodeTransaction: + """ + The transaction type added in [EIP-7702]. + + This transaction type allows Ethereum Externally Owned Accounts (EOAs) + to set code on their account, enabling them to act as smart contracts. + + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U64 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + authorizations: Tuple[Authorization, ...] + """ + A tuple of `Authorization` objects that specify what code the signer + desires to execute in the context of their EOA. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +Transaction = ( + LegacyTransaction + | AccessListTransaction + | FeeMarketTransaction + | BlobTransaction + | SetCodeTransaction +) +""" +Union type representing any valid transaction type. +""" + + +AccessListCapableTransaction = ( + AccessListTransaction + | FeeMarketTransaction + | BlobTransaction + | SetCodeTransaction +) +""" +Transaction types that include an [EIP-2930]-style access list. + +See [`has_access_list`][hal] and [`Access`][a] for more details. + +[EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 +[hal]: ref:ethereum.forks.amsterdam.transactions.has_access_list +[a]: ref:ethereum.forks.amsterdam.transactions.Access +""" + + +def encode_transaction(tx: Transaction) -> LegacyTransaction | Bytes: + """ + Encode a transaction into its RLP or typed transaction format. + Needed because non-legacy transactions aren't RLP. + + Legacy transactions are returned as-is, while other transaction types + are prefixed with their type identifier and RLP encoded. + """ + if isinstance(tx, LegacyTransaction): + return tx + elif isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(tx) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(tx) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(tx) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + rlp.encode(tx) + else: + raise Exception(f"Unable to encode transaction of type {type(tx)}") + + +def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: + """ + Decode a transaction from its RLP or typed transaction format. + Needed because non-legacy transactions aren't RLP. + + Legacy transactions are returned as-is, while other transaction types + are decoded based on their type identifier prefix. + """ + if isinstance(tx, Bytes): + if tx[0] == 1: + return rlp.decode_to(AccessListTransaction, tx[1:]) + elif tx[0] == 2: + return rlp.decode_to(FeeMarketTransaction, tx[1:]) + elif tx[0] == 3: + return rlp.decode_to(BlobTransaction, tx[1:]) + elif tx[0] == 4: + return rlp.decode_to(SetCodeTransaction, tx[1:]) + else: + raise TransactionTypeError(tx[0]) + else: + return tx + + +def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: + """ + Verifies a transaction. + + The gas in a transaction gets used to pay for the intrinsic cost of + operations, therefore if there is insufficient gas then it would not + be possible to execute a transaction and it will be declared invalid. + + Additionally, the nonce of a transaction must not equal or exceed the + limit defined in [EIP-2681]. + In practice, defining the limit as ``2**64-1`` has no impact because + sending ``2**64-1`` transactions is improbable. It's not strictly + impossible though, ``2**64-1`` transactions is the entire capacity of the + Ethereum blockchain at 2022 gas limits for a little over 22 years. + + Also, the code size of a contract creation transaction must be within + limits of the protocol. + + This function takes a transaction as a parameter and returns the intrinsic + gas cost and the minimum calldata gas cost for the transaction after + validation. It throws an `InsufficientTransactionGasError` exception if + the transaction does not provide enough gas to cover the intrinsic cost, + and a `NonceOverflowError` exception if the nonce is greater than + `2**64 - 2`. It also raises an `InitCodeTooLargeError` if the code size of + a contract creation transaction exceeds the maximum allowed size. + + [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 + [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 + """ + from .vm.interpreter import MAX_INIT_CODE_SIZE + + intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) + if max(intrinsic_gas, calldata_floor_gas_cost) > tx.gas: + raise InsufficientTransactionGasError("Insufficient gas") + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise NonceOverflowError("Nonce too high") + if tx.to == Bytes0(b"") and len(tx.data) > MAX_INIT_CODE_SIZE: + raise InitCodeTooLargeError("Code size too large") + if tx.gas > TX_MAX_GAS_LIMIT: + raise TransactionGasLimitExceededError("Gas limit too high") + + return intrinsic_gas, calldata_floor_gas_cost + + +def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: + """ + Calculates the gas that is charged before execution is started. + + The intrinsic cost of the transaction is charged before execution has + begun. Functions/operations in the EVM cost money to execute so this + intrinsic cost is for the operations that need to be paid for as part of + the transaction. Data transfer, for example, is part of this intrinsic + cost. It costs ether to send data over the wire and that ether is + accounted for in the intrinsic cost calculated in this function. This + intrinsic cost must be calculated and paid for before execution in order + for all operations to be implemented. + + The intrinsic cost includes: + 1. Base cost (`TX_BASE`) + 2. Cost for data (zero and non-zero bytes) + 3. Cost for contract creation (if applicable) + 4. Cost for access list entries (if applicable) + 5. Cost for authorizations (if applicable) + + + This function takes a transaction as a parameter and returns the intrinsic + gas cost of the transaction and the minimum gas cost used by the + transaction based on the calldata size. + """ + from .vm.gas import GasCosts, init_code_cost + + num_zeros = Uint(tx.data.count(0)) + num_non_zeros = ulen(tx.data) - num_zeros + + tokens_in_calldata = num_zeros + num_non_zeros * Uint(4) + # EIP-7623 floor price (note: no EVM costs) + calldata_floor_gas_cost = ( + tokens_in_calldata * GasCosts.TX_DATA_TOKEN_FLOOR + GasCosts.TX_BASE + ) + + data_cost = tokens_in_calldata * GasCosts.TX_DATA_TOKEN_STANDARD + + if tx.to == Bytes0(b""): + create_cost = GasCosts.TX_CREATE + init_code_cost(ulen(tx.data)) + else: + create_cost = Uint(0) + + access_list_cost = Uint(0) + if has_access_list(tx): + for access in tx.access_list: + access_list_cost += GasCosts.TX_ACCESS_LIST_ADDRESS + access_list_cost += ( + ulen(access.slots) * GasCosts.TX_ACCESS_LIST_STORAGE_KEY + ) + + auth_cost = Uint(0) + if isinstance(tx, SetCodeTransaction): + auth_cost += Uint( + GasCosts.AUTH_PER_EMPTY_ACCOUNT * len(tx.authorizations) + ) + + return ( + Uint( + GasCosts.TX_BASE + + data_cost + + create_cost + + access_list_cost + + auth_cost + ), + calldata_floor_gas_cost, + ) + + +def recover_sender(chain_id: U64, tx: Transaction) -> Address: + """ + Extracts the sender address from a transaction. + + The v, r, and s values are the three parts that make up the signature + of a transaction. In order to recover the sender of a transaction the two + components needed are the signature (``v``, ``r``, and ``s``) and the + signing hash of the transaction. The sender's public key can be obtained + with these two values and therefore the sender address can be retrieved. + + This function takes chain_id and a transaction as parameters and returns + the address of the sender of the transaction. It raises an + `InvalidSignatureError` if the signature values (r, s, v) are invalid. + """ + r, s = tx.r, tx.s + if U256(0) >= r or r >= SECP256K1N: + raise InvalidSignatureError("bad r") + if U256(0) >= s or s > SECP256K1N // U256(2): + raise InvalidSignatureError("bad s") + + if isinstance(tx, LegacyTransaction): + v = tx.v + if v == 27 or v == 28: + public_key = secp256k1_recover( + r, s, v - U256(27), signing_hash_pre155(tx) + ) + else: + chain_id_x2 = U256(chain_id) * U256(2) + if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: + raise InvalidSignatureError("bad v") + public_key = secp256k1_recover( + r, + s, + v - U256(35) - chain_id_x2, + signing_hash_155(tx, chain_id), + ) + elif isinstance(tx, AccessListTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_2930(tx) + ) + elif isinstance(tx, FeeMarketTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_1559(tx) + ) + elif isinstance(tx, BlobTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_4844(tx) + ) + elif isinstance(tx, SetCodeTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_7702(tx) + ) + + return Address(keccak256(public_key)[12:32]) + + +def signing_hash_pre155(tx: LegacyTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a legacy (pre [EIP-155]) + signature. + + This function takes a legacy transaction as a parameter and returns the + signing hash of the transaction. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + ) + ) + ) + + +def signing_hash_155(tx: LegacyTransaction, chain_id: U64) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-155] signature. + + This function takes a legacy transaction and a chain ID as parameters + and returns the hash of the transaction used in an [EIP-155] signature. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + chain_id, + Uint(0), + Uint(0), + ) + ) + ) + + +def signing_hash_2930(tx: AccessListTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-2930] signature. + + This function takes an access list transaction as a parameter + and returns the hash of the transaction used in an [EIP-2930] signature. + + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + """ + return keccak256( + b"\x01" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: + """ + Compute the hash of a transaction used in an [EIP-1559] signature. + + This function takes a fee market transaction as a parameter + and returns the hash of the transaction used in an [EIP-1559] signature. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + return keccak256( + b"\x02" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_4844(tx: BlobTransaction) -> Hash32: + """ + Compute the hash of a transaction used in an [EIP-4844] signature. + + This function takes a transaction as a parameter and returns the + signing hash of the transaction used in an [EIP-4844] signature. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + return keccak256( + b"\x03" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.max_fee_per_blob_gas, + tx.blob_versioned_hashes, + ) + ) + ) + + +def signing_hash_7702(tx: SetCodeTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-7702] signature. + + This function takes a transaction as a parameter and returns the + signing hash of the transaction used in a [EIP-7702] signature. + + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + return keccak256( + b"\x04" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.authorizations, + ) + ) + ) + + +def get_transaction_hash(tx: Bytes | LegacyTransaction) -> Hash32: + """ + Compute the hash of a transaction. + + This function takes a transaction as a parameter and returns the + keccak256 hash of the transaction. It can handle both legacy transactions + and typed transactions (`AccessListTransaction`, `FeeMarketTransaction`, + etc.). + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) + + +def has_access_list( + tx: Transaction, +) -> TypeGuard[AccessListCapableTransaction]: + """ + Return whether the transaction has an [EIP-2930]-style access list. + + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + """ + return isinstance( + tx, + AccessListCapableTransaction, + ) diff --git a/src/ethereum/forks/monad_next/utils/__init__.py b/src/ethereum/forks/monad_next/utils/__init__.py new file mode 100644 index 00000000000..224a4d269b9 --- /dev/null +++ b/src/ethereum/forks/monad_next/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Utility functions unique to this particular fork. +""" diff --git a/src/ethereum/forks/monad_next/utils/address.py b/src/ethereum/forks/monad_next/utils/address.py new file mode 100644 index 00000000000..8c2d00b9f49 --- /dev/null +++ b/src/ethereum/forks/monad_next/utils/address.py @@ -0,0 +1,93 @@ +""" +Hardfork Utility Functions For Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Address specific functions used in this MONAD_NINE version of +specification. +""" + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.state import Address +from ethereum.utils.byte import left_pad_zero_bytes + + +def to_address_masked(data: Uint | U256) -> Address: + """ + Convert a Uint or U256 value to a valid address (20 bytes). + + Parameters + ---------- + data : + The numeric value to be converted to address. + + Returns + ------- + address : `Address` + The obtained address. + + """ + return Address(data.to_be_bytes32()[-20:]) + + +def compute_contract_address(address: Address, nonce: Uint) -> Address: + """ + Computes address of the new account that needs to be created. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + nonce : + The transaction count of the account that wants to create the new + account. + + Returns + ------- + address: `Address` + The computed address of the new account. + + """ + computed_address = keccak256(rlp.encode([address, nonce])) + canonical_address = computed_address[-20:] + padded_address = left_pad_zero_bytes(canonical_address, 20) + return Address(padded_address) + + +def compute_create2_contract_address( + address: Address, salt: Bytes32, call_data: Bytes +) -> Address: + """ + Computes address of the new account that needs to be created, which is + based on the sender address, salt and the call data as well. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + salt : + Address generation salt. + call_data : + The code of the new account which is to be created. + + Returns + ------- + address: `ethereum.forks.monad_next.fork_types.Address` + The computed address of the new account. + + """ + preimage = b"\xff" + address + salt + keccak256(call_data) + computed_address = keccak256(preimage) + canonical_address = computed_address[-20:] + padded_address = left_pad_zero_bytes(canonical_address, 20) + + return Address(padded_address) diff --git a/src/ethereum/forks/monad_next/utils/hexadecimal.py b/src/ethereum/forks/monad_next/utils/hexadecimal.py new file mode 100644 index 00000000000..d647084b730 --- /dev/null +++ b/src/ethereum/forks/monad_next/utils/hexadecimal.py @@ -0,0 +1,54 @@ +""" +Utility Functions For Hexadecimal Strings. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Hexadecimal utility functions used in this specification, specific to +Monad Next types. +""" + +from ethereum_types.bytes import Bytes + +from ethereum.state import Address, Root +from ethereum.utils.hexadecimal import remove_hex_prefix + + +def hex_to_root(hex_string: str) -> Root: + """ + Convert hex string to trie root. + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to trie root. + + Returns + ------- + root : `Root` + Trie root obtained from the given hexadecimal string. + + """ + return Root(Bytes.fromhex(remove_hex_prefix(hex_string))) + + +def hex_to_address(hex_string: str) -> Address: + """ + Convert hex string to Address (20 bytes). + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to Address. + + Returns + ------- + address : `Address` + The address obtained from the given hexadecimal string. + + """ + return Address(Bytes.fromhex(remove_hex_prefix(hex_string).rjust(40, "0"))) diff --git a/src/ethereum/forks/monad_next/utils/message.py b/src/ethereum/forks/monad_next/utils/message.py new file mode 100644 index 00000000000..ab5039f6e5c --- /dev/null +++ b/src/ethereum/forks/monad_next/utils/message.py @@ -0,0 +1,94 @@ +""" +Hardfork Utility Functions For The Message Data-structure. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Message specific functions used in this MONAD_NINE version of +specification. +""" + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint + +from ethereum.state import Address + +from ..state_tracker import get_account, get_code +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from .address import compute_contract_address + + +def prepare_message( + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, +) -> Message: + """ + Execute a transaction against the provided environment. + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. + + Returns + ------- + message: `ethereum.forks.monad_next.vm.Message` + Items containing contract creation or message call specific data. + + """ + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): + current_target = compute_contract_address( + tx_env.origin, + get_account(tx_env.state, tx_env.origin).nonce - Uint(1), + ) + msg_data = Bytes(b"") + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_code( + tx_env.state, get_account(tx_env.state, tx.to).code_hash + ) + code_address = tx.to + else: + raise AssertionError("Target must be address or empty bytes") + + accessed_addresses.add(current_target) + + return Message( + block_env=block_env, + tx_env=tx_env, + caller=tx_env.origin, + target=tx.to, + gas=tx_env.gas, + value=tx.value, + data=msg_data, + code=code, + depth=Uint(0), + current_target=current_target, + code_address=code_address, + should_transfer_value=True, + is_static=False, + accessed_addresses=accessed_addresses, + accessed_storage_keys=set(tx_env.access_list_storage_keys), + disable_precompiles=False, + parent_evm=None, + disable_create_opcodes=False, + ) diff --git a/src/ethereum/forks/monad_next/vm/__init__.py b/src/ethereum/forks/monad_next/vm/__init__.py new file mode 100644 index 00000000000..5738e4f0d5f --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/__init__.py @@ -0,0 +1,238 @@ +""" +Ethereum Virtual Machine (EVM). + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The abstract computer which runs the code stored in an +`.fork_types.Account`. +""" + +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Set, Tuple, final + +from ethereum_types.bytes import Bytes, Bytes0, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException +from ethereum.merkle_patricia_trie import Trie +from ethereum.state import Address + +from ..blocks import Log, Receipt, Withdrawal +from ..fork_types import Authorization, VersionedHash +from ..state_tracker import BlockState, TransactionState +from ..transactions import LegacyTransaction + +__all__ = ("Environment", "Evm", "Message") + + +@final +@dataclass +class BlockEnvironment: + """ + Items external to the virtual machine itself, provided by the environment. + """ + + chain_id: U64 + state: BlockState + block_gas_limit: Uint + block_hashes: List[Hash32] + coinbase: Address + number: Uint + base_fee_per_gas: Uint + time: U256 + prev_randao: Bytes32 + excess_blob_gas: U64 + parent_beacon_block_root: Hash32 + + +@final +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + receipt_keys : + Keys of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + withdrawals_trie : `ethereum.fork_types.Root` + Trie root of all the withdrawals in the block. + blob_gas_used : `ethereum.base_types.U64` + Total blob gas used in the block. + requests : `Bytes` + Hash of all the requests in the block. + """ + + block_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Bytes | LegacyTransaction]] = ( + field(default_factory=lambda: Trie(secured=False, default=None)) + ) + receipts_trie: Trie[Bytes, Optional[Bytes | Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipt_keys: Tuple[Bytes, ...] = field(default_factory=tuple) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + withdrawals_trie: Trie[Bytes, Optional[Bytes | Withdrawal]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + blob_gas_used: U64 = U64(0) + requests: List[Bytes] = field(default_factory=list) + + +@final +@dataclass +class TransactionEnvironment: + """ + Items that are used while processing a transaction. + """ + + origin: Address + gas_price: Uint + gas: Uint + tx_gas_limit: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + state: TransactionState + blob_versioned_hashes: Tuple[VersionedHash, ...] + authorizations: Tuple[Authorization, ...] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] + # Monad: snapshot of `state` captured at top-level EVM begin (i.e. + # after pre-execution gas/nonce deduction). Recreates the + # pre-refactor ``state._snapshots[0]`` view used by the reserve + # balance check (end-of-tx) and the dippedIntoReserve precompile. + tx_snapshot: Optional[TransactionState] = None + + +@final +@dataclass +class Message: + """ + Items that are used by contract creation or message call. + """ + + block_env: BlockEnvironment + tx_env: TransactionEnvironment + caller: Address + target: Bytes0 | Address + current_target: Address + gas: Uint + value: U256 + data: Bytes + code_address: Optional[Address] + code: Bytes + depth: Uint + should_transfer_value: bool + is_static: bool + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + disable_precompiles: bool + parent_evm: Optional["Evm"] + disable_create_opcodes: bool + + +@dataclass +class EvmMemory: + """ + Memory of the EVM. + """ + + data: bytearray + high_watermark_bytes: int + + def __len__(self) -> int: + """Return the length of the memory data.""" + return len(self.data) + + def hex(self) -> str: + """Return the hex string of the memory data.""" + return self.data.hex() + + +@final +@dataclass +class Evm: + """The internal state of the virtual machine.""" + + pc: Uint + stack: List[U256] + memory: EvmMemory + code: Bytes + gas_left: Uint + valid_jump_destinations: Set[Uint] + logs: Tuple[Log, ...] + refund_counter: int + running: bool + message: Message + output: Bytes + accounts_to_delete: Set[Address] + return_data: Bytes + error: Optional[EthereumException] + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + read_accessed_pages: Set[Tuple[Address, U256]] + write_accessed_pages: Set[Tuple[Address, U256]] + current_state_growth: Dict[Tuple[Address, U256], int] + net_state_growth: Dict[Tuple[Address, U256], int] + + +def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of a successful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + + """ + evm.gas_left += child_evm.gas_left + evm.logs += child_evm.logs + evm.refund_counter += child_evm.refund_counter + evm.accounts_to_delete.update(child_evm.accounts_to_delete) + evm.accessed_addresses.update(child_evm.accessed_addresses) + evm.accessed_storage_keys.update(child_evm.accessed_storage_keys) + # MIP-8: page warming and per-page state growth propagate upward only + # on success, so a reverted child's page accesses are discarded. + evm.read_accessed_pages.update(child_evm.read_accessed_pages) + evm.write_accessed_pages.update(child_evm.write_accessed_pages) + evm.current_state_growth = dict(child_evm.current_state_growth) + evm.net_state_growth = dict(child_evm.net_state_growth) + + # NOTE: absence of `evm.memory`, in particular of its high watermark + # is intended for memory to deallocate on call frame exit. + + +def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of an unsuccessful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + + """ + evm.gas_left += child_evm.gas_left + + # NOTE: absence of `evm.memory`, in particular of its high watermark + # is intended for memory to deallocate on call frame exit. diff --git a/src/ethereum/forks/monad_next/vm/eoa_delegation.py b/src/ethereum/forks/monad_next/vm/eoa_delegation.py new file mode 100644 index 00000000000..3f49d12b3cd --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/eoa_delegation.py @@ -0,0 +1,227 @@ +""" +Set EOA account code. +""" + +from typing import Optional, Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import keccak256 +from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.state import Address + +from ..fork_types import Authorization +from ..state_tracker import ( + account_exists, + add_sender_authority, + get_account, + get_code, + increment_nonce, + set_code, +) +from ..utils.hexadecimal import hex_to_address +from ..vm.gas import GasCosts +from . import Evm, Message + +SET_CODE_TX_MAGIC = b"\x05" +EOA_DELEGATION_MARKER = b"\xef\x01\x00" +EOA_DELEGATION_MARKER_LENGTH = len(EOA_DELEGATION_MARKER) +EOA_DELEGATED_CODE_LENGTH = 23 +REFUND_AUTH_PER_EXISTING_ACCOUNT = 12500 +NULL_ADDRESS = hex_to_address("0x0000000000000000000000000000000000000000") + + +def is_valid_delegation(code: bytes) -> bool: + """ + Whether the code is a valid delegation designation. + + Parameters + ---------- + code: `bytes` + The code to check. + + Returns + ------- + valid : `bool` + True if the code is a valid delegation designation, + False otherwise. + + """ + if ( + len(code) == EOA_DELEGATED_CODE_LENGTH + and code[:EOA_DELEGATION_MARKER_LENGTH] == EOA_DELEGATION_MARKER + ): + return True + return False + + +def get_delegated_code_address(code: bytes) -> Optional[Address]: + """ + Get the address to which the code delegates. + + Parameters + ---------- + code: `bytes` + The code to get the address from. + + Returns + ------- + address : `Optional[Address]` + The address of the delegated code. + + """ + if is_valid_delegation(code): + return Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + return None + + +def recover_authority(authorization: Authorization) -> Address: + """ + Recover the authority address from the authorization. + + Parameters + ---------- + authorization + The authorization to recover the authority from. + + Raises + ------ + InvalidSignatureError + If the signature is invalid. + + Returns + ------- + authority : `Address` + The recovered authority address. + + """ + y_parity, r, s = authorization.y_parity, authorization.r, authorization.s + if y_parity not in (0, 1): + raise InvalidSignatureError("Invalid y_parity in authorization") + if U256(0) >= r or r >= SECP256K1N: + raise InvalidSignatureError("Invalid r value in authorization") + if U256(0) >= s or s > SECP256K1N // U256(2): + raise InvalidSignatureError("Invalid s value in authorization") + + signing_hash = keccak256( + SET_CODE_TX_MAGIC + + rlp.encode( + ( + authorization.chain_id, + authorization.address, + authorization.nonce, + ) + ) + ) + + public_key = secp256k1_recover(r, s, U256(y_parity), signing_hash) + return Address(keccak256(public_key)[12:32]) + + +def access_delegation( + evm: Evm, address: Address +) -> Tuple[bool, bool, Address, Bytes, Uint]: + """ + Get the delegation address, code, and the cost of access from the address. + + Parameters + ---------- + evm : `Evm` + The execution frame. + address : `Address` + The address to get the delegation from. + + Returns + ------- + delegation : `Tuple[bool, bool, Address, Bytes, Uint]` + The disable precompiles, disable creates, delegation address, code, and + access gas cost. + + """ + tx_state = evm.message.tx_env.state + + code = get_code(tx_state, get_account(tx_state, address).code_hash) + if not is_valid_delegation(code): + return False, False, address, code, Uint(0) + + address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + if address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + code = get_code(tx_state, get_account(tx_state, address).code_hash) + + return True, True, address, code, access_gas_cost + + +def set_delegation(message: Message) -> U256: + """ + Set the delegation code for the authorities in the message. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + refund_counter: `U256` + Refund from authority which already exists in state. + + """ + tx_state = message.tx_env.state + refund_counter = U256(0) + for auth in message.tx_env.authorizations: + if auth.chain_id not in (message.block_env.chain_id, U256(0)): + continue + + if auth.nonce >= U64.MAX_VALUE: + continue + + try: + authority = recover_authority(auth) + except InvalidSignatureError: + continue + + message.accessed_addresses.add(authority) + + authority_account = get_account(tx_state, authority) + authority_code = get_code(tx_state, authority_account.code_hash) + + if authority_code and not is_valid_delegation(authority_code): + continue + + authority_nonce = authority_account.nonce + if authority_nonce != auth.nonce: + continue + + if account_exists(tx_state, authority): + refund_counter += U256( + GasCosts.AUTH_PER_EMPTY_ACCOUNT + - REFUND_AUTH_PER_EXISTING_ACCOUNT + ) + + if auth.address == NULL_ADDRESS: + code_to_set = b"" + else: + code_to_set = EOA_DELEGATION_MARKER + auth.address + set_code(tx_state, authority, code_to_set) + + increment_nonce(tx_state, authority) + + add_sender_authority( + message.block_env.state, message.block_env.number, authority + ) + + if message.code_address is None: + raise InvalidBlock("Invalid type 4 transaction: no target") + + message.code = get_code( + tx_state, get_account(tx_state, message.code_address).code_hash + ) + + return refund_counter diff --git a/src/ethereum/forks/monad_next/vm/exceptions.py b/src/ethereum/forks/monad_next/vm/exceptions.py new file mode 100644 index 00000000000..7c1473772cd --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/exceptions.py @@ -0,0 +1,169 @@ +""" +Ethereum Virtual Machine (EVM) Exceptions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Exceptions which cause the EVM to halt exceptionally. +""" + +from ethereum.exceptions import EthereumException + + +class ExceptionalHalt(EthereumException): + """ + Indicates that the EVM has experienced an exceptional halt. This causes + execution to immediately end with all gas being consumed. + """ + + +class Revert(EthereumException): + """ + Raised by the `REVERT` opcode. + + Unlike other EVM exceptions this does not result in the consumption of all + gas. + """ + + pass + + +class RevertOnReserveBalance(EthereumException): + """ + Raised when reserve balance is violated by a transaction. + + .. FIXME description. + """ + + pass + + +class StackUnderflowError(ExceptionalHalt): + """ + Occurs when a pop is executed on an empty stack. + """ + + pass + + +class StackOverflowError(ExceptionalHalt): + """ + Occurs when a push is executed on a stack at max capacity. + """ + + pass + + +class OutOfGasError(ExceptionalHalt): + """ + Occurs when an operation costs more than the amount of gas left in the + frame. + """ + + pass + + +class InvalidOpcode(ExceptionalHalt): + """ + Raised when an invalid opcode is encountered. + """ + + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code + + +class InvalidJumpDestError(ExceptionalHalt): + """ + Occurs when the destination of a jump operation doesn't meet any of the + following criteria. + + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + """ + + +class StackDepthLimitError(ExceptionalHalt): + """ + Raised when the message depth is greater than `1024`. + """ + + pass + + +class WriteInStaticContext(ExceptionalHalt): + """ + Raised when an attempt is made to modify the state while operating inside + of a STATICCALL context. + """ + + pass + + +class CreateIn7702Context(ExceptionalHalt): + """ + Raised when an attempt is made to CREATE or CREATE2 inside an EIP-7702 + delegated account context. + """ + + pass + + +class OutOfBoundsRead(ExceptionalHalt): + """ + Raised when an attempt was made to read data beyond the + boundaries of the buffer. + """ + + pass + + +class InvalidParameter(ExceptionalHalt): + """ + Raised when invalid parameters are passed. + """ + + pass + + +class RevertInMonadPrecompile(ExceptionalHalt): + """ + Raised by a Monad precompile to revert with an error message. + + Consumes all gas like ExceptionalHalt but preserves evm.output + so the caller sees the revert reason. + """ + + pass + + +class InvalidContractPrefix(ExceptionalHalt): + """ + Raised when the new contract code starts with 0xEF. + """ + + pass + + +class AddressCollision(ExceptionalHalt): + """ + Raised when the new contract address has a collision. + """ + + pass + + +class KZGProofError(ExceptionalHalt): + """ + Raised when the point evaluation precompile can't verify a proof. + """ + + pass diff --git a/src/ethereum/forks/monad_next/vm/gas.py b/src/ethereum/forks/monad_next/vm/gas.py new file mode 100644 index 00000000000..f7f25d89dfc --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/gas.py @@ -0,0 +1,558 @@ +""" +Ethereum Virtual Machine (EVM) Gas. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM gas constants and calculators. +""" + +from dataclasses import dataclass +from typing import Final, List, Tuple, final + +from ethereum_types.numeric import U64, U256, Uint, ulen + +from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.numeric import ceil32, taylor_exponential + +from ..blocks import Header +from ..transactions import BlobTransaction, Transaction +from . import Evm, EvmMemory +from .exceptions import OutOfGasError + +# Monad MIP-3: linear memory pricing parameters. +MAX_TX_MEMORY_USAGE = 8 * 1024 * 1024 +MEMORY_WORDS_PER_GAS = Uint(2) + +# Monad MIP-8: number of storage slots per page (page = slot >> 7). +SLOTS_PER_PAGE = 128 + + +def page_index(slot: U256) -> U256: + """ + Return the page index for a given storage slot. + + Parameters + ---------- + slot : + The storage slot number. + + Returns + ------- + page_index : U256 + The page index (``slot >> 7``). + + """ + return slot >> U256(7) + + +# These values may be patched at runtime by a future gas repricing utility +class GasCosts: + """ + Constant gas values for the EVM. + """ + + # Tiers + BASE: Final[Uint] = Uint(2) + VERY_LOW: Final[Uint] = Uint(3) + LOW: Final[Uint] = Uint(5) + MID: Final[Uint] = Uint(8) + HIGH: Final[Uint] = Uint(10) + + # Access + WARM_ACCESS: Final[Uint] = Uint(100) + COLD_ACCOUNT_ACCESS: Final[Uint] = Uint(2600 + 7500) + COLD_STORAGE_ACCESS: Final[Uint] = Uint(2100 + 6000) + + # Storage + STORAGE_SET: Final[Uint] = Uint(20000) + COLD_STORAGE_WRITE: Final[Uint] = Uint(5000 + 6000) + + # MIP-8 page-based storage + PAGE_BASE_COST: Final[Uint] = Uint(100) + PAGE_LOAD_COST: Final[Uint] = Uint(8000) + PAGE_WRITE_COST: Final[Uint] = Uint(2800) + PAGE_STATE_GROWTH_COST: Final[Uint] = Uint(17000) + + # Call + CALL_VALUE: Final[Uint] = Uint(9000) + CALL_STIPEND: Final[Uint] = Uint(2300) + NEW_ACCOUNT: Final[Uint] = Uint(25000) + + # Contract Creation + CODE_DEPOSIT_PER_BYTE: Final[Uint] = Uint(200) + CODE_INIT_PER_WORD: Final[Uint] = Uint(2) + + # Authorization + AUTH_PER_EMPTY_ACCOUNT: Final[int] = 25000 + + # Utility + ZERO: Final[Uint] = Uint(0) + MEMORY_PER_WORD: Final[Uint] = Uint(3) + FAST_STEP: Final[Uint] = Uint(5) + + # Refunds + REFUND_STORAGE_CLEAR: Final[int] = 4800 + + # Precompiles + PRECOMPILE_ECRECOVER: Final[Uint] = Uint(3000 * 2) + PRECOMPILE_P256VERIFY: Final[Uint] = Uint(6900) + PRECOMPILE_SHA256_BASE: Final[Uint] = Uint(60) + PRECOMPILE_SHA256_PER_WORD: Final[Uint] = Uint(12) + PRECOMPILE_RIPEMD160_BASE: Final[Uint] = Uint(600) + PRECOMPILE_RIPEMD160_PER_WORD: Final[Uint] = Uint(120) + PRECOMPILE_IDENTITY_BASE: Final[Uint] = Uint(15) + PRECOMPILE_IDENTITY_PER_WORD: Final[Uint] = Uint(3) + PRECOMPILE_BLAKE2F_PER_ROUND: Final[Uint] = Uint(1 * 2) + PRECOMPILE_POINT_EVALUATION: Final[Uint] = Uint(50000 * 4) + PRECOMPILE_BLS_G1ADD: Final[Uint] = Uint(375) + PRECOMPILE_BLS_G1MUL: Final[Uint] = Uint(12000) + PRECOMPILE_BLS_G1MAP: Final[Uint] = Uint(5500) + PRECOMPILE_BLS_G2ADD: Final[Uint] = Uint(600) + PRECOMPILE_BLS_G2MUL: Final[Uint] = Uint(22500) + PRECOMPILE_BLS_G2MAP: Final[Uint] = Uint(23800) + PRECOMPILE_ECADD: Final[Uint] = Uint(150 * 2) + PRECOMPILE_ECMUL: Final[Uint] = Uint(6000 * 5) + PRECOMPILE_ECPAIRING_BASE: Final[Uint] = Uint(45000 * 5) + PRECOMPILE_ECPAIRING_PER_POINT: Final[Uint] = Uint(34000 * 5) + + # Blobs + PER_BLOB: Final[U64] = U64(2**17) + BLOB_SCHEDULE_TARGET: Final[U64] = U64(6) + BLOB_TARGET_GAS_PER_BLOCK: Final[U64] = PER_BLOB * BLOB_SCHEDULE_TARGET + BLOB_BASE_COST: Final[Uint] = Uint(2**13) + BLOB_SCHEDULE_MAX: Final[U64] = U64(9) + BLOB_MIN_GASPRICE: Final[Uint] = Uint(1) + BLOB_BASE_FEE_UPDATE_FRACTION: Final[Uint] = Uint(5007716) + + # Transactions + TX_BASE: Final[Uint] = Uint(21000) + TX_CREATE: Final[Uint] = Uint(32000) + TX_DATA_TOKEN_STANDARD: Final[Uint] = Uint(4) + TX_DATA_TOKEN_FLOOR: Final[Uint] = Uint(10) + TX_ACCESS_LIST_ADDRESS: Final[Uint] = Uint(2400) + TX_ACCESS_LIST_STORAGE_KEY: Final[Uint] = Uint(1900) + + # Block + LIMIT_ADJUSTMENT_FACTOR: Final[Uint] = Uint(1024) + LIMIT_MINIMUM: Final[Uint] = Uint(5000) + + # Static Opcodes + OPCODE_ADD: Final[Uint] = VERY_LOW + OPCODE_SUB: Final[Uint] = VERY_LOW + OPCODE_MUL: Final[Uint] = LOW + OPCODE_DIV: Final[Uint] = LOW + OPCODE_SDIV: Final[Uint] = LOW + OPCODE_MOD: Final[Uint] = LOW + OPCODE_SMOD: Final[Uint] = LOW + OPCODE_ADDMOD: Final[Uint] = MID + OPCODE_MULMOD: Final[Uint] = MID + OPCODE_SIGNEXTEND: Final[Uint] = LOW + OPCODE_LT: Final[Uint] = VERY_LOW + OPCODE_GT: Final[Uint] = VERY_LOW + OPCODE_SLT: Final[Uint] = VERY_LOW + OPCODE_SGT: Final[Uint] = VERY_LOW + OPCODE_EQ: Final[Uint] = VERY_LOW + OPCODE_ISZERO: Final[Uint] = VERY_LOW + OPCODE_AND: Final[Uint] = VERY_LOW + OPCODE_OR: Final[Uint] = VERY_LOW + OPCODE_XOR: Final[Uint] = VERY_LOW + OPCODE_NOT: Final[Uint] = VERY_LOW + OPCODE_BYTE: Final[Uint] = VERY_LOW + OPCODE_SHL: Final[Uint] = VERY_LOW + OPCODE_SHR: Final[Uint] = VERY_LOW + OPCODE_SAR: Final[Uint] = VERY_LOW + OPCODE_CLZ: Final[Uint] = LOW + OPCODE_JUMP: Final[Uint] = MID + OPCODE_JUMPI: Final[Uint] = HIGH + OPCODE_JUMPDEST: Final[Uint] = Uint(1) + OPCODE_CALLDATALOAD: Final[Uint] = VERY_LOW + OPCODE_BLOCKHASH: Final[Uint] = Uint(20) + OPCODE_COINBASE: Final[Uint] = BASE + OPCODE_POP: Final[Uint] = BASE + OPCODE_MSIZE: Final[Uint] = BASE + OPCODE_PC: Final[Uint] = BASE + OPCODE_GAS: Final[Uint] = BASE + OPCODE_ADDRESS: Final[Uint] = BASE + OPCODE_ORIGIN: Final[Uint] = BASE + OPCODE_CALLER: Final[Uint] = BASE + OPCODE_CALLVALUE: Final[Uint] = BASE + OPCODE_CALLDATASIZE: Final[Uint] = BASE + OPCODE_CODESIZE: Final[Uint] = BASE + OPCODE_GASPRICE: Final[Uint] = BASE + OPCODE_TIMESTAMP: Final[Uint] = BASE + OPCODE_NUMBER: Final[Uint] = BASE + OPCODE_GASLIMIT: Final[Uint] = BASE + OPCODE_PREVRANDAO: Final[Uint] = BASE + OPCODE_RETURNDATASIZE: Final[Uint] = BASE + OPCODE_CHAINID: Final[Uint] = BASE + OPCODE_BASEFEE: Final[Uint] = BASE + OPCODE_BLOBBASEFEE: Final[Uint] = BASE + OPCODE_BLOBHASH: Final[Uint] = Uint(3) + OPCODE_PUSH: Final[Uint] = VERY_LOW + OPCODE_PUSH0: Final[Uint] = BASE + OPCODE_DUP: Final[Uint] = VERY_LOW + OPCODE_SWAP: Final[Uint] = VERY_LOW + + # Dynamic Opcodes + OPCODE_RETURNDATACOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_RETURNDATACOPY_PER_WORD: Final[Uint] = Uint(3) + OPCODE_CALLDATACOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_CODECOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_MCOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_MLOAD_BASE: Final[Uint] = VERY_LOW + OPCODE_MSTORE_BASE: Final[Uint] = VERY_LOW + OPCODE_MSTORE8_BASE: Final[Uint] = VERY_LOW + OPCODE_COPY_PER_WORD: Final[Uint] = Uint(3) + OPCODE_CREATE_BASE: Final[Uint] = Uint(32000) + OPCODE_EXP_BASE: Final[Uint] = Uint(10) + OPCODE_EXP_PER_BYTE: Final[Uint] = Uint(50) + OPCODE_KECCAK256_BASE: Final[Uint] = Uint(30) + OPCODE_KECCAK256_PER_WORD: Final[Uint] = Uint(6) + OPCODE_LOG_BASE: Final[Uint] = Uint(375) + OPCODE_LOG_DATA_PER_BYTE: Final[Uint] = Uint(8) + OPCODE_LOG_TOPIC: Final[Uint] = Uint(375) + OPCODE_SELFDESTRUCT_BASE: Final[Uint] = Uint(5000) + OPCODE_SELFDESTRUCT_NEW_ACCOUNT: Final[Uint] = Uint(25000) + + +@final +@dataclass +class ExtendMemory: + """ + Define the parameters for memory extension in opcodes. + + `cost`: `ethereum.base_types.Uint` + The gas required to perform the extension + `expand_by`: `ethereum.base_types.Uint` + The size by which the memory will be extended + """ + + cost: Uint + expand_by: Uint + + +@final +@dataclass +class MessageCallGas: + """ + Define the gas cost and gas given to the sub-call for executing the call + opcodes. + + `cost`: `ethereum.base_types.Uint` + The gas required to execute the call opcode, excludes + memory expansion costs. + `sub_call`: `ethereum.base_types.Uint` + The portion of gas available to sub-calls that is refundable + if not consumed. + """ + + cost: Uint + sub_call: Uint + + +def charge_gas(evm: Evm, amount: Uint) -> None: + """ + Subtracts `amount` from `evm.gas_left`. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of gas the current operation requires. + + """ + evm_trace(evm, GasAndRefund(int(amount))) + + if evm.gas_left < amount: + raise OutOfGasError + else: + evm.gas_left -= amount + + +def calculate_memory_gas_cost(size_in_bytes: Uint) -> Uint: + """ + Calculates the gas cost for allocating memory + to the smallest multiple of 32 bytes, + such that the allocated size is at least as big as the given size. + + Parameters + ---------- + size_in_bytes : + The size of the data in bytes. + + Returns + ------- + total_gas_cost : `ethereum.base_types.Uint` + The gas cost for storing data in memory. + + """ + size_in_words = ceil32(size_in_bytes) // Uint(32) + # Monad MIP-3 linear memory pricing. + total_gas_cost = size_in_words // MEMORY_WORDS_PER_GAS + return total_gas_cost + + +def calculate_gas_extend_memory( + memory: EvmMemory, extensions: List[Tuple[U256, U256]] +) -> ExtendMemory: + """ + Calculates the gas amount to extend memory. + + Parameters + ---------- + memory : + Memory object of the EVM. + extensions: + List of extensions to be made to the memory. + Consists of a tuple of start position and size. + + Returns + ------- + extend_memory: `ExtendMemory` + + """ + size_to_extend = Uint(0) + to_be_paid = Uint(0) + current_size = ulen(memory.data) + for start_position, size in extensions: + if size == 0: + continue + before_size = ceil32(current_size) + after_size = ceil32(Uint(start_position) + Uint(size)) + if after_size <= before_size: + continue + + size_to_extend += after_size - before_size + already_paid = calculate_memory_gas_cost(before_size) + total_cost = calculate_memory_gas_cost(after_size) + to_be_paid += total_cost - already_paid + + current_size = after_size + + return ExtendMemory(to_be_paid, size_to_extend) + + +def update_memory_high_watermark( + evm: Evm, extend_memory: ExtendMemory +) -> None: + """ + Update the memory high watermark and check it doesn't exceed + MAX_TX_MEMORY_USAGE. The high watermark is intended to not be + propagated from child EVM call frame to its parent, as the memory + is respectively deallocated on exit. + + Parameters + ---------- + evm : + The EVM object. + extend_memory : + The memory extension info from calculate_gas_extend_memory. + + Raises + ------ + OutOfGasError + If the new memory size would exceed MAX_TX_MEMORY_USAGE. + + """ + evm.memory.high_watermark_bytes += int(extend_memory.expand_by) + if evm.memory.high_watermark_bytes > MAX_TX_MEMORY_USAGE: + raise OutOfGasError + + +def calculate_message_call_gas( + value: U256, + gas: Uint, + gas_left: Uint, + memory_cost: Uint, + extra_gas: Uint, + call_stipend: Uint = GasCosts.CALL_STIPEND, +) -> MessageCallGas: + """ + Calculates the MessageCallGas (cost and gas made available to the sub-call) + for executing call Opcodes. + + Parameters + ---------- + value: + The amount of `ETH` that needs to be transferred. + gas : + The amount of gas provided to the message-call. + gas_left : + The amount of gas left in the current frame. + memory_cost : + The amount needed to extend the memory in the current frame. + extra_gas : + The amount of gas needed for transferring value + creating a new + account inside a message call. + call_stipend : + The amount of stipend provided to a message call to execute code while + transferring value (ETH). + + Returns + ------- + message_call_gas: `MessageCallGas` + + """ + call_stipend = Uint(0) if value == 0 else call_stipend + if gas_left < extra_gas + memory_cost: + return MessageCallGas(gas, gas + call_stipend) + + gas = min(gas, max_message_call_gas(gas_left - memory_cost - extra_gas)) + + return MessageCallGas(gas, gas + call_stipend) + + +def max_message_call_gas(gas: Uint) -> Uint: + """ + Calculates the maximum gas that is allowed for making a message call. + + Parameters + ---------- + gas : + The amount of gas provided to the message-call. + + Returns + ------- + max_allowed_message_call_gas: `ethereum.base_types.Uint` + The maximum gas allowed for making the message-call. + + """ + return gas - (gas // Uint(64)) + + +def init_code_cost(init_code_length: Uint) -> Uint: + """ + Calculates the gas to be charged for the init code in CREATE* + opcodes as well as create transactions. + + Parameters + ---------- + init_code_length : + The length of the init code provided to the opcode + or a create transaction + + Returns + ------- + init_code_gas: `ethereum.base_types.Uint` + The gas to be charged for the init code. + + """ + return GasCosts.CODE_INIT_PER_WORD * ceil32(init_code_length) // Uint(32) + + +def calculate_excess_blob_gas(parent_header: Header) -> U64: + """ + Calculates the excess blob gas for the current block based + on the gas used in the parent block. + + Parameters + ---------- + parent_header : + The parent block of the current block. + + Returns + ------- + excess_blob_gas: `ethereum.base_types.U64` + The excess blob gas for the current block. + + """ + # At the fork block, these are defined as zero. + excess_blob_gas = U64(0) + blob_gas_used = U64(0) + base_fee_per_gas = Uint(0) + + if isinstance(parent_header, Header): + # After the fork block, read them from the parent header. + excess_blob_gas = parent_header.excess_blob_gas + blob_gas_used = parent_header.blob_gas_used + base_fee_per_gas = parent_header.base_fee_per_gas + + parent_blob_gas = excess_blob_gas + blob_gas_used + if parent_blob_gas < GasCosts.BLOB_TARGET_GAS_PER_BLOCK: + return U64(0) + + target_blob_gas_price = Uint(GasCosts.PER_BLOB) + target_blob_gas_price *= calculate_blob_gas_price(excess_blob_gas) + + base_blob_tx_price = GasCosts.BLOB_BASE_COST * base_fee_per_gas + if base_blob_tx_price > target_blob_gas_price: + blob_schedule_delta = ( + GasCosts.BLOB_SCHEDULE_MAX - GasCosts.BLOB_SCHEDULE_TARGET + ) + return ( + excess_blob_gas + + blob_gas_used * blob_schedule_delta // GasCosts.BLOB_SCHEDULE_MAX + ) + + return parent_blob_gas - GasCosts.BLOB_TARGET_GAS_PER_BLOCK + + +def calculate_total_blob_gas(tx: Transaction) -> U64: + """ + Calculate the total blob gas for a transaction. + + Parameters + ---------- + tx : + The transaction for which the blob gas is to be calculated. + + Returns + ------- + total_blob_gas: `ethereum.base_types.Uint` + The total blob gas for the transaction. + + """ + if isinstance(tx, BlobTransaction): + return GasCosts.PER_BLOB * U64(len(tx.blob_versioned_hashes)) + else: + return U64(0) + + +def calculate_blob_gas_price(excess_blob_gas: U64) -> Uint: + """ + Calculate the blob gasprice for a block. + + Parameters + ---------- + excess_blob_gas : + The excess blob gas for the block. + + Returns + ------- + blob_gasprice: `Uint` + The blob gasprice. + + """ + return taylor_exponential( + GasCosts.BLOB_MIN_GASPRICE, + Uint(excess_blob_gas), + GasCosts.BLOB_BASE_FEE_UPDATE_FRACTION, + ) + + +def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: + """ + Calculate the blob data fee for a transaction. + + Parameters + ---------- + excess_blob_gas : + The excess_blob_gas for the execution. + tx : + The transaction for which the blob data fee is to be calculated. + + Returns + ------- + data_fee: `Uint` + The blob data fee. + + """ + return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( + excess_blob_gas + ) diff --git a/src/ethereum/forks/monad_next/vm/instructions/__init__.py b/src/ethereum/forks/monad_next/vm/instructions/__init__.py new file mode 100644 index 00000000000..0da72c8ea5c --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/__init__.py @@ -0,0 +1,367 @@ +""" +EVM Instruction Encoding (Opcodes). + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Machine readable representations of EVM instructions, and a mapping to their +implementations. +""" + +import enum +from typing import Callable, Dict + +from . import arithmetic as arithmetic_instructions +from . import bitwise as bitwise_instructions +from . import block as block_instructions +from . import comparison as comparison_instructions +from . import control_flow as control_flow_instructions +from . import environment as environment_instructions +from . import keccak as keccak_instructions +from . import log as log_instructions +from . import memory as memory_instructions +from . import stack as stack_instructions +from . import storage as storage_instructions +from . import system as system_instructions + + +class Ops(enum.Enum): + """ + Enum for EVM Opcodes. + """ + + # Arithmetic Ops + ADD = 0x01 + MUL = 0x02 + SUB = 0x03 + DIV = 0x04 + SDIV = 0x05 + MOD = 0x06 + SMOD = 0x07 + ADDMOD = 0x08 + MULMOD = 0x09 + EXP = 0x0A + SIGNEXTEND = 0x0B + + # Comparison Ops + LT = 0x10 + GT = 0x11 + SLT = 0x12 + SGT = 0x13 + EQ = 0x14 + ISZERO = 0x15 + + # Bitwise Ops + AND = 0x16 + OR = 0x17 + XOR = 0x18 + NOT = 0x19 + BYTE = 0x1A + SHL = 0x1B + SHR = 0x1C + SAR = 0x1D + CLZ = 0x1E + + # Keccak Op + KECCAK = 0x20 + + # Environmental Ops + ADDRESS = 0x30 + BALANCE = 0x31 + ORIGIN = 0x32 + CALLER = 0x33 + CALLVALUE = 0x34 + CALLDATALOAD = 0x35 + CALLDATASIZE = 0x36 + CALLDATACOPY = 0x37 + CODESIZE = 0x38 + CODECOPY = 0x39 + GASPRICE = 0x3A + EXTCODESIZE = 0x3B + EXTCODECOPY = 0x3C + RETURNDATASIZE = 0x3D + RETURNDATACOPY = 0x3E + EXTCODEHASH = 0x3F + + # Block Ops + BLOCKHASH = 0x40 + COINBASE = 0x41 + TIMESTAMP = 0x42 + NUMBER = 0x43 + PREVRANDAO = 0x44 + GASLIMIT = 0x45 + CHAINID = 0x46 + SELFBALANCE = 0x47 + BASEFEE = 0x48 + BLOBHASH = 0x49 + BLOBBASEFEE = 0x4A + + # Control Flow Ops + STOP = 0x00 + JUMP = 0x56 + JUMPI = 0x57 + PC = 0x58 + GAS = 0x5A + JUMPDEST = 0x5B + + # Storage Ops + SLOAD = 0x54 + SSTORE = 0x55 + TLOAD = 0x5C + TSTORE = 0x5D + + # Pop Operation + POP = 0x50 + + # Push Operations + PUSH0 = 0x5F + PUSH1 = 0x60 + PUSH2 = 0x61 + PUSH3 = 0x62 + PUSH4 = 0x63 + PUSH5 = 0x64 + PUSH6 = 0x65 + PUSH7 = 0x66 + PUSH8 = 0x67 + PUSH9 = 0x68 + PUSH10 = 0x69 + PUSH11 = 0x6A + PUSH12 = 0x6B + PUSH13 = 0x6C + PUSH14 = 0x6D + PUSH15 = 0x6E + PUSH16 = 0x6F + PUSH17 = 0x70 + PUSH18 = 0x71 + PUSH19 = 0x72 + PUSH20 = 0x73 + PUSH21 = 0x74 + PUSH22 = 0x75 + PUSH23 = 0x76 + PUSH24 = 0x77 + PUSH25 = 0x78 + PUSH26 = 0x79 + PUSH27 = 0x7A + PUSH28 = 0x7B + PUSH29 = 0x7C + PUSH30 = 0x7D + PUSH31 = 0x7E + PUSH32 = 0x7F + + # Dup operations + DUP1 = 0x80 + DUP2 = 0x81 + DUP3 = 0x82 + DUP4 = 0x83 + DUP5 = 0x84 + DUP6 = 0x85 + DUP7 = 0x86 + DUP8 = 0x87 + DUP9 = 0x88 + DUP10 = 0x89 + DUP11 = 0x8A + DUP12 = 0x8B + DUP13 = 0x8C + DUP14 = 0x8D + DUP15 = 0x8E + DUP16 = 0x8F + + # Swap operations + SWAP1 = 0x90 + SWAP2 = 0x91 + SWAP3 = 0x92 + SWAP4 = 0x93 + SWAP5 = 0x94 + SWAP6 = 0x95 + SWAP7 = 0x96 + SWAP8 = 0x97 + SWAP9 = 0x98 + SWAP10 = 0x99 + SWAP11 = 0x9A + SWAP12 = 0x9B + SWAP13 = 0x9C + SWAP14 = 0x9D + SWAP15 = 0x9E + SWAP16 = 0x9F + + # Memory Operations + MLOAD = 0x51 + MSTORE = 0x52 + MSTORE8 = 0x53 + MSIZE = 0x59 + MCOPY = 0x5E + + # Log Operations + LOG0 = 0xA0 + LOG1 = 0xA1 + LOG2 = 0xA2 + LOG3 = 0xA3 + LOG4 = 0xA4 + + # System Operations + CREATE = 0xF0 + CALL = 0xF1 + CALLCODE = 0xF2 + RETURN = 0xF3 + DELEGATECALL = 0xF4 + CREATE2 = 0xF5 + STATICCALL = 0xFA + REVERT = 0xFD + SELFDESTRUCT = 0xFF + + +op_implementation: Dict[Ops, Callable] = { + Ops.STOP: control_flow_instructions.stop, + Ops.ADD: arithmetic_instructions.add, + Ops.MUL: arithmetic_instructions.mul, + Ops.SUB: arithmetic_instructions.sub, + Ops.DIV: arithmetic_instructions.div, + Ops.SDIV: arithmetic_instructions.sdiv, + Ops.MOD: arithmetic_instructions.mod, + Ops.SMOD: arithmetic_instructions.smod, + Ops.ADDMOD: arithmetic_instructions.addmod, + Ops.MULMOD: arithmetic_instructions.mulmod, + Ops.EXP: arithmetic_instructions.exp, + Ops.SIGNEXTEND: arithmetic_instructions.signextend, + Ops.LT: comparison_instructions.less_than, + Ops.GT: comparison_instructions.greater_than, + Ops.SLT: comparison_instructions.signed_less_than, + Ops.SGT: comparison_instructions.signed_greater_than, + Ops.EQ: comparison_instructions.equal, + Ops.ISZERO: comparison_instructions.is_zero, + Ops.AND: bitwise_instructions.bitwise_and, + Ops.OR: bitwise_instructions.bitwise_or, + Ops.XOR: bitwise_instructions.bitwise_xor, + Ops.NOT: bitwise_instructions.bitwise_not, + Ops.BYTE: bitwise_instructions.get_byte, + Ops.SHL: bitwise_instructions.bitwise_shl, + Ops.SHR: bitwise_instructions.bitwise_shr, + Ops.SAR: bitwise_instructions.bitwise_sar, + Ops.CLZ: bitwise_instructions.count_leading_zeros, + Ops.KECCAK: keccak_instructions.keccak, + Ops.SLOAD: storage_instructions.sload, + Ops.BLOCKHASH: block_instructions.block_hash, + Ops.COINBASE: block_instructions.coinbase, + Ops.TIMESTAMP: block_instructions.timestamp, + Ops.NUMBER: block_instructions.number, + Ops.PREVRANDAO: block_instructions.prev_randao, + Ops.GASLIMIT: block_instructions.gas_limit, + Ops.CHAINID: block_instructions.chain_id, + Ops.MLOAD: memory_instructions.mload, + Ops.MSTORE: memory_instructions.mstore, + Ops.MSTORE8: memory_instructions.mstore8, + Ops.MSIZE: memory_instructions.msize, + Ops.MCOPY: memory_instructions.mcopy, + Ops.ADDRESS: environment_instructions.address, + Ops.BALANCE: environment_instructions.balance, + Ops.ORIGIN: environment_instructions.origin, + Ops.CALLER: environment_instructions.caller, + Ops.CALLVALUE: environment_instructions.callvalue, + Ops.CALLDATALOAD: environment_instructions.calldataload, + Ops.CALLDATASIZE: environment_instructions.calldatasize, + Ops.CALLDATACOPY: environment_instructions.calldatacopy, + Ops.CODESIZE: environment_instructions.codesize, + Ops.CODECOPY: environment_instructions.codecopy, + Ops.GASPRICE: environment_instructions.gasprice, + Ops.EXTCODESIZE: environment_instructions.extcodesize, + Ops.EXTCODECOPY: environment_instructions.extcodecopy, + Ops.RETURNDATASIZE: environment_instructions.returndatasize, + Ops.RETURNDATACOPY: environment_instructions.returndatacopy, + Ops.EXTCODEHASH: environment_instructions.extcodehash, + Ops.SELFBALANCE: environment_instructions.self_balance, + Ops.BASEFEE: environment_instructions.base_fee, + Ops.BLOBHASH: environment_instructions.blob_hash, + Ops.BLOBBASEFEE: environment_instructions.blob_base_fee, + Ops.SSTORE: storage_instructions.sstore, + Ops.TLOAD: storage_instructions.tload, + Ops.TSTORE: storage_instructions.tstore, + Ops.JUMP: control_flow_instructions.jump, + Ops.JUMPI: control_flow_instructions.jumpi, + Ops.PC: control_flow_instructions.pc, + Ops.GAS: control_flow_instructions.gas_left, + Ops.JUMPDEST: control_flow_instructions.jumpdest, + Ops.POP: stack_instructions.pop, + Ops.PUSH0: stack_instructions.push0, + Ops.PUSH1: stack_instructions.push1, + Ops.PUSH2: stack_instructions.push2, + Ops.PUSH3: stack_instructions.push3, + Ops.PUSH4: stack_instructions.push4, + Ops.PUSH5: stack_instructions.push5, + Ops.PUSH6: stack_instructions.push6, + Ops.PUSH7: stack_instructions.push7, + Ops.PUSH8: stack_instructions.push8, + Ops.PUSH9: stack_instructions.push9, + Ops.PUSH10: stack_instructions.push10, + Ops.PUSH11: stack_instructions.push11, + Ops.PUSH12: stack_instructions.push12, + Ops.PUSH13: stack_instructions.push13, + Ops.PUSH14: stack_instructions.push14, + Ops.PUSH15: stack_instructions.push15, + Ops.PUSH16: stack_instructions.push16, + Ops.PUSH17: stack_instructions.push17, + Ops.PUSH18: stack_instructions.push18, + Ops.PUSH19: stack_instructions.push19, + Ops.PUSH20: stack_instructions.push20, + Ops.PUSH21: stack_instructions.push21, + Ops.PUSH22: stack_instructions.push22, + Ops.PUSH23: stack_instructions.push23, + Ops.PUSH24: stack_instructions.push24, + Ops.PUSH25: stack_instructions.push25, + Ops.PUSH26: stack_instructions.push26, + Ops.PUSH27: stack_instructions.push27, + Ops.PUSH28: stack_instructions.push28, + Ops.PUSH29: stack_instructions.push29, + Ops.PUSH30: stack_instructions.push30, + Ops.PUSH31: stack_instructions.push31, + Ops.PUSH32: stack_instructions.push32, + Ops.DUP1: stack_instructions.dup1, + Ops.DUP2: stack_instructions.dup2, + Ops.DUP3: stack_instructions.dup3, + Ops.DUP4: stack_instructions.dup4, + Ops.DUP5: stack_instructions.dup5, + Ops.DUP6: stack_instructions.dup6, + Ops.DUP7: stack_instructions.dup7, + Ops.DUP8: stack_instructions.dup8, + Ops.DUP9: stack_instructions.dup9, + Ops.DUP10: stack_instructions.dup10, + Ops.DUP11: stack_instructions.dup11, + Ops.DUP12: stack_instructions.dup12, + Ops.DUP13: stack_instructions.dup13, + Ops.DUP14: stack_instructions.dup14, + Ops.DUP15: stack_instructions.dup15, + Ops.DUP16: stack_instructions.dup16, + Ops.SWAP1: stack_instructions.swap1, + Ops.SWAP2: stack_instructions.swap2, + Ops.SWAP3: stack_instructions.swap3, + Ops.SWAP4: stack_instructions.swap4, + Ops.SWAP5: stack_instructions.swap5, + Ops.SWAP6: stack_instructions.swap6, + Ops.SWAP7: stack_instructions.swap7, + Ops.SWAP8: stack_instructions.swap8, + Ops.SWAP9: stack_instructions.swap9, + Ops.SWAP10: stack_instructions.swap10, + Ops.SWAP11: stack_instructions.swap11, + Ops.SWAP12: stack_instructions.swap12, + Ops.SWAP13: stack_instructions.swap13, + Ops.SWAP14: stack_instructions.swap14, + Ops.SWAP15: stack_instructions.swap15, + Ops.SWAP16: stack_instructions.swap16, + Ops.LOG0: log_instructions.log0, + Ops.LOG1: log_instructions.log1, + Ops.LOG2: log_instructions.log2, + Ops.LOG3: log_instructions.log3, + Ops.LOG4: log_instructions.log4, + Ops.CREATE: system_instructions.create, + Ops.RETURN: system_instructions.return_, + Ops.CALL: system_instructions.call, + Ops.CALLCODE: system_instructions.callcode, + Ops.DELEGATECALL: system_instructions.delegatecall, + Ops.SELFDESTRUCT: system_instructions.selfdestruct, + Ops.STATICCALL: system_instructions.staticcall, + Ops.REVERT: system_instructions.revert, + Ops.CREATE2: system_instructions.create2, +} diff --git a/src/ethereum/forks/monad_next/vm/instructions/arithmetic.py b/src/ethereum/forks/monad_next/vm/instructions/arithmetic.py new file mode 100644 index 00000000000..4c7423cba8e --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/arithmetic.py @@ -0,0 +1,371 @@ +""" +Ethereum Virtual Machine (EVM) Arithmetic Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Arithmetic instructions. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import get_sign + +from .. import Evm +from ..gas import ( + GasCosts, + charge_gas, +) +from ..stack import pop, push + + +def add(evm: Evm) -> None: + """ + Adds the top two elements of the stack together, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_ADD) + + # OPERATION + result = x.wrapping_add(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sub(evm: Evm) -> None: + """ + Subtracts the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_SUB) + + # OPERATION + result = x.wrapping_sub(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mul(evm: Evm) -> None: + """ + Multiplies the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_MUL) + + # OPERATION + result = x.wrapping_mul(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def div(evm: Evm) -> None: + """ + Integer division of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + dividend = pop(evm.stack) + divisor = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_DIV) + + # OPERATION + if divisor == 0: + quotient = U256(0) + else: + quotient = dividend // divisor + + push(evm.stack, quotient) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +U255_CEIL_VALUE = 2**255 + + +def sdiv(evm: Evm) -> None: + """ + Signed integer division of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + dividend = pop(evm.stack).to_signed() + divisor = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SDIV) + + # OPERATION + if divisor == 0: + quotient = 0 + elif dividend == -U255_CEIL_VALUE and divisor == -1: + quotient = -U255_CEIL_VALUE + else: + sign = get_sign(dividend * divisor) + quotient = sign * (abs(dividend) // abs(divisor)) + + push(evm.stack, U256.from_signed(quotient)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mod(evm: Evm) -> None: + """ + Modulo remainder of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_MOD) + + # OPERATION + if y == 0: + remainder = U256(0) + else: + remainder = x % y + + push(evm.stack, remainder) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def smod(evm: Evm) -> None: + """ + Signed modulo remainder of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack).to_signed() + y = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SMOD) + + # OPERATION + if y == 0: + remainder = 0 + else: + remainder = get_sign(x) * (abs(x) % abs(y)) + + push(evm.stack, U256.from_signed(remainder)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def addmod(evm: Evm) -> None: + """ + Modulo addition of the top 2 elements with the 3rd element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_ADDMOD) + + # OPERATION + if z == 0: + result = U256(0) + else: + result = U256((x + y) % z) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mulmod(evm: Evm) -> None: + """ + Modulo multiplication of the top 2 elements with the 3rd element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_MULMOD) + + # OPERATION + if z == 0: + result = U256(0) + else: + result = U256((x * y) % z) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def exp(evm: Evm) -> None: + """ + Exponential operation of the top 2 elements. Pushes the result back on + the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + base = Uint(pop(evm.stack)) + exponent = Uint(pop(evm.stack)) + + # GAS + # This is equivalent to 1 + floor(log(y, 256)). But in python the log + # function is inaccurate leading to wrong results. + exponent_bits = exponent.bit_length() + exponent_bytes = (exponent_bits + Uint(7)) // Uint(8) + charge_gas( + evm, + GasCosts.OPCODE_EXP_BASE + + GasCosts.OPCODE_EXP_PER_BYTE * exponent_bytes, + ) + + # OPERATION + result = U256(pow(base, exponent, Uint(U256.MAX_VALUE) + Uint(1))) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signextend(evm: Evm) -> None: + """ + Sign extend operation. In other words, extend a signed number which + fits in N bytes to 32 bytes. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + byte_num = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_SIGNEXTEND) + + # OPERATION + if byte_num > U256(31): + # Can't extend any further + result = value + else: + # U256(0).to_be_bytes() gives b'' instead of b'\x00'. + value_bytes = Bytes(value.to_be_bytes32()) + # Now among the obtained value bytes, consider only + # N `least significant bytes`, where N is `byte_num + 1`. + value_bytes = value_bytes[31 - int(byte_num) :] + sign_bit = value_bytes[0] >> 7 + if sign_bit == 0: + result = U256.from_be_bytes(value_bytes) + else: + num_bytes_prepend = U256(32) - (byte_num + U256(1)) + result = U256.from_be_bytes( + bytearray([0xFF] * num_bytes_prepend) + value_bytes + ) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/bitwise.py b/src/ethereum/forks/monad_next/vm/instructions/bitwise.py new file mode 100644 index 00000000000..7674d3c720f --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/bitwise.py @@ -0,0 +1,277 @@ +""" +Ethereum Virtual Machine (EVM) Bitwise Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM bitwise instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import ( + GasCosts, + charge_gas, +) +from ..stack import pop, push + + +def bitwise_and(evm: Evm) -> None: + """ + Bitwise AND operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_AND) + + # OPERATION + push(evm.stack, x & y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_or(evm: Evm) -> None: + """ + Bitwise OR operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_OR) + + # OPERATION + push(evm.stack, x | y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_xor(evm: Evm) -> None: + """ + Bitwise XOR operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_XOR) + + # OPERATION + push(evm.stack, x ^ y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_not(evm: Evm) -> None: + """ + Bitwise NOT operation of the top element of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_NOT) + + # OPERATION + push(evm.stack, ~x) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def get_byte(evm: Evm) -> None: + """ + For a word (defined by next top element of the stack), retrieve the + Nth byte (0-indexed and defined by top element of stack) from the + left (most significant) to right (least significant). + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + byte_index = pop(evm.stack) + word = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_BYTE) + + # OPERATION + if byte_index >= U256(32): + result = U256(0) + else: + extra_bytes_to_right = U256(31) - byte_index + # Remove the extra bytes in the right + word = word >> (extra_bytes_to_right * U256(8)) + # Remove the extra bytes in the left + word = word & U256(0xFF) + result = word + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_shl(evm: Evm) -> None: + """ + Logical shift left (SHL) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = Uint(pop(evm.stack)) + value = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_SHL) + + # OPERATION + if shift < Uint(256): + result = U256((value << shift) & Uint(U256.MAX_VALUE)) + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_shr(evm: Evm) -> None: + """ + Logical shift right (SHR) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_SHR) + + # OPERATION + if shift < U256(256): + result = value >> shift + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_sar(evm: Evm) -> None: + """ + Arithmetic shift right (SAR) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = int(pop(evm.stack)) + signed_value = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SAR) + + # OPERATION + if shift < 256: + result = U256.from_signed(signed_value >> shift) + elif signed_value >= 0: + result = U256(0) + else: + result = U256.MAX_VALUE + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def count_leading_zeros(evm: Evm) -> None: + """ + Count the number of leading zero bits in a 256-bit word. + + Pops one value from the stack and pushes the number of leading zero bits. + If the input is zero, pushes 256. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_CLZ) + + # OPERATION + bit_length = U256(x.bit_length()) + result = U256(256) - bit_length + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/block.py b/src/ethereum/forks/monad_next/vm/instructions/block.py new file mode 100644 index 00000000000..baa589c4395 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/block.py @@ -0,0 +1,261 @@ +""" +Ethereum Virtual Machine (EVM) Block Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM block instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import GasCosts, charge_gas +from ..stack import pop, push + + +def block_hash(evm: Evm) -> None: + """ + Push the hash of one of the 256 most recent complete blocks onto the + stack. The block number to hash is present at the top of the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackUnderflowError` + If `len(stack)` is less than `1`. + :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `20`. + + """ + # STACK + block_number = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_BLOCKHASH) + + # OPERATION + max_block_number = block_number + Uint(256) + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): + # Default hash to 0, if the block of interest is not yet on the chain + # (including the block which has the current executing transaction), + # or if the block's age is more than 256. + current_block_hash = b"\x00" + else: + current_block_hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] + + push(evm.stack, U256.from_be_bytes(current_block_hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def coinbase(evm: Evm) -> None: + """ + Push the current block's beneficiary address (address of the block miner) + onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_COINBASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def timestamp(evm: Evm) -> None: + """ + Push the current block's timestamp onto the stack. Here the timestamp + being referred to is actually the unix timestamp in seconds. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_TIMESTAMP) + + # OPERATION + push(evm.stack, evm.message.block_env.time) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def number(evm: Evm) -> None: + """ + Push the current block's number onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_NUMBER) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.number)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def prev_randao(evm: Evm) -> None: + """ + Push the `prev_randao` value onto the stack. + + The `prev_randao` value is the random output of the beacon chain's + randomness oracle for the previous block. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_PREVRANDAO) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.block_env.prev_randao)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gas_limit(evm: Evm) -> None: + """ + Push the current block's gas limit onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_GASLIMIT) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def chain_id(evm: Evm) -> None: + """ + Push the chain id onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CHAINID) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.chain_id)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/comparison.py b/src/ethereum/forks/monad_next/vm/instructions/comparison.py new file mode 100644 index 00000000000..22d3d8916b1 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/comparison.py @@ -0,0 +1,180 @@ +""" +Ethereum Virtual Machine (EVM) Comparison Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Comparison instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import ( + GasCosts, + charge_gas, +) +from ..stack import pop, push + + +def less_than(evm: Evm) -> None: + """ + Checks if the top element is less than the next top element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_LT) + + # OPERATION + result = U256(left < right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signed_less_than(evm: Evm) -> None: + """ + Signed less-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SLT) + + # OPERATION + result = U256(left < right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def greater_than(evm: Evm) -> None: + """ + Checks if the top element is greater than the next top element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_GT) + + # OPERATION + result = U256(left > right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signed_greater_than(evm: Evm) -> None: + """ + Signed greater-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SGT) + + # OPERATION + result = U256(left > right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def equal(evm: Evm) -> None: + """ + Checks if the top element is equal to the next top element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_EQ) + + # OPERATION + result = U256(left == right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def is_zero(evm: Evm) -> None: + """ + Checks if the top element is equal to 0. Pushes the result back on the + stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_ISZERO) + + # OPERATION + result = U256(x == 0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/control_flow.py b/src/ethereum/forks/monad_next/vm/instructions/control_flow.py new file mode 100644 index 00000000000..548a05d3163 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/control_flow.py @@ -0,0 +1,174 @@ +""" +Ethereum Virtual Machine (EVM) Control Flow Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM control flow instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ...vm.gas import ( + GasCosts, + charge_gas, +) +from .. import Evm +from ..exceptions import InvalidJumpDestError +from ..stack import pop, push + + +def stop(evm: Evm) -> None: + """ + Stop further execution of EVM code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + pass + + # OPERATION + evm.running = False + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def jump(evm: Evm) -> None: + """ + Alter the program counter to the location specified by the top of the + stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + jump_dest = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_JUMP) + + # OPERATION + if jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + + # PROGRAM COUNTER + evm.pc = Uint(jump_dest) + + +def jumpi(evm: Evm) -> None: + """ + Alter the program counter to the specified location if and only if a + condition is true. If the condition is not true, then the program counter + would increase only by 1. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + jump_dest = Uint(pop(evm.stack)) + conditional_value = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_JUMPI) + + # OPERATION + if conditional_value == 0: + destination = evm.pc + Uint(1) + elif jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + else: + destination = jump_dest + + # PROGRAM COUNTER + evm.pc = destination + + +def pc(evm: Evm) -> None: + """ + Push onto the stack the value of the program counter after reaching the + current instruction and without increasing it for the next instruction. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_PC) + + # OPERATION + push(evm.stack, U256(evm.pc)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gas_left(evm: Evm) -> None: + """ + Push the amount of available gas (including the corresponding reduction + for the cost of this instruction) onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_GAS) + + # OPERATION + push(evm.stack, U256(evm.gas_left)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def jumpdest(evm: Evm) -> None: + """ + Mark a valid destination for jumps. This is a noop, present only + to be used by `JUMP` and `JUMPI` opcodes to verify that their jump is + valid. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_JUMPDEST) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/environment.py b/src/ethereum/forks/monad_next/vm/instructions/environment.py new file mode 100644 index 00000000000..83d439bd751 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/environment.py @@ -0,0 +1,610 @@ +""" +Ethereum Virtual Machine (EVM) Environmental Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM environment related instructions. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U256, Uint, ulen + +from ethereum.state import EMPTY_ACCOUNT +from ethereum.utils.numeric import ceil32 + +from ...state_tracker import get_account, get_code +from ...utils.address import to_address_masked +from ...vm.memory import buffer_read, memory_write +from .. import Evm +from ..exceptions import OutOfBoundsRead +from ..gas import ( + GasCosts, + calculate_blob_gas_price, + calculate_gas_extend_memory, + charge_gas, + update_memory_high_watermark, +) +from ..stack import pop, push + + +def address(evm: Evm) -> None: + """ + Pushes the address of the current executing account to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_ADDRESS) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.current_target)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def balance(evm: Evm) -> None: + """ + Pushes the balance of the given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + charge_gas(evm, GasCosts.WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GasCosts.COLD_ACCOUNT_ACCESS) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + tx_state = evm.message.tx_env.state + balance = get_account(tx_state, address).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def origin(evm: Evm) -> None: + """ + Pushes the address of the original transaction sender to the stack. + The origin address can only be an EOA. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_ORIGIN) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def caller(evm: Evm) -> None: + """ + Pushes the address of the caller onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLER) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.caller)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def callvalue(evm: Evm) -> None: + """ + Push the value (in wei) sent with the call onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLVALUE) + + # OPERATION + push(evm.stack, evm.message.value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldataload(evm: Evm) -> None: + """ + Push a word (32 bytes) of the input data belonging to the current + environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_index = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLDATALOAD) + + # OPERATION + value = buffer_read(evm.message.data, start_index, U256(32)) + + push(evm.stack, U256.from_be_bytes(value)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldatasize(evm: Evm) -> None: + """ + Push the size of input data in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLDATASIZE) + + # OPERATION + push(evm.stack, U256(len(evm.message.data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldatacopy(evm: Evm) -> None: + """ + Copy a portion of the input data in current environment to memory. + + This will also expand the memory, in case that the memory is insufficient + to store the data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + data_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_CALLDATACOPY_BASE + copy_gas_cost + extend_memory.cost, + ) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + value = buffer_read(evm.message.data, data_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def codesize(evm: Evm) -> None: + """ + Push the size of code running in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CODESIZE) + + # OPERATION + push(evm.stack, U256(len(evm.code))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def codecopy(evm: Evm) -> None: + """ + Copy a portion of the code in current environment to memory. + + This will also expand the memory, in case that the memory is insufficient + to store the data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + code_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_CODECOPY_BASE + copy_gas_cost + extend_memory.cost, + ) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + value = buffer_read(evm.code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gasprice(evm: Evm) -> None: + """ + Push the gas price used in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_GASPRICE) + + # OPERATION + push(evm.stack, U256(evm.message.tx_env.gas_price)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodesize(evm: Evm) -> None: + """ + Push the code size of a given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) + + # OPERATION + tx_state = evm.message.tx_env.state + code = get_code(tx_state, get_account(tx_state, address).code_hash) + + codesize = U256(len(code)) + push(evm.stack, codesize) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodecopy(evm: Evm) -> None: + """ + Copy a portion of an account's code to memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + memory_start_index = pop(evm.stack) + code_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + if address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + tx_state = evm.message.tx_env.state + code = get_code(tx_state, get_account(tx_state, address).code_hash) + + value = buffer_read(code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def returndatasize(evm: Evm) -> None: + """ + Pushes the size of the return data buffer onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_RETURNDATASIZE) + + # OPERATION + push(evm.stack, U256(len(evm.return_data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def returndatacopy(evm: Evm) -> None: + """ + Copies data from the return data buffer to memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + return_data_start_position = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_RETURNDATACOPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_RETURNDATACOPY_BASE + + copy_gas_cost + + extend_memory.cost, + ) + update_memory_high_watermark(evm, extend_memory) + if Uint(return_data_start_position) + Uint(size) > ulen(evm.return_data): + raise OutOfBoundsRead + + evm.memory.data += b"\x00" * extend_memory.expand_by + value = evm.return_data[ + return_data_start_position : return_data_start_position + size + ] + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodehash(evm: Evm) -> None: + """ + Returns the keccak256 hash of a contract’s bytecode. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) + + # OPERATION + account = get_account(evm.message.tx_env.state, address) + + if account == EMPTY_ACCOUNT: + codehash = U256(0) + else: + codehash = U256.from_be_bytes(account.code_hash) + + push(evm.stack, codehash) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def self_balance(evm: Evm) -> None: + """ + Pushes the balance of the current address to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.FAST_STEP) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account( + evm.message.tx_env.state, evm.message.current_target + ).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def base_fee(evm: Evm) -> None: + """ + Pushes the base fee of the current block on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_BASEFEE) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def blob_hash(evm: Evm) -> None: + """ + Pushes the versioned hash at a particular index on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + index = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_BLOBHASH) + + # OPERATION + if int(index) < len(evm.message.tx_env.blob_versioned_hashes): + blob_hash = evm.message.tx_env.blob_versioned_hashes[index] + else: + blob_hash = Bytes32(b"\x00" * 32) + push(evm.stack, U256.from_be_bytes(blob_hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def blob_base_fee(evm: Evm) -> None: + """ + Pushes the blob base fee on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_BLOBBASEFEE) + + # OPERATION + blob_base_fee = calculate_blob_gas_price( + evm.message.block_env.excess_blob_gas + ) + push(evm.stack, U256(blob_base_fee)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/keccak.py b/src/ethereum/forks/monad_next/vm/instructions/keccak.py new file mode 100644 index 00000000000..fe827c605bd --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/keccak.py @@ -0,0 +1,67 @@ +""" +Ethereum Virtual Machine (EVM) Keccak Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM keccak instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.utils.numeric import ceil32 + +from .. import Evm +from ..gas import ( + GasCosts, + calculate_gas_extend_memory, + charge_gas, + update_memory_high_watermark, +) +from ..memory import memory_read_bytes +from ..stack import pop, push + + +def keccak(evm: Evm) -> None: + """ + Pushes to the stack the Keccak-256 hash of a region of memory. + + This also expands the memory, in case the memory is insufficient to + access the data's memory location. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + word_gas_cost = GasCosts.OPCODE_KECCAK256_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_KECCAK256_BASE + word_gas_cost + extend_memory.cost, + ) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + data = memory_read_bytes(evm.memory, memory_start_index, size) + hashed = keccak256(data) + + push(evm.stack, U256.from_be_bytes(hashed)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/log.py b/src/ethereum/forks/monad_next/vm/instructions/log.py new file mode 100644 index 00000000000..bc81b0e8a63 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/log.py @@ -0,0 +1,89 @@ +""" +Ethereum Virtual Machine (EVM) Logging Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM logging instructions. +""" + +from functools import partial +from typing import Callable + +from ethereum_types.numeric import Uint + +from ...blocks import Log +from .. import Evm +from ..exceptions import WriteInStaticContext +from ..gas import ( + GasCosts, + calculate_gas_extend_memory, + charge_gas, + update_memory_high_watermark, +) +from ..memory import memory_read_bytes +from ..stack import pop + + +def log_n(evm: Evm, num_topics: int) -> None: + """ + Appends a log entry, having `num_topics` topics, to the evm logs. + + This will also expand the memory if the data (required by the log entry) + corresponding to the memory is not accessible. + + Parameters + ---------- + evm : + The current EVM frame. + num_topics : + The number of topics to be included in the log entry. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + topics = [] + for _ in range(num_topics): + topic = pop(evm.stack).to_be_bytes32() + topics.append(topic) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_LOG_BASE + + GasCosts.OPCODE_LOG_DATA_PER_BYTE * Uint(size) + + GasCosts.OPCODE_LOG_TOPIC * Uint(num_topics) + + extend_memory.cost, + ) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + if evm.message.is_static: + raise WriteInStaticContext + log_entry = Log( + address=evm.message.current_target, + topics=tuple(topics), + data=memory_read_bytes(evm.memory, memory_start_index, size), + ) + + evm.logs = evm.logs + (log_entry,) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +log0: Callable[[Evm], None] = partial(log_n, num_topics=0) +log1: Callable[[Evm], None] = partial(log_n, num_topics=1) +log2: Callable[[Evm], None] = partial(log_n, num_topics=2) +log3: Callable[[Evm], None] = partial(log_n, num_topics=3) +log4: Callable[[Evm], None] = partial(log_n, num_topics=4) diff --git a/src/ethereum/forks/monad_next/vm/instructions/memory.py b/src/ethereum/forks/monad_next/vm/instructions/memory.py new file mode 100644 index 00000000000..beef91e7493 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/memory.py @@ -0,0 +1,183 @@ +""" +Ethereum Virtual Machine (EVM) Memory Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Memory instructions. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import ceil32 + +from .. import Evm +from ..gas import ( + GasCosts, + calculate_gas_extend_memory, + charge_gas, + update_memory_high_watermark, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def mstore(evm: Evm) -> None: + """ + Stores a word to memory. + This also expands the memory, if the memory is + insufficient to store the word. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + value = pop(evm.stack).to_be_bytes32() + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(len(value)))] + ) + + charge_gas(evm, GasCosts.OPCODE_MSTORE_BASE + extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + memory_write(evm.memory, start_position, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mstore8(evm: Evm) -> None: + """ + Stores a byte to memory. + This also expands the memory, if the memory is + insufficient to store the word. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + value = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(1))] + ) + + charge_gas(evm, GasCosts.OPCODE_MSTORE8_BASE + extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + normalized_bytes_value = Bytes([value & U256(0xFF)]) + memory_write(evm.memory, start_position, normalized_bytes_value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mload(evm: Evm) -> None: + """ + Loads a word from memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(32))] + ) + charge_gas(evm, GasCosts.OPCODE_MLOAD_BASE + extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + value = U256.from_be_bytes( + memory_read_bytes(evm.memory, start_position, U256(32)) + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def msize(evm: Evm) -> None: + """ + Pushes the size of active memory in bytes onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_MSIZE) + + # OPERATION + push(evm.stack, U256(len(evm.memory.data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mcopy(evm: Evm) -> None: + """ + Copies the bytes in memory from one location to another. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + destination = pop(evm.stack) + source = pop(evm.stack) + length = pop(evm.stack) + + # GAS + words = ceil32(Uint(length)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + + extend_memory = calculate_gas_extend_memory( + evm.memory, [(source, length), (destination, length)] + ) + charge_gas( + evm, + GasCosts.OPCODE_MCOPY_BASE + copy_gas_cost + extend_memory.cost, + ) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + value = memory_read_bytes(evm.memory, source, length) + memory_write(evm.memory, destination, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/stack.py b/src/ethereum/forks/monad_next/vm/instructions/stack.py new file mode 100644 index 00000000000..ce94af6ce8e --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/stack.py @@ -0,0 +1,212 @@ +""" +Ethereum Virtual Machine (EVM) Stack Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM stack related instructions. +""" + +from functools import partial +from typing import Callable + +from ethereum_types.numeric import U256, Uint + +from .. import Evm, stack +from ..exceptions import StackUnderflowError +from ..gas import ( + GasCosts, + charge_gas, +) +from ..memory import buffer_read + + +def pop(evm: Evm) -> None: + """ + Removes an item from the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + stack.pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_POP) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def push_n(evm: Evm, num_bytes: int) -> None: + """ + Pushes an N-byte immediate onto the stack. Push zero if num_bytes is zero. + + Parameters + ---------- + evm : + The current EVM frame. + + num_bytes : + The number of immediate bytes to be read from the code and pushed to + the stack. Push zero if num_bytes is zero. + + """ + # STACK + pass + + # GAS + if num_bytes == 0: + charge_gas(evm, GasCosts.OPCODE_PUSH0) + else: + charge_gas(evm, GasCosts.OPCODE_PUSH) + + # OPERATION + data_to_push = U256.from_be_bytes( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(num_bytes)) + ) + stack.push(evm.stack, data_to_push) + + # PROGRAM COUNTER + evm.pc += Uint(1) + Uint(num_bytes) + + +def dup_n(evm: Evm, item_number: int) -> None: + """ + Duplicates the Nth stack item (from top of the stack) to the top of stack. + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be duplicated + to the top of stack. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_DUP) + if item_number >= len(evm.stack): + raise StackUnderflowError + data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] + stack.push(evm.stack, data_to_duplicate) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def swap_n(evm: Evm, item_number: int) -> None: + """ + Swaps the top and the `item_number` element of the stack, where + the top of the stack is position zero. + + If `item_number` is zero, this function does nothing (which should not be + possible, since there is no `SWAP0` instruction). + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be swapped + with the top of stack element. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_SWAP) + if item_number >= len(evm.stack): + raise StackUnderflowError + evm.stack[-1], evm.stack[-1 - item_number] = ( + evm.stack[-1 - item_number], + evm.stack[-1], + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +push0: Callable[[Evm], None] = partial(push_n, num_bytes=0) +push1: Callable[[Evm], None] = partial(push_n, num_bytes=1) +push2: Callable[[Evm], None] = partial(push_n, num_bytes=2) +push3: Callable[[Evm], None] = partial(push_n, num_bytes=3) +push4: Callable[[Evm], None] = partial(push_n, num_bytes=4) +push5: Callable[[Evm], None] = partial(push_n, num_bytes=5) +push6: Callable[[Evm], None] = partial(push_n, num_bytes=6) +push7: Callable[[Evm], None] = partial(push_n, num_bytes=7) +push8: Callable[[Evm], None] = partial(push_n, num_bytes=8) +push9: Callable[[Evm], None] = partial(push_n, num_bytes=9) +push10: Callable[[Evm], None] = partial(push_n, num_bytes=10) +push11: Callable[[Evm], None] = partial(push_n, num_bytes=11) +push12: Callable[[Evm], None] = partial(push_n, num_bytes=12) +push13: Callable[[Evm], None] = partial(push_n, num_bytes=13) +push14: Callable[[Evm], None] = partial(push_n, num_bytes=14) +push15: Callable[[Evm], None] = partial(push_n, num_bytes=15) +push16: Callable[[Evm], None] = partial(push_n, num_bytes=16) +push17: Callable[[Evm], None] = partial(push_n, num_bytes=17) +push18: Callable[[Evm], None] = partial(push_n, num_bytes=18) +push19: Callable[[Evm], None] = partial(push_n, num_bytes=19) +push20: Callable[[Evm], None] = partial(push_n, num_bytes=20) +push21: Callable[[Evm], None] = partial(push_n, num_bytes=21) +push22: Callable[[Evm], None] = partial(push_n, num_bytes=22) +push23: Callable[[Evm], None] = partial(push_n, num_bytes=23) +push24: Callable[[Evm], None] = partial(push_n, num_bytes=24) +push25: Callable[[Evm], None] = partial(push_n, num_bytes=25) +push26: Callable[[Evm], None] = partial(push_n, num_bytes=26) +push27: Callable[[Evm], None] = partial(push_n, num_bytes=27) +push28: Callable[[Evm], None] = partial(push_n, num_bytes=28) +push29: Callable[[Evm], None] = partial(push_n, num_bytes=29) +push30: Callable[[Evm], None] = partial(push_n, num_bytes=30) +push31: Callable[[Evm], None] = partial(push_n, num_bytes=31) +push32: Callable[[Evm], None] = partial(push_n, num_bytes=32) + +dup1: Callable[[Evm], None] = partial(dup_n, item_number=0) +dup2: Callable[[Evm], None] = partial(dup_n, item_number=1) +dup3: Callable[[Evm], None] = partial(dup_n, item_number=2) +dup4: Callable[[Evm], None] = partial(dup_n, item_number=3) +dup5: Callable[[Evm], None] = partial(dup_n, item_number=4) +dup6: Callable[[Evm], None] = partial(dup_n, item_number=5) +dup7: Callable[[Evm], None] = partial(dup_n, item_number=6) +dup8: Callable[[Evm], None] = partial(dup_n, item_number=7) +dup9: Callable[[Evm], None] = partial(dup_n, item_number=8) +dup10: Callable[[Evm], None] = partial(dup_n, item_number=9) +dup11: Callable[[Evm], None] = partial(dup_n, item_number=10) +dup12: Callable[[Evm], None] = partial(dup_n, item_number=11) +dup13: Callable[[Evm], None] = partial(dup_n, item_number=12) +dup14: Callable[[Evm], None] = partial(dup_n, item_number=13) +dup15: Callable[[Evm], None] = partial(dup_n, item_number=14) +dup16: Callable[[Evm], None] = partial(dup_n, item_number=15) + +swap1: Callable[[Evm], None] = partial(swap_n, item_number=1) +swap2: Callable[[Evm], None] = partial(swap_n, item_number=2) +swap3: Callable[[Evm], None] = partial(swap_n, item_number=3) +swap4: Callable[[Evm], None] = partial(swap_n, item_number=4) +swap5: Callable[[Evm], None] = partial(swap_n, item_number=5) +swap6: Callable[[Evm], None] = partial(swap_n, item_number=6) +swap7: Callable[[Evm], None] = partial(swap_n, item_number=7) +swap8: Callable[[Evm], None] = partial(swap_n, item_number=8) +swap9: Callable[[Evm], None] = partial(swap_n, item_number=9) +swap10: Callable[[Evm], None] = partial(swap_n, item_number=10) +swap11: Callable[[Evm], None] = partial(swap_n, item_number=11) +swap12: Callable[[Evm], None] = partial(swap_n, item_number=12) +swap13: Callable[[Evm], None] = partial(swap_n, item_number=13) +swap14: Callable[[Evm], None] = partial(swap_n, item_number=14) +swap15: Callable[[Evm], None] = partial(swap_n, item_number=15) +swap16: Callable[[Evm], None] = partial(swap_n, item_number=16) diff --git a/src/ethereum/forks/monad_next/vm/instructions/storage.py b/src/ethereum/forks/monad_next/vm/instructions/storage.py new file mode 100644 index 00000000000..65fd48c366a --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/storage.py @@ -0,0 +1,181 @@ +""" +Ethereum Virtual Machine (EVM) Storage Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM storage related instructions. +""" + +from ethereum_types.numeric import Uint + +from ...state_tracker import ( + get_storage, + get_transient_storage, + set_storage, + set_transient_storage, +) +from .. import Evm +from ..exceptions import OutOfGasError, WriteInStaticContext +from ..gas import ( + GasCosts, + charge_gas, + page_index, +) +from ..stack import pop, push + + +def sload(evm: Evm) -> None: + """ + Load a value from storage to the stack, with page-level access + tracking per MIP-8. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + slot = pop(evm.stack) + key = slot.to_be_bytes32() + + # GAS + page_key = (evm.message.current_target, page_index(slot)) + if page_key in evm.read_accessed_pages: + charge_gas(evm, GasCosts.PAGE_BASE_COST) + else: + evm.read_accessed_pages.add(page_key) + charge_gas(evm, GasCosts.PAGE_LOAD_COST + GasCosts.PAGE_BASE_COST) + + # OPERATION + tx_state = evm.message.tx_env.state + value = get_storage(tx_state, evm.message.current_target, key) + + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sstore(evm: Evm) -> None: + """ + Store a value in storage, with page-level I/O cost and per-page + state growth tracking per MIP-8. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + slot = pop(evm.stack) + key = slot.to_be_bytes32() + new_value = pop(evm.stack) + if evm.gas_left <= GasCosts.CALL_STIPEND: + raise OutOfGasError + + tx_state = evm.message.tx_env.state + target = evm.message.current_target + current_value = get_storage(tx_state, target, key) + + page_key = (target, page_index(slot)) + + # Page I/O cost + gas_cost = GasCosts.PAGE_BASE_COST + + if page_key not in evm.read_accessed_pages: + gas_cost += GasCosts.PAGE_LOAD_COST + evm.read_accessed_pages.add(page_key) + + if current_value != new_value: + if page_key not in evm.write_accessed_pages: + gas_cost += GasCosts.PAGE_WRITE_COST + evm.write_accessed_pages.add(page_key) + evm.current_state_growth[page_key] = 0 + evm.net_state_growth[page_key] = 0 + + # State growth cost + if current_value == 0 and new_value != 0: + evm.current_state_growth[page_key] = ( + evm.current_state_growth.get(page_key, 0) + 1 + ) + elif current_value != 0 and new_value == 0: + evm.current_state_growth[page_key] = ( + evm.current_state_growth.get(page_key, 0) - 1 + ) + + current = evm.current_state_growth.get(page_key, 0) + peak = evm.net_state_growth.get(page_key, 0) + if current > peak: + gas_cost += GasCosts.PAGE_STATE_GROWTH_COST + evm.net_state_growth[page_key] = current + + charge_gas(evm, gas_cost) + if evm.message.is_static: + raise WriteInStaticContext + set_storage(tx_state, target, key, new_value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def tload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + transient storage of the current account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + charge_gas(evm, GasCosts.WARM_ACCESS) + + # OPERATION + value = get_transient_storage( + evm.message.tx_env.state, evm.message.current_target, key + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def tstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's transient storage. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.WARM_ACCESS) + if evm.message.is_static: + raise WriteInStaticContext + set_transient_storage( + evm.message.tx_env.state, + evm.message.current_target, + key, + new_value, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/instructions/system.py b/src/ethereum/forks/monad_next/vm/instructions/system.py new file mode 100644 index 00000000000..7b8634c5d7e --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/instructions/system.py @@ -0,0 +1,787 @@ +""" +Ethereum Virtual Machine (EVM) System Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM system related instructions. +""" + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import U256, Uint + +from ethereum.state import Address +from ethereum.utils.numeric import ceil32 + +from ...state_tracker import ( + account_has_code_or_nonce, + account_has_storage, + get_account, + increment_nonce, + is_account_alive, + move_ether, + set_account_balance, +) +from ...utils.address import ( + compute_contract_address, + compute_create2_contract_address, + to_address_masked, +) +from ...vm.eoa_delegation import access_delegation +from .. import ( + Evm, + Message, + incorporate_child_on_error, + incorporate_child_on_success, +) +from ..exceptions import ( + CreateIn7702Context, + OutOfGasError, + Revert, + WriteInStaticContext, +) +from ..gas import ( + GasCosts, + calculate_gas_extend_memory, + calculate_message_call_gas, + charge_gas, + init_code_cost, + max_message_call_gas, + update_memory_high_watermark, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def generic_create( + evm: Evm, + endowment: U256, + contract_address: Address, + memory_start_position: U256, + memory_size: U256, +) -> None: + """ + Core logic used by the `CREATE*` family of opcodes. + """ + # This import causes a circular import error + # if it's not moved inside this method + from ...vm.interpreter import ( + MAX_INIT_CODE_SIZE, + STACK_DEPTH_LIMIT, + process_create_message, + ) + + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + if len(call_data) > MAX_INIT_CODE_SIZE: + raise OutOfGasError + + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) + evm.gas_left -= create_message_gas + if evm.message.is_static: + raise WriteInStaticContext + if evm.message.disable_create_opcodes: + raise CreateIn7702Context + evm.return_data = b"" + + sender_address = evm.message.current_target + sender = get_account(evm.message.tx_env.state, sender_address) + + if ( + sender.balance < endowment + or sender.nonce == Uint(2**64 - 1) + or evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT + ): + evm.gas_left += create_message_gas + push(evm.stack, U256(0)) + return + + evm.accessed_addresses.add(contract_address) + + if account_has_code_or_nonce( + evm.message.tx_env.state, contract_address + ) or account_has_storage(evm.message.tx_env.state, contract_address): + increment_nonce(evm.message.tx_env.state, evm.message.current_target) + push(evm.stack, U256(0)) + return + + increment_nonce(evm.message.tx_env.state, evm.message.current_target) + + child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, + caller=evm.message.current_target, + target=Bytes0(), + gas=create_message_gas, + value=endowment, + data=b"", + code=call_data, + current_target=contract_address, + depth=evm.message.depth + Uint(1), + code_address=None, + should_transfer_value=True, + is_static=False, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + disable_precompiles=False, + parent_evm=evm, + disable_create_opcodes=False, + ) + child_evm = process_create_message(child_message) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = b"" + push(evm.stack, U256.from_be_bytes(child_evm.message.current_target)) + + +def create(evm: Evm) -> None: + """ + Creates a new account with associated code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + init_code_gas = init_code_cost(Uint(memory_size)) + + charge_gas( + evm, + GasCosts.OPCODE_CREATE_BASE + extend_memory.cost + init_code_gas, + ) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + contract_address = compute_contract_address( + evm.message.current_target, + get_account( + evm.message.tx_env.state, evm.message.current_target + ).nonce, + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def create2(evm: Evm) -> None: + """ + Creates a new account with associated code. + + It's similar to the CREATE opcode except that the address of the new + account depends on the init_code instead of the nonce of sender. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + salt = pop(evm.stack).to_be_bytes32() + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + call_data_words = ceil32(Uint(memory_size)) // Uint(32) + init_code_gas = init_code_cost(Uint(memory_size)) + charge_gas( + evm, + GasCosts.OPCODE_CREATE_BASE + + GasCosts.OPCODE_KECCAK256_PER_WORD * call_data_words + + extend_memory.cost + + init_code_gas, + ) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + contract_address = compute_create2_contract_address( + evm.message.current_target, + salt, + memory_read_bytes(evm.memory, memory_start_position, memory_size), + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def return_(evm: Evm) -> None: + """ + Halts execution returning output data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + + charge_gas(evm, GasCosts.ZERO + extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + evm.output = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + evm.running = False + + # PROGRAM COUNTER + pass + + +def generic_call( + evm: Evm, + gas: Uint, + value: U256, + caller: Address, + to: Address, + code_address: Address, + should_transfer_value: bool, + is_staticcall: bool, + memory_input_start_position: U256, + memory_input_size: U256, + memory_output_start_position: U256, + memory_output_size: U256, + code: Bytes, + disable_precompiles: bool, + disable_create_opcodes: bool, +) -> None: + """ + Perform the core logic of the `CALL*` family of opcodes. + """ + from ...vm.interpreter import STACK_DEPTH_LIMIT, process_message + + evm.return_data = b"" + + if evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT: + evm.gas_left += gas + push(evm.stack, U256(0)) + return + + call_data = memory_read_bytes( + evm.memory, memory_input_start_position, memory_input_size + ) + + child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, + caller=caller, + target=to, + gas=gas, + value=value, + data=call_data, + code=code, + current_target=to, + depth=evm.message.depth + Uint(1), + code_address=code_address, + should_transfer_value=should_transfer_value, + is_static=True if is_staticcall else evm.message.is_static, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + disable_precompiles=disable_precompiles, + parent_evm=evm, + disable_create_opcodes=disable_create_opcodes, + ) + child_evm = process_message(child_message) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, U256(1)) + + actual_output_size = min(memory_output_size, U256(len(child_evm.output))) + memory_write( + evm.memory, + memory_output_start_position, + child_evm.output[:actual_output_size], + ) + + +def call(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address_masked(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if to in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(to) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + code_address = to + ( + disable_precompiles, + disable_create_opcodes, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + + create_gas_cost = GasCosts.NEW_ACCOUNT + if value == 0 or is_account_alive(evm.message.tx_env.state, to): + create_gas_cost = Uint(0) + transfer_gas_cost = Uint(0) if value == 0 else GasCosts.CALL_VALUE + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + create_gas_cost + transfer_gas_cost, + ) + charge_gas( + evm, + access_gas_cost + + create_gas_cost + + transfer_gas_cost + + extend_memory.cost, + ) + update_memory_high_watermark(evm, extend_memory) + charge_gas(evm, message_call_gas.cost) + if evm.message.is_static and value != U256(0): + raise WriteInStaticContext + evm.memory.data += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.message.tx_env.state, evm.message.current_target + ).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.sub_call + else: + generic_call( + evm, + message_call_gas.sub_call, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + disable_create_opcodes, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def callcode(evm: Evm) -> None: + """ + Message-call into this account with alternative account’s code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address_masked(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + to = evm.message.current_target + + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if code_address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(code_address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + ( + disable_precompiles, + disable_create_opcodes, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + + transfer_gas_cost = Uint(0) if value == 0 else GasCosts.CALL_VALUE + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + transfer_gas_cost, + ) + charge_gas(evm, access_gas_cost + transfer_gas_cost + extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + charge_gas(evm, message_call_gas.cost) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.message.tx_env.state, + evm.message.current_target, + ).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.sub_call + else: + generic_call( + evm, + message_call_gas.sub_call, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + disable_create_opcodes, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def selfdestruct(evm: Evm) -> None: + """ + Halt execution and register account for later deletion. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + beneficiary = to_address_masked(pop(evm.stack)) + + # GAS + gas_cost = GasCosts.OPCODE_SELFDESTRUCT_BASE + if beneficiary not in evm.accessed_addresses: + evm.accessed_addresses.add(beneficiary) + gas_cost += GasCosts.COLD_ACCOUNT_ACCESS + + if ( + not is_account_alive(evm.message.tx_env.state, beneficiary) + and get_account( + evm.message.tx_env.state, + evm.message.current_target, + ).balance + != 0 + ): + gas_cost += GasCosts.OPCODE_SELFDESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) + if evm.message.is_static: + raise WriteInStaticContext + + originator = evm.message.current_target + originator_balance = get_account( + evm.message.tx_env.state, originator + ).balance + + move_ether( + evm.message.tx_env.state, + originator, + beneficiary, + originator_balance, + ) + + # register account for deletion only if it was created + # in the same transaction + if originator in evm.message.tx_env.state.created_accounts: + # If beneficiary is the same as originator, then + # the ether is burnt. + set_account_balance(evm.message.tx_env.state, originator, U256(0)) + evm.accounts_to_delete.add(originator) + + # HALT the execution + evm.running = False + + # PROGRAM COUNTER + pass + + +def delegatecall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address_masked(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if code_address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(code_address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + ( + disable_precompiles, + disable_create_opcodes, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost, + ) + charge_gas(evm, access_gas_cost + extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + charge_gas(evm, message_call_gas.cost) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.sub_call, + evm.message.value, + evm.message.caller, + evm.message.current_target, + code_address, + False, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + disable_create_opcodes, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def staticcall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address_masked(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if to in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(to) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + code_address = to + ( + disable_precompiles, + disable_create_opcodes, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost, + ) + charge_gas(evm, access_gas_cost + extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + charge_gas(evm, message_call_gas.cost) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.sub_call, + U256(0), + evm.message.current_target, + to, + code_address, + True, + True, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + disable_create_opcodes, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def revert(evm: Evm) -> None: + """ + Stop execution and revert state changes, without consuming all provided gas + and also has the ability to return a reason. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + charge_gas(evm, extend_memory.cost) + update_memory_high_watermark(evm, extend_memory) + + # OPERATION + evm.memory.data += b"\x00" * extend_memory.expand_by + output = memory_read_bytes(evm.memory, memory_start_index, size) + evm.output = Bytes(output) + raise Revert + + # PROGRAM COUNTER + # no-op diff --git a/src/ethereum/forks/monad_next/vm/interpreter.py b/src/ethereum/forks/monad_next/vm/interpreter.py new file mode 100644 index 00000000000..912ac3e42e8 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/interpreter.py @@ -0,0 +1,452 @@ +""" +Ethereum Virtual Machine (EVM) Interpreter. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +A straightforward interpreter that executes EVM code. +""" + +from dataclasses import dataclass +from typing import Dict, Optional, Set, Tuple, final + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import U256, Uint, ulen + +from ethereum.exceptions import EthereumException +from ethereum.state import Address +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) + +from ..blocks import Log +from ..state_tracker import ( + account_has_code_or_nonce, + account_has_storage, + copy_tx_state, + destroy_storage, + get_account, + get_balance_original, + get_code, + increment_nonce, + is_sender_authority, + iter_all_addresses, + mark_account_created, + move_ether, + restore_tx_state, + set_code, +) +from ..vm import Message +from ..vm.eoa_delegation import ( + get_delegated_code_address, + is_valid_delegation, + set_delegation, +) +from ..vm.gas import GasCosts, charge_gas, page_index +from ..vm.precompiled_contracts import MONAD_PRECOMPILE_ADDRESSES +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from . import Evm, EvmMemory +from .exceptions import ( + AddressCollision, + ExceptionalHalt, + InvalidContractPrefix, + InvalidOpcode, + OutOfGasError, + Revert, + RevertInMonadPrecompile, + RevertOnReserveBalance, + StackDepthLimitError, +) +from .instructions import Ops, op_implementation +from .runtime import get_valid_jump_destinations + +STACK_DEPTH_LIMIT = Uint(1024) +MAX_CODE_SIZE = 128 * 1024 +MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE + +RESERVE_BALANCE = U256(10 * 10**18) # 10 MON + + +def is_reserve_balance_violated(evm: Evm) -> bool: + """ + Check if any EOA has violated the reserve balance constraint. + + Returns True if a violation is detected, False otherwise. + + Reads "balance at start of EVM execution" from + ``tx_env.tx_snapshot`` — captured at the top-level message begin + (after pre-execution gas/nonce deduction). Recreates pre-refactor + ``state._snapshots[0]`` semantics. Callable from anywhere inside + the tx (top-level end-of-tx check or the dippedIntoReserve + precompile). + """ + message = evm.message + tx_state = message.tx_env.state + tx_env = message.tx_env + snapshot = tx_env.tx_snapshot + assert snapshot is not None, ( + "tx_snapshot must be set on tx_env before reserve balance check" + ) + + # Collect accounts_to_delete from all ancestor frames. accounts_to_delete + # only propagates upward on success (incorporate_child_on_success), so a + # child frame like a precompile call won't see deletions from its parent. + all_accounts_to_delete: Set[Address] = set() + current_evm = evm + while True: + all_accounts_to_delete.update(current_evm.accounts_to_delete) + if current_evm.message.parent_evm is not None: + current_evm = current_evm.message.parent_evm + else: + break + + for addr in iter_all_addresses(tx_state): + # Account SELFDESTRUCTed - skip explicitly. + if addr in all_accounts_to_delete: + continue + + acc = get_account(tx_state, addr) + # For creation txs, code hasn't been set yet on the new contract + # (set_code runs after process_message returns). Use evm.output which + # holds the code to be deployed. + if ( + isinstance(message.target, Bytes0) + and addr == message.current_target + ): + code = evm.output + else: + code = get_code(tx_state, acc.code_hash) + + # NOTE: this also matches initcode ending with empty code deployments + # via `Op.STOP` or `Op.RETURN(0, 0)`, AND check made during initcode + # execution, but this aligns with Monad EVM implementation. + if code == b"" or is_valid_delegation(code): + original_balance = get_balance_original(snapshot, addr) + if tx_env.origin == addr: + # gas_fees already deducted, need to re-add if sender + # to match with spec. + gas_fees = U256(tx_env.gas_price * tx_env.tx_gas_limit) + original_balance += gas_fees + reserve = min(RESERVE_BALANCE, original_balance) + threshold = reserve - gas_fees + else: + threshold = RESERVE_BALANCE + + is_exception = ( + message.tx_env.origin == addr + and not is_sender_authority(tx_state.parent, addr) + and not is_valid_delegation(code) + ) + + if ( + acc.balance < original_balance + and acc.balance < threshold + and not is_exception + ): + return True + return False + + +@final +@dataclass +class MessageCallOutput: + """ + Output of a particular message call. + + Contains the following: + + 1. `gas_left`: remaining gas after execution. + 2. `refund_counter`: gas to refund after execution. + 3. `logs`: list of `Log` generated during execution. + 4. `accounts_to_delete`: Contracts which have self-destructed. + 5. `error`: The error from the execution if any. + 6. `return_data`: The output of the execution. + """ + + gas_left: Uint + refund_counter: U256 + logs: Tuple[Log, ...] + accounts_to_delete: Set[Address] + error: Optional[EthereumException] + return_data: Bytes + + +def process_message_call(message: Message) -> MessageCallOutput: + """ + If `message.target` is empty then it creates a smart contract + else it executes a call from the `message.caller` to the `message.target`. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + output : `MessageCallOutput` + Output of the message call + + """ + tx_state = message.tx_env.state + refund_counter = U256(0) + if message.target == Bytes0(b""): + is_collision = account_has_code_or_nonce( + tx_state, message.current_target + ) or account_has_storage(tx_state, message.current_target) + if is_collision: + return MessageCallOutput( + gas_left=Uint(0), + refund_counter=U256(0), + logs=tuple(), + accounts_to_delete=set(), + error=AddressCollision(), + return_data=Bytes(b""), + ) + else: + evm = process_create_message(message) + else: + if message.tx_env.authorizations != (): + refund_counter += set_delegation(message) + + delegated_address = get_delegated_code_address(message.code) + if delegated_address is not None: + message.disable_precompiles = True + message.accessed_addresses.add(delegated_address) + message.code = get_code( + tx_state, + get_account(tx_state, delegated_address).code_hash, + ) + message.code_address = delegated_address + message.disable_create_opcodes = True + + evm = process_message(message) + + if evm.error: + logs: Tuple[Log, ...] = () + accounts_to_delete = set() + else: + logs = evm.logs + accounts_to_delete = evm.accounts_to_delete + refund_counter += U256(evm.refund_counter) + + tx_end = TransactionEnd( + int(message.gas) - int(evm.gas_left), evm.output, evm.error + ) + evm_trace(evm, tx_end) + + return MessageCallOutput( + gas_left=evm.gas_left, + refund_counter=refund_counter, + logs=logs, + accounts_to_delete=accounts_to_delete, + error=evm.error, + return_data=evm.output, + ) + + +def process_create_message(message: Message) -> Evm: + """ + Executes a call to create a smart contract. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + evm: :py:class:`~ethereum.forks.monad_next.vm.Evm` + Items containing execution specific objects. + + """ + tx_state = message.tx_env.state + # take snapshot of state before processing the message + snapshot = copy_tx_state(tx_state) + + # If the address where the account is being created has storage, it is + # destroyed. This can only happen in the following highly unlikely + # circumstances: + # * The address created by a `CREATE` call collides with a subsequent + # `CREATE` or `CREATE2` call. + # * The first `CREATE` happened before Spurious Dragon and left empty + # code. + destroy_storage(tx_state, message.current_target) + + # In the previously mentioned edge case the preexisting storage is ignored + # for gas refund purposes. In order to do this we must track created + # accounts. This tracking is also needed to respect the constraints + # added to SELFDESTRUCT by EIP-6780. + mark_account_created(tx_state, message.current_target) + + increment_nonce(tx_state, message.current_target) + evm = process_message(message) + if not evm.error: + contract_code = evm.output + contract_code_gas = ( + ulen(contract_code) * GasCosts.CODE_DEPOSIT_PER_BYTE + ) + try: + if len(contract_code) > 0: + if contract_code[0] == 0xEF: + raise InvalidContractPrefix + charge_gas(evm, contract_code_gas) + if len(contract_code) > MAX_CODE_SIZE: + raise OutOfGasError + except ExceptionalHalt as error: + restore_tx_state(tx_state, snapshot) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + else: + set_code(tx_state, message.current_target, contract_code) + else: + restore_tx_state(tx_state, snapshot) + return evm + + +def process_message(message: Message) -> Evm: + """ + Move ether and execute the relevant code. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + evm: :py:class:`~ethereum.forks.monad_next.vm.Evm` + Items containing execution specific objects + + """ + tx_state = message.tx_env.state + if message.depth > STACK_DEPTH_LIMIT: + raise StackDepthLimitError("Stack depth limit reached") + + code = message.code + valid_jump_destinations = get_valid_jump_destinations(code) + + parent_high_watermark = ( + message.parent_evm.memory.high_watermark_bytes + if message.parent_evm is not None + else 0 + ) + + # MIP-8: child frames inherit the parent's page warming and per-page + # growth; the top-level frame seeds its read-warm pages from the + # transaction access list (a warmed slot warms its whole page). + init_read_pages: Set[Tuple[Address, U256]] = set() + init_write_pages: Set[Tuple[Address, U256]] = set() + init_current_growth: Dict[Tuple[Address, U256], int] = {} + init_net_growth: Dict[Tuple[Address, U256], int] = {} + + if message.parent_evm is not None: + init_read_pages = message.parent_evm.read_accessed_pages.copy() + init_write_pages = message.parent_evm.write_accessed_pages.copy() + init_current_growth = dict(message.parent_evm.current_state_growth) + init_net_growth = dict(message.parent_evm.net_state_growth) + else: + for addr, slot_key in message.accessed_storage_keys: + slot_val = U256.from_be_bytes(slot_key) + init_read_pages.add((addr, page_index(slot_val))) + + evm = Evm( + pc=Uint(0), + stack=[], + memory=EvmMemory( + data=bytearray(), high_watermark_bytes=parent_high_watermark + ), + code=code, + gas_left=message.gas, + valid_jump_destinations=valid_jump_destinations, + logs=(), + refund_counter=0, + running=True, + message=message, + output=b"", + accounts_to_delete=set(), + return_data=b"", + error=None, + accessed_addresses=message.accessed_addresses, + accessed_storage_keys=message.accessed_storage_keys, + read_accessed_pages=init_read_pages, + write_accessed_pages=init_write_pages, + current_state_growth=init_current_growth, + net_state_growth=init_net_growth, + ) + + # take snapshot of state before processing the message + snapshot = copy_tx_state(tx_state) + # Monad: at top-level message begin, also stash the snapshot on + # tx_env so the reserve-balance check and dippedIntoReserve + # precompile can read "balance at start of EVM execution" from + # any frame. + if message.depth == 0: + message.tx_env.tx_snapshot = snapshot + + if message.should_transfer_value and message.value != 0: + move_ether( + tx_state, + message.caller, + message.current_target, + message.value, + ) + + try: + if evm.message.code_address in PRE_COMPILED_CONTRACTS: + if not message.disable_precompiles: + evm_trace(evm, PrecompileStart(evm.message.code_address)) + PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) + elif evm.message.code_address in MONAD_PRECOMPILE_ADDRESSES: + # Calling a precompile via delegation and it's a Monad + # precompile => revert. + raise RevertInMonadPrecompile + else: + while evm.running and evm.pc < ulen(evm.code): + try: + op = Ops(evm.code[evm.pc]) + except ValueError as e: + raise InvalidOpcode(evm.code[evm.pc]) from e + + evm_trace(evm, OpStart(op)) + op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) + + except RevertInMonadPrecompile as error: + evm_trace(evm, OpException(error)) + evm.gas_left = Uint(0) + # evm.output preserved — contains the raw error message + evm.error = error + except ExceptionalHalt as error: + evm_trace(evm, OpException(error)) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + except Revert as error: + evm_trace(evm, OpException(error)) + evm.error = error + + if evm.error: + restore_tx_state(tx_state, snapshot) + else: + # FIXME: index_in_block is a proxy for not being a system tx + if message.depth == 0 and message.tx_env.index_in_block is not None: + if is_reserve_balance_violated(evm): + restore_tx_state(tx_state, snapshot) + evm.error = RevertOnReserveBalance() + return evm + return evm diff --git a/src/ethereum/forks/monad_next/vm/memory.py b/src/ethereum/forks/monad_next/vm/memory.py new file mode 100644 index 00000000000..6ebeb20566f --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/memory.py @@ -0,0 +1,87 @@ +""" +Ethereum Virtual Machine (EVM) Memory. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM memory operations. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.byte import right_pad_zero_bytes + +from . import EvmMemory + + +def memory_write( + memory: EvmMemory, start_position: U256, value: Bytes +) -> None: + """ + Writes to memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + value : + Data to write to memory. + + """ + memory.data[start_position : int(start_position) + len(value)] = value + + +def memory_read_bytes( + memory: EvmMemory, start_position: U256, size: U256 +) -> Bytes: + """ + Read bytes from memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Size of the data that needs to be read from `start_position`. + + Returns + ------- + data_bytes : + Data read from memory. + + """ + return Bytes( + memory.data[start_position : Uint(start_position) + Uint(size)] + ) + + +def buffer_read(buffer: Bytes, start_position: U256, size: U256) -> Bytes: + """ + Read bytes from a buffer. Padding with zeros if necessary. + + Parameters + ---------- + buffer : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Size of the data that needs to be read from `start_position`. + + Returns + ------- + data_bytes : + Data read from memory. + + """ + buffer_slice = buffer[start_position : Uint(start_position) + Uint(size)] + return right_pad_zero_bytes(bytes(buffer_slice), size) diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/__init__.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/__init__.py new file mode 100644 index 00000000000..dc6440aa169 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/__init__.py @@ -0,0 +1,69 @@ +""" +Precompiled Contract Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Addresses of precompiled contracts and mappings to their +implementations. +""" + +from ...utils.hexadecimal import hex_to_address + +__all__ = ( + "ECRECOVER_ADDRESS", + "SHA256_ADDRESS", + "RIPEMD160_ADDRESS", + "IDENTITY_ADDRESS", + "MODEXP_ADDRESS", + "ALT_BN128_ADD_ADDRESS", + "ALT_BN128_MUL_ADDRESS", + "ALT_BN128_PAIRING_CHECK_ADDRESS", + "BLAKE2F_ADDRESS", + "POINT_EVALUATION_ADDRESS", + "BLS12_G1_ADD_ADDRESS", + "BLS12_G1_MSM_ADDRESS", + "BLS12_G2_ADD_ADDRESS", + "BLS12_G2_MSM_ADDRESS", + "BLS12_PAIRING_ADDRESS", + "BLS12_MAP_FP_TO_G1_ADDRESS", + "BLS12_MAP_FP2_TO_G2_ADDRESS", + "P256VERIFY_ADDRESS", + "RESERVE_BALANCE_ADDRESS", + "STAKING_ADDRESS", + "MONAD_PRECOMPILE_ADDRESSES", +) + +ECRECOVER_ADDRESS = hex_to_address("0x01") +SHA256_ADDRESS = hex_to_address("0x02") +RIPEMD160_ADDRESS = hex_to_address("0x03") +IDENTITY_ADDRESS = hex_to_address("0x04") +MODEXP_ADDRESS = hex_to_address("0x05") +ALT_BN128_ADD_ADDRESS = hex_to_address("0x06") +ALT_BN128_MUL_ADDRESS = hex_to_address("0x07") +ALT_BN128_PAIRING_CHECK_ADDRESS = hex_to_address("0x08") +BLAKE2F_ADDRESS = hex_to_address("0x09") +POINT_EVALUATION_ADDRESS = hex_to_address("0x0a") +BLS12_G1_ADD_ADDRESS = hex_to_address("0x0b") +BLS12_G1_MSM_ADDRESS = hex_to_address("0x0c") +BLS12_G2_ADD_ADDRESS = hex_to_address("0x0d") +BLS12_G2_MSM_ADDRESS = hex_to_address("0x0e") +BLS12_PAIRING_ADDRESS = hex_to_address("0x0f") +BLS12_MAP_FP_TO_G1_ADDRESS = hex_to_address("0x10") +BLS12_MAP_FP2_TO_G2_ADDRESS = hex_to_address("0x11") +P256VERIFY_ADDRESS = hex_to_address("0x100") +STAKING_ADDRESS = hex_to_address("0x1000") +RESERVE_BALANCE_ADDRESS = hex_to_address("0x1001") + +# Monad-specific precompile addresses: calling these via a delegating EOA +# must revert rather than execute as empty code. +MONAD_PRECOMPILE_ADDRESSES: frozenset = frozenset( + { + STAKING_ADDRESS, + RESERVE_BALANCE_ADDRESS, + } +) diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/alt_bn128.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/alt_bn128.py new file mode 100644 index 00000000000..862506c54c3 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/alt_bn128.py @@ -0,0 +1,234 @@ +""" +Ethereum Virtual Machine (EVM) ALT_BN128 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ALT_BN128 precompiled contracts. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint, ulen +from py_ecc.optimized_bn128.optimized_curve import ( + FQ, + FQ2, + FQ12, + add, + b, + b2, + curve_order, + field_modulus, + is_inf, + is_on_curve, + multiply, + normalize, +) +from py_ecc.optimized_bn128.optimized_pairing import pairing +from py_ecc.typing import Optimized_Point3D as Point3D + +from ...vm import Evm +from ...vm.gas import GasCosts, charge_gas +from ...vm.memory import buffer_read +from ..exceptions import InvalidParameter, OutOfGasError + + +def bytes_to_g1(data: Bytes) -> Point3D[FQ]: + """ + Decode 64 bytes to a point on the curve. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Point3D + A point on the curve. + + Raises + ------ + InvalidParameter + Either a field element is invalid or the point is not on the curve. + + """ + if len(data) != 64: + raise InvalidParameter("Input should be 64 bytes long") + + x_bytes = buffer_read(data, U256(0), U256(32)) + x = int(U256.from_be_bytes(x_bytes)) + y_bytes = buffer_read(data, U256(32), U256(32)) + y = int(U256.from_be_bytes(y_bytes)) + + if x >= field_modulus: + raise InvalidParameter("Invalid field element") + if y >= field_modulus: + raise InvalidParameter("Invalid field element") + + z = 1 + if x == 0 and y == 0: + z = 0 + + point = (FQ(x), FQ(y), FQ(z)) + + # Check if the point is on the curve + if not is_on_curve(point, b): + raise InvalidParameter("Point is not on curve") + + return point + + +def bytes_to_g2(data: Bytes) -> Point3D[FQ2]: + """ + Decode 128 bytes to a G2 point. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Point2D + A point on the curve. + + Raises + ------ + InvalidParameter + Either a field element is invalid or the point is not on the curve. + + """ + if len(data) != 128: + raise InvalidParameter("G2 should be 128 bytes long") + + x0_bytes = buffer_read(data, U256(0), U256(32)) + x0 = int(U256.from_be_bytes(x0_bytes)) + x1_bytes = buffer_read(data, U256(32), U256(32)) + x1 = int(U256.from_be_bytes(x1_bytes)) + + y0_bytes = buffer_read(data, U256(64), U256(32)) + y0 = int(U256.from_be_bytes(y0_bytes)) + y1_bytes = buffer_read(data, U256(96), U256(32)) + y1 = int(U256.from_be_bytes(y1_bytes)) + + if x0 >= field_modulus or x1 >= field_modulus: + raise InvalidParameter("Invalid field element") + if y0 >= field_modulus or y1 >= field_modulus: + raise InvalidParameter("Invalid field element") + + x = FQ2((x1, x0)) + y = FQ2((y1, y0)) + + z = (1, 0) + if x == FQ2((0, 0)) and y == FQ2((0, 0)): + z = (0, 0) + + point = (x, y, FQ2(z)) + + # Check if the point is on the curve + if not is_on_curve(point, b2): + raise InvalidParameter("Point is not on curve") + + return point + + +def alt_bn128_add(evm: Evm) -> None: + """ + The ALT_BN128 addition precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_ECADD) + + # OPERATION + try: + p0 = bytes_to_g1(buffer_read(data, U256(0), U256(64))) + p1 = bytes_to_g1(buffer_read(data, U256(64), U256(64))) + except InvalidParameter as e: + raise OutOfGasError from e + + p = add(p0, p1) + x, y = normalize(p) + + evm.output = Uint(x).to_be_bytes32() + Uint(y).to_be_bytes32() + + +def alt_bn128_mul(evm: Evm) -> None: + """ + The ALT_BN128 multiplication precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_ECMUL) + + # OPERATION + try: + p0 = bytes_to_g1(buffer_read(data, U256(0), U256(64))) + except InvalidParameter as e: + raise OutOfGasError from e + n = int(U256.from_be_bytes(buffer_read(data, U256(64), U256(32)))) + + p = multiply(p0, n) + x, y = normalize(p) + + evm.output = Uint(x).to_be_bytes32() + Uint(y).to_be_bytes32() + + +def alt_bn128_pairing_check(evm: Evm) -> None: + """ + The ALT_BN128 pairing check precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas( + evm, + GasCosts.PRECOMPILE_ECPAIRING_PER_POINT * (ulen(data) // Uint(192)) + + GasCosts.PRECOMPILE_ECPAIRING_BASE, + ) + + # OPERATION + if len(data) % 192 != 0: + raise OutOfGasError + result = FQ12.one() + for i in range(len(data) // 192): + try: + p = bytes_to_g1(buffer_read(data, U256(192 * i), U256(64))) + q = bytes_to_g2(buffer_read(data, U256(192 * i + 64), U256(128))) + except InvalidParameter as e: + raise OutOfGasError from e + if not is_inf(multiply(p, curve_order)): + raise OutOfGasError + if not is_inf(multiply(q, curve_order)): + raise OutOfGasError + + result *= pairing(q, p) + + if result == FQ12.one(): + evm.output = U256(1).to_be_bytes32() + else: + evm.output = U256(0).to_be_bytes32() diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/blake2f.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/blake2f.py new file mode 100644 index 00000000000..ae53b1ab4b5 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/blake2f.py @@ -0,0 +1,42 @@ +""" +Ethereum Virtual Machine (EVM) Blake2 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `Blake2` precompiled contract. +""" + +from ethereum.crypto.blake2 import Blake2b + +from ...vm import Evm +from ...vm.gas import GasCosts, charge_gas +from ..exceptions import InvalidParameter + + +def blake2f(evm: Evm) -> None: + """ + Writes the Blake2 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + if len(data) != 213: + raise InvalidParameter + + blake2b = Blake2b() + rounds, h, m, t_0, t_1, f = blake2b.get_blake2_parameters(data) + + charge_gas(evm, GasCosts.PRECOMPILE_BLAKE2F_PER_ROUND * rounds) + if f not in [0, 1]: + raise InvalidParameter + + evm.output = blake2b.compress(rounds, h, m, t_0, t_1, f) diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/__init__.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/__init__.py new file mode 100644 index 00000000000..7d622da39d1 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/__init__.py @@ -0,0 +1,622 @@ +""" +BLS12 381 Precompile. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Precompile for BLS12-381 curve operations. +""" + +from functools import lru_cache +from typing import Tuple + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint +from py_ecc.optimized_bls12_381.optimized_curve import ( + FQ, + FQ2, + b, + b2, + curve_order, + is_inf, + is_on_curve, + normalize, +) +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) +from py_ecc.typing import Optimized_Point3D as Point3D + +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter + +G1_K_DISCOUNT = [ + 1000, + 949, + 848, + 797, + 764, + 750, + 738, + 728, + 719, + 712, + 705, + 698, + 692, + 687, + 682, + 677, + 673, + 669, + 665, + 661, + 658, + 654, + 651, + 648, + 645, + 642, + 640, + 637, + 635, + 632, + 630, + 627, + 625, + 623, + 621, + 619, + 617, + 615, + 613, + 611, + 609, + 608, + 606, + 604, + 603, + 601, + 599, + 598, + 596, + 595, + 593, + 592, + 591, + 589, + 588, + 586, + 585, + 584, + 582, + 581, + 580, + 579, + 577, + 576, + 575, + 574, + 573, + 572, + 570, + 569, + 568, + 567, + 566, + 565, + 564, + 563, + 562, + 561, + 560, + 559, + 558, + 557, + 556, + 555, + 554, + 553, + 552, + 551, + 550, + 549, + 548, + 547, + 547, + 546, + 545, + 544, + 543, + 542, + 541, + 540, + 540, + 539, + 538, + 537, + 536, + 536, + 535, + 534, + 533, + 532, + 532, + 531, + 530, + 529, + 528, + 528, + 527, + 526, + 525, + 525, + 524, + 523, + 522, + 522, + 521, + 520, + 520, + 519, +] + +G2_K_DISCOUNT = [ + 1000, + 1000, + 923, + 884, + 855, + 832, + 812, + 796, + 782, + 770, + 759, + 749, + 740, + 732, + 724, + 717, + 711, + 704, + 699, + 693, + 688, + 683, + 679, + 674, + 670, + 666, + 663, + 659, + 655, + 652, + 649, + 646, + 643, + 640, + 637, + 634, + 632, + 629, + 627, + 624, + 622, + 620, + 618, + 615, + 613, + 611, + 609, + 607, + 606, + 604, + 602, + 600, + 598, + 597, + 595, + 593, + 592, + 590, + 589, + 587, + 586, + 584, + 583, + 582, + 580, + 579, + 578, + 576, + 575, + 574, + 573, + 571, + 570, + 569, + 568, + 567, + 566, + 565, + 563, + 562, + 561, + 560, + 559, + 558, + 557, + 556, + 555, + 554, + 553, + 552, + 552, + 551, + 550, + 549, + 548, + 547, + 546, + 545, + 545, + 544, + 543, + 542, + 541, + 541, + 540, + 539, + 538, + 537, + 537, + 536, + 535, + 535, + 534, + 533, + 532, + 532, + 531, + 530, + 530, + 529, + 528, + 528, + 527, + 526, + 526, + 525, + 524, + 524, +] + +G1_MAX_DISCOUNT = 519 +G2_MAX_DISCOUNT = 524 +MULTIPLIER = Uint(1000) + + +# Note: Caching as a way to optimize client performance can create a DoS +# attack vector for worst-case inputs that trigger only cache misses. This +# should not be relied upon for client performance optimization in +# production systems. +@lru_cache(maxsize=128) +def _bytes_to_g1_cached( + data: bytes, + subgroup_check: bool = False, +) -> Point3D[FQ]: + """ + Internal cached version of `bytes_to_g1` that works with hashable `bytes`. + """ + if len(data) != 128: + raise InvalidParameter("Input should be 128 bytes long") + + x = bytes_to_fq(data[:64]) + y = bytes_to_fq(data[64:]) + + if x >= FQ.field_modulus: + raise InvalidParameter("x >= field modulus") + if y >= FQ.field_modulus: + raise InvalidParameter("y >= field modulus") + + z = 1 + if x == 0 and y == 0: + z = 0 + point = FQ(x), FQ(y), FQ(z) + + if not is_on_curve(point, b): + raise InvalidParameter("G1 point is not on curve") + + if subgroup_check and not is_inf(bls12_multiply(point, curve_order)): + raise InvalidParameter("Subgroup check failed for G1 point.") + + return point + + +def bytes_to_g1( + data: Bytes, + subgroup_check: bool = False, +) -> Point3D[FQ]: + """ + Decode 128 bytes to a G1 point with or without subgroup check. + + Parameters + ---------- + data : + The bytes data to decode. + subgroup_check : bool + Whether to perform a subgroup check on the G1 point. + + Returns + ------- + point : Point3D[FQ] + The G1 point. + + Raises + ------ + InvalidParameter + If a field element is invalid, the point is not on the curve, or the + subgroup check fails. + + """ + # This is needed bc when we slice `Bytes` we get a `bytearray`, + # which is not hashable + return _bytes_to_g1_cached(bytes(data), subgroup_check) + + +def g1_to_bytes( + g1_point: Point3D[FQ], +) -> Bytes: + """ + Encode a G1 point to 128 bytes. + + Parameters + ---------- + g1_point : + The G1 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + g1_normalized = normalize(g1_point) + x, y = g1_normalized + return int(x).to_bytes(64, "big") + int(y).to_bytes(64, "big") + + +def decode_g1_scalar_pair( + data: Bytes, +) -> Tuple[Point3D[FQ], int]: + """ + Decode 160 bytes to a G1 point and a scalar. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Tuple[Point3D[FQ], int] + The G1 point and the scalar. + + Raises + ------ + InvalidParameter + If the subgroup check failed. + + """ + if len(data) != 160: + raise InvalidParameter("Input should be 160 bytes long") + + point = bytes_to_g1(data[:128], subgroup_check=True) + + m = int.from_bytes(buffer_read(data, U256(128), U256(32)), "big") + + return point, m + + +def bytes_to_fq(data: Bytes) -> FQ: + """ + Decode 64 bytes to a FQ element. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + fq : FQ + The FQ element. + + Raises + ------ + InvalidParameter + If the field element is invalid. + + """ + if len(data) != 64: + raise InvalidParameter("FQ should be 64 bytes long") + + c = int.from_bytes(data[:64], "big") + + if c >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + + return FQ(c) + + +def bytes_to_fq2(data: Bytes) -> FQ2: + """ + Decode 128 bytes to an FQ2 element. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + fq2 : FQ2 + The FQ2 element. + + Raises + ------ + InvalidParameter + If the field element is invalid. + + """ + if len(data) != 128: + raise InvalidParameter("FQ2 input should be 128 bytes long") + c_0 = int.from_bytes(data[:64], "big") + c_1 = int.from_bytes(data[64:], "big") + + if c_0 >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + if c_1 >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + + return FQ2((c_0, c_1)) + + +# Note: Caching as a way to optimize client performance can create a DoS +# attack vector for worst-case inputs that trigger only cache misses. This +# should not be relied upon for client performance optimization in +# production systems. +@lru_cache(maxsize=128) +def _bytes_to_g2_cached( + data: bytes, + subgroup_check: bool = False, +) -> Point3D[FQ2]: + """ + Internal cached version of `bytes_to_g2` that works with hashable `bytes`. + """ + if len(data) != 256: + raise InvalidParameter("G2 should be 256 bytes long") + + x = bytes_to_fq2(data[:128]) + y = bytes_to_fq2(data[128:]) + + z = (1, 0) + if x == FQ2((0, 0)) and y == FQ2((0, 0)): + z = (0, 0) + + point = x, y, FQ2(z) + + if not is_on_curve(point, b2): + raise InvalidParameter("Point is not on curve") + + if subgroup_check and not is_inf(bls12_multiply(point, curve_order)): + raise InvalidParameter("Subgroup check failed for G2 point.") + + return point + + +def bytes_to_g2( + data: Bytes, + subgroup_check: bool = False, +) -> Point3D[FQ2]: + """ + Decode 256 bytes to a G2 point with or without subgroup check. + + Parameters + ---------- + data : + The bytes data to decode. + subgroup_check : bool + Whether to perform a subgroup check on the G2 point. + + Returns + ------- + point : Point3D[FQ2] + The G2 point. + + Raises + ------ + InvalidParameter + If a field element is invalid, the point is not on the curve, or the + subgroup check fails. + + """ + # This is needed bc when we slice `Bytes` we get a `bytearray`, + # which is not hashable + return _bytes_to_g2_cached(data, subgroup_check) + + +def fq2_to_bytes(fq2: FQ2) -> Bytes: + """ + Encode a FQ2 point to 128 bytes. + + Parameters + ---------- + fq2 : + The FQ2 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + coord0, coord1 = fq2.coeffs + return int(coord0).to_bytes(64, "big") + int(coord1).to_bytes(64, "big") + + +def g2_to_bytes( + g2_point: Point3D[FQ2], +) -> Bytes: + """ + Encode a G2 point to 256 bytes. + + Parameters + ---------- + g2_point : + The G2 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + x_coords, y_coords = normalize(g2_point) + return fq2_to_bytes(x_coords) + fq2_to_bytes(y_coords) + + +def decode_g2_scalar_pair( + data: Bytes, +) -> Tuple[Point3D[FQ2], int]: + """ + Decode 288 bytes to a G2 point and a scalar. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Tuple[Point3D[FQ2], int] + The G2 point and the scalar. + + Raises + ------ + InvalidParameter + If the subgroup check failed. + + """ + if len(data) != 288: + raise InvalidParameter("Input should be 288 bytes long") + + point = bytes_to_g2(data[:256], subgroup_check=True) + n = int.from_bytes(data[256 : 256 + 32], "big") + + return point, n diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g1.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g1.py new file mode 100644 index 00000000000..d1f63224a0c --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g1.py @@ -0,0 +1,149 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of pre-compiles in G1 (curve over base prime field). +""" + +from ethereum_types.numeric import U256, Uint +from py_ecc.bls.hash_to_curve import clear_cofactor_G1, map_to_curve_G1 +from py_ecc.optimized_bls12_381.optimized_curve import FQ +from py_ecc.optimized_bls12_381.optimized_curve import add as bls12_add +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) + +from ....vm import Evm +from ....vm.gas import ( + GasCosts, + charge_gas, +) +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import ( + G1_K_DISCOUNT, + G1_MAX_DISCOUNT, + MULTIPLIER, + bytes_to_g1, + decode_g1_scalar_pair, + g1_to_bytes, +) + +LENGTH_PER_PAIR = 160 + + +def bls12_g1_add(evm: Evm) -> None: + """ + The bls12_381 G1 point addition precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 256: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_BLS_G1ADD) + + # OPERATION + p1 = bytes_to_g1(buffer_read(data, U256(0), U256(128))) + p2 = bytes_to_g1(buffer_read(data, U256(128), U256(128))) + + result = bls12_add(p1, p2) + + evm.output = g1_to_bytes(result) + + +def bls12_g1_msm(evm: Evm) -> None: + """ + The bls12_381 G1 multi-scalar multiplication precompile. + Note: This uses the naive approach to multi-scalar multiplication + which is not suitably optimized for production clients. Clients are + required to implement a more efficient algorithm such as the Pippenger + algorithm. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // LENGTH_PER_PAIR + if k <= 128: + discount = Uint(G1_K_DISCOUNT[k - 1]) + else: + discount = Uint(G1_MAX_DISCOUNT) + + gas_cost = Uint(k) * GasCosts.PRECOMPILE_BLS_G1MUL * discount // MULTIPLIER + charge_gas(evm, gas_cost) + + # OPERATION + for i in range(k): + start_index = i * LENGTH_PER_PAIR + end_index = start_index + LENGTH_PER_PAIR + + p, m = decode_g1_scalar_pair(data[start_index:end_index]) + product = bls12_multiply(p, m) + + if i == 0: + result = product + else: + result = bls12_add(result, product) + + evm.output = g1_to_bytes(result) + + +def bls12_map_fp_to_g1(evm: Evm) -> None: + """ + Precompile to map field element to G1. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 64: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_BLS_G1MAP) + + # OPERATION + fp = int.from_bytes(data, "big") + if fp >= FQ.field_modulus: + raise InvalidParameter("coordinate >= field modulus") + + g1_optimized_3d = clear_cofactor_G1(map_to_curve_G1(FQ(fp))) + evm.output = g1_to_bytes(g1_optimized_3d) diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g2.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g2.py new file mode 100644 index 00000000000..2fd32313f89 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g2.py @@ -0,0 +1,151 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 G2 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of pre-compiles in G2 (curve over base prime field). +""" + +from ethereum_types.numeric import U256, Uint +from py_ecc.bls.hash_to_curve import clear_cofactor_G2, map_to_curve_G2 +from py_ecc.optimized_bls12_381.optimized_curve import FQ2 +from py_ecc.optimized_bls12_381.optimized_curve import add as bls12_add +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) + +from ....vm import Evm +from ....vm.gas import ( + GasCosts, + charge_gas, +) +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import ( + G2_K_DISCOUNT, + G2_MAX_DISCOUNT, + MULTIPLIER, + bytes_to_fq2, + bytes_to_g2, + decode_g2_scalar_pair, + g2_to_bytes, +) + +LENGTH_PER_PAIR = 288 + + +def bls12_g2_add(evm: Evm) -> None: + """ + The bls12_381 G2 point addition precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 512: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_BLS_G2ADD) + + # OPERATION + p1 = bytes_to_g2(buffer_read(data, U256(0), U256(256))) + p2 = bytes_to_g2(buffer_read(data, U256(256), U256(256))) + + result = bls12_add(p1, p2) + + evm.output = g2_to_bytes(result) + + +def bls12_g2_msm(evm: Evm) -> None: + """ + The bls12_381 G2 multi-scalar multiplication precompile. + Note: This uses the naive approach to multi-scalar multiplication + which is not suitably optimized for production clients. Clients are + required to implement a more efficient algorithm such as the Pippenger + algorithm. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // LENGTH_PER_PAIR + if k <= 128: + discount = Uint(G2_K_DISCOUNT[k - 1]) + else: + discount = Uint(G2_MAX_DISCOUNT) + + gas_cost = Uint(k) * GasCosts.PRECOMPILE_BLS_G2MUL * discount // MULTIPLIER + charge_gas(evm, gas_cost) + + # OPERATION + for i in range(k): + start_index = i * LENGTH_PER_PAIR + end_index = start_index + LENGTH_PER_PAIR + + p, m = decode_g2_scalar_pair(data[start_index:end_index]) + product = bls12_multiply(p, m) + + if i == 0: + result = product + else: + result = bls12_add(result, product) + + evm.output = g2_to_bytes(result) + + +def bls12_map_fp2_to_g2(evm: Evm) -> None: + """ + Precompile to map field element to G2. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 128: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_BLS_G2MAP) + + # OPERATION + field_element = bytes_to_fq2(data) + assert isinstance(field_element, FQ2) + + fp2 = bytes_to_fq2(data) + g2_3d = clear_cofactor_G2(map_to_curve_G2(fp2)) + + evm.output = g2_to_bytes(g2_3d) diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py new file mode 100644 index 00000000000..c7a62cb49c0 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py @@ -0,0 +1,69 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 PAIRING PRE-COMPILE. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the BLS12 381 pairing pre-compile. +""" + +from ethereum_types.numeric import Uint +from py_ecc.optimized_bls12_381 import FQ12, curve_order, is_inf, pairing +from py_ecc.optimized_bls12_381 import multiply as bls12_multiply + +from ....vm import Evm +from ....vm.gas import charge_gas +from ...exceptions import InvalidParameter +from . import bytes_to_g1, bytes_to_g2 + + +def bls12_pairing(evm: Evm) -> None: + """ + The bls12_381 pairing precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid or if the subgroup check fails. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % 384 != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // 384 + gas_cost = Uint(32600 * k + 37700) + charge_gas(evm, gas_cost) + + # OPERATION + result = FQ12.one() + for i in range(k): + g1_start = Uint(384 * i) + g2_start = Uint(384 * i + 128) + + g1_slice = data[g1_start : g1_start + Uint(128)] + g1_point = bytes_to_g1(bytes(g1_slice)) + if not is_inf(bls12_multiply(g1_point, curve_order)): + raise InvalidParameter("Subgroup check failed for G1 point.") + + g2_slice = data[g2_start : g2_start + Uint(256)] + g2_point = bytes_to_g2(bytes(g2_slice)) + if not is_inf(bls12_multiply(g2_point, curve_order)): + raise InvalidParameter("Subgroup check failed for G2 point.") + + result *= pairing(g2_point, g1_point) + + if result == FQ12.one(): + evm.output = b"\x00" * 31 + b"\x01" + else: + evm.output = b"\x00" * 32 diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/ecrecover.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/ecrecover.py new file mode 100644 index 00000000000..17a0174f6ed --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/ecrecover.py @@ -0,0 +1,64 @@ +""" +Ethereum Virtual Machine (EVM) ECRECOVER PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `ECRECOVER` precompiled contract. +""" + +from ethereum_types.numeric import U256 + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GasCosts, charge_gas +from ...vm.memory import buffer_read + + +def ecrecover(evm: Evm) -> None: + """ + Decrypts the address using elliptic curve DSA recovery mechanism and writes + the address to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_ECRECOVER) + + # OPERATION + message_hash_bytes = buffer_read(data, U256(0), U256(32)) + message_hash = Hash32(message_hash_bytes) + v = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + r = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + s = U256.from_be_bytes(buffer_read(data, U256(96), U256(32))) + + if v != U256(27) and v != U256(28): + return + if U256(0) >= r or r >= SECP256K1N: + return + if U256(0) >= s or s >= SECP256K1N: + return + + try: + public_key = secp256k1_recover(r, s, v - U256(27), message_hash) + except InvalidSignatureError: + # unable to extract public key + return + + address = keccak256(public_key)[12:32] + padded_address = left_pad_zero_bytes(address, 32) + evm.output = padded_address diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/identity.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/identity.py new file mode 100644 index 00000000000..b7631736074 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/identity.py @@ -0,0 +1,46 @@ +""" +Ethereum Virtual Machine (EVM) IDENTITY PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `IDENTITY` precompiled contract. +""" + +from ethereum_types.numeric import Uint, ulen + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import ( + GasCosts, + charge_gas, +) + + +def identity(evm: Evm) -> None: + """ + Writes the message data to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(ulen(data)) // Uint(32) + charge_gas( + evm, + GasCosts.PRECOMPILE_IDENTITY_BASE + + GasCosts.PRECOMPILE_IDENTITY_PER_WORD * word_count, + ) + + # OPERATION + evm.output = data diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/mapping.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/mapping.py new file mode 100644 index 00000000000..9ff150e9c20 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/mapping.py @@ -0,0 +1,81 @@ +""" +Precompiled Contract Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Mapping of precompiled contracts to their implementations. +""" + +from typing import Callable, Dict + +from ethereum.state import Address + +from . import ( + ALT_BN128_ADD_ADDRESS, + ALT_BN128_MUL_ADDRESS, + ALT_BN128_PAIRING_CHECK_ADDRESS, + BLAKE2F_ADDRESS, + BLS12_G1_ADD_ADDRESS, + BLS12_G1_MSM_ADDRESS, + BLS12_G2_ADD_ADDRESS, + BLS12_G2_MSM_ADDRESS, + BLS12_MAP_FP2_TO_G2_ADDRESS, + BLS12_MAP_FP_TO_G1_ADDRESS, + BLS12_PAIRING_ADDRESS, + ECRECOVER_ADDRESS, + IDENTITY_ADDRESS, + MODEXP_ADDRESS, + P256VERIFY_ADDRESS, + POINT_EVALUATION_ADDRESS, + RESERVE_BALANCE_ADDRESS, + RIPEMD160_ADDRESS, + SHA256_ADDRESS, +) +from .alt_bn128 import alt_bn128_add, alt_bn128_mul, alt_bn128_pairing_check +from .blake2f import blake2f +from .bls12_381.bls12_381_g1 import ( + bls12_g1_add, + bls12_g1_msm, + bls12_map_fp_to_g1, +) +from .bls12_381.bls12_381_g2 import ( + bls12_g2_add, + bls12_g2_msm, + bls12_map_fp2_to_g2, +) +from .bls12_381.bls12_381_pairing import bls12_pairing +from .ecrecover import ecrecover +from .identity import identity +from .modexp import modexp +from .p256verify import p256verify +from .point_evaluation import point_evaluation +from .reserve_balance import reserve_balance +from .ripemd160 import ripemd160 +from .sha256 import sha256 + +PRE_COMPILED_CONTRACTS: Dict[Address, Callable] = { + ECRECOVER_ADDRESS: ecrecover, + SHA256_ADDRESS: sha256, + RIPEMD160_ADDRESS: ripemd160, + IDENTITY_ADDRESS: identity, + MODEXP_ADDRESS: modexp, + ALT_BN128_ADD_ADDRESS: alt_bn128_add, + ALT_BN128_MUL_ADDRESS: alt_bn128_mul, + ALT_BN128_PAIRING_CHECK_ADDRESS: alt_bn128_pairing_check, + BLAKE2F_ADDRESS: blake2f, + POINT_EVALUATION_ADDRESS: point_evaluation, + BLS12_G1_ADD_ADDRESS: bls12_g1_add, + BLS12_G1_MSM_ADDRESS: bls12_g1_msm, + BLS12_G2_ADD_ADDRESS: bls12_g2_add, + BLS12_G2_MSM_ADDRESS: bls12_g2_msm, + BLS12_PAIRING_ADDRESS: bls12_pairing, + BLS12_MAP_FP_TO_G1_ADDRESS: bls12_map_fp_to_g1, + BLS12_MAP_FP2_TO_G2_ADDRESS: bls12_map_fp2_to_g2, + P256VERIFY_ADDRESS: p256verify, + RESERVE_BALANCE_ADDRESS: reserve_balance, +} diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/modexp.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/modexp.py new file mode 100644 index 00000000000..bf828ee8f6e --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/modexp.py @@ -0,0 +1,175 @@ +""" +Ethereum Virtual Machine (EVM) MODEXP PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `MODEXP` precompiled contract. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ...vm import Evm +from ...vm.exceptions import ExceptionalHalt +from ...vm.gas import charge_gas +from ..memory import buffer_read + + +def modexp(evm: Evm) -> None: + """ + Calculates `(base**exp) % modulus` for arbitrary sized `base`, `exp` and + `modulus`. The return value is the same length as the modulus. + """ + data = evm.message.data + + # GAS + base_length = U256.from_be_bytes(buffer_read(data, U256(0), U256(32))) + if base_length > U256(1024): + raise ExceptionalHalt("Mod-exp base length is too large") + + exp_length = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + if exp_length > U256(1024): + raise ExceptionalHalt("Mod-exp exponent length is too large") + + modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + if modulus_length > U256(1024): + raise ExceptionalHalt("Mod-exp modulus length is too large") + + exp_start = U256(96) + base_length + + exp_head = U256.from_be_bytes( + buffer_read(data, exp_start, min(U256(32), exp_length)) + ) + + charge_gas( + evm, + gas_cost(base_length, modulus_length, exp_length, exp_head), + ) + + # OPERATION + if base_length == 0 and modulus_length == 0: + evm.output = Bytes() + return + + base = Uint.from_be_bytes(buffer_read(data, U256(96), base_length)) + exp = Uint.from_be_bytes(buffer_read(data, exp_start, exp_length)) + + modulus_start = exp_start + exp_length + modulus = Uint.from_be_bytes( + buffer_read(data, modulus_start, modulus_length) + ) + + if modulus == 0: + evm.output = Bytes(b"\x00") * modulus_length + else: + evm.output = pow(base, exp, modulus).to_bytes( + Uint(modulus_length), "big" + ) + + +def complexity(base_length: U256, modulus_length: U256) -> Uint: + """ + Estimate the complexity of performing a modular exponentiation. + + Parameters + ---------- + base_length : + Length of the array representing the base integer. + + modulus_length : + Length of the array representing the modulus integer. + + Returns + ------- + complexity : `Uint` + Complexity of performing the operation. + + """ + max_length = max(Uint(base_length), Uint(modulus_length)) + words = (max_length + Uint(7)) // Uint(8) + complexity = Uint(16) + if max_length > Uint(32): + complexity = Uint(2) * words ** Uint(2) + return complexity + + +def iterations(exponent_length: U256, exponent_head: U256) -> Uint: + """ + Calculate the number of iterations required to perform a modular + exponentiation. + + Parameters + ---------- + exponent_length : + Length of the array representing the exponent integer. + + exponent_head : + First 32 bytes of the exponent (with leading zero padding if it is + shorter than 32 bytes), as a U256. + + Returns + ------- + iterations : `Uint` + Number of iterations. + + """ + if exponent_length <= U256(32) and exponent_head == U256(0): + count = Uint(0) + elif exponent_length <= U256(32): + bit_length = exponent_head.bit_length() + + if bit_length > Uint(0): + bit_length -= Uint(1) + + count = bit_length + else: + length_part = Uint(16) * (Uint(exponent_length) - Uint(32)) + bits_part = exponent_head.bit_length() + + if bits_part > Uint(0): + bits_part -= Uint(1) + + count = length_part + bits_part + + return max(count, Uint(1)) + + +def gas_cost( + base_length: U256, + modulus_length: U256, + exponent_length: U256, + exponent_head: U256, +) -> Uint: + """ + Calculate the gas cost of performing a modular exponentiation. + + Parameters + ---------- + base_length : + Length of the array representing the base integer. + + modulus_length : + Length of the array representing the modulus integer. + + exponent_length : + Length of the array representing the exponent integer. + + exponent_head : + First 32 bytes of the exponent (with leading zero padding if it is + shorter than 32 bytes), as a U256. + + Returns + ------- + gas_cost : `Uint` + Gas required for performing the operation. + + """ + multiplication_complexity = complexity(base_length, modulus_length) + iteration_count = iterations(exponent_length, exponent_head) + cost = multiplication_complexity * iteration_count + return max(Uint(500), cost) diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/p256verify.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/p256verify.py new file mode 100644 index 00000000000..29c2e91e0f0 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/p256verify.py @@ -0,0 +1,90 @@ +""" +Ethereum Virtual Machine (EVM) P256VERIFY PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `P256VERIFY` precompiled contract. +""" + +from ethereum_types.numeric import U256 + +from ethereum.crypto.elliptic_curve import ( + SECP256R1N, + SECP256R1P, + is_on_curve_secp256r1, + secp256r1_verify, +) +from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import InvalidSignatureError +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GasCosts, charge_gas +from ...vm.memory import buffer_read + + +def p256verify(evm: Evm) -> None: + """ + Verifies a P-256 signature. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_P256VERIFY) + + if len(data) != 160: + return + + # OPERATION + message_hash_bytes = buffer_read(data, U256(0), U256(32)) + message_hash = Hash32(message_hash_bytes) + r = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + s = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + public_key_x = U256.from_be_bytes( + buffer_read(data, U256(96), U256(32)) + ) # qx + public_key_y = U256.from_be_bytes( + buffer_read(data, U256(128), U256(32)) + ) # qy + + # Signature component bounds: + # Both r and s MUST satisfy 0 < r < n and 0 < s < n + if r <= U256(0) or r >= SECP256R1N: + return + if s <= U256(0) or s >= SECP256R1N: + return + + # Public key bounds: + # Both qx and qy MUST satisfy 0 ≤ qx < p and 0 ≤ qy < p + # U256 is unsigned, so we don't need to check for < 0 + if public_key_x >= SECP256R1P: + return + if public_key_y >= SECP256R1P: + return + + # Point should not be at infinity (represented as (0, 0)) + if public_key_x == U256(0) and public_key_y == U256(0): + return + + # Point validity: The point (qx, qy) MUST satisfy the curve equation + # qy^2 ≡ qx^3 + a*qx + b (mod p) + if not is_on_curve_secp256r1(public_key_x, public_key_y): + return + + try: + secp256r1_verify(r, s, public_key_x, public_key_y, message_hash) + except InvalidSignatureError: + return + + evm.output = left_pad_zero_bytes(b"\x01", 32) diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/point_evaluation.py new file mode 100644 index 00000000000..d2d105ba13b --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/point_evaluation.py @@ -0,0 +1,72 @@ +""" +Ethereum Virtual Machine (EVM) POINT EVALUATION PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `POINT EVALUATION` precompiled contract. +""" + +from ethereum_types.bytes import Bytes, Bytes32, Bytes48 +from ethereum_types.numeric import U256 + +from ethereum.crypto.kzg import ( + KZGCommitment, + kzg_commitment_to_versioned_hash, + verify_kzg_proof, +) + +from ...vm import Evm +from ...vm.exceptions import KZGProofError +from ...vm.gas import GasCosts, charge_gas + +FIELD_ELEMENTS_PER_BLOB = 4096 +BLS_MODULUS = 52435875175126190479447740508185965837690552500527637822603658699938581184513 # noqa: E501 +VERSIONED_HASH_VERSION_KZG = b"\x01" + + +def point_evaluation(evm: Evm) -> None: + """ + A pre-compile that verifies a KZG proof which claims that a blob + (represented by a commitment) evaluates to a given value at a given point. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + if len(data) != 192: + raise KZGProofError + + versioned_hash = data[:32] + z = Bytes32(data[32:64]) + y = Bytes32(data[64:96]) + commitment = KZGCommitment(data[96:144]) + proof = Bytes48(data[144:192]) + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_POINT_EVALUATION) + if kzg_commitment_to_versioned_hash(commitment) != versioned_hash: + raise KZGProofError + + # Verify KZG proof with z and y in big endian format + try: + kzg_proof_verification = verify_kzg_proof(commitment, z, y, proof) + except Exception as e: + raise KZGProofError from e + + if not kzg_proof_verification: + raise KZGProofError + + # Return FIELD_ELEMENTS_PER_BLOB and BLS_MODULUS as padded + # 32 byte big endian values + evm.output = Bytes( + U256(FIELD_ELEMENTS_PER_BLOB).to_be_bytes32() + + U256(BLS_MODULUS).to_be_bytes32() + ) diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/reserve_balance.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/reserve_balance.py new file mode 100644 index 00000000000..d02fa81036d --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/reserve_balance.py @@ -0,0 +1,91 @@ +""" +Ethereum Virtual Machine (EVM) RESERVE BALANCE PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the RESERVE BALANCE precompiled contract for MIP-4. +""" + +from ethereum_types.numeric import U256 + +from ...vm import Evm +from ...vm.exceptions import InvalidParameter, RevertInMonadPrecompile +from ...vm.gas import GasCosts, charge_gas + +# Function selector for dippedIntoReserve() +# keccak256("dippedIntoReserve()")[:4].hex() == "3a61584e" +DIPPED_INTO_RESERVE_SELECTOR = bytes.fromhex("3a61584e") + + +def _is_call(evm: Evm) -> bool: + # STATICCALL: is_static is True + # DELEGATECALL: should_transfer_value is False + # CALLCODE: code_address != current_target + if evm.message.is_static: + return False + if not evm.message.should_transfer_value: + return False + if evm.message.code_address != evm.message.current_target: + return False + return True + + +def reserve_balance(evm: Evm) -> None: + """ + Return whether execution is in reserve balance violation. + + The precompile must be invoked via CALL. Invocations via STATICCALL, + DELEGATECALL, or CALLCODE must revert. + + The method is not payable and must revert with the error message + "value is nonzero" when called with a nonzero value. + + Calldata must be exactly the 4-byte function selector (0x3a61584e). + If the selector does not match, the precompile reverts with "method + not supported". If extra calldata is appended beyond the selector, + the precompile reverts with "input is invalid". + + Reverts consume all gas provided to the call frame. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + from ..interpreter import is_reserve_balance_violated + + data = evm.message.data + + # Must be invoked via CALL only (not STATICCALL, DELEGATECALL, CALLCODE) + if not _is_call(evm): + raise InvalidParameter + + # GAS + charge_gas(evm, GasCosts.WARM_ACCESS) + + if len(data) < 4: + evm.output = b"method not supported" + raise RevertInMonadPrecompile + + if data[:4] != DIPPED_INTO_RESERVE_SELECTOR: + evm.output = b"method not supported" + raise RevertInMonadPrecompile + + if evm.message.value != 0: + evm.output = b"value is nonzero" + raise RevertInMonadPrecompile + + if len(data) > 4: + evm.output = b"input is invalid" + raise RevertInMonadPrecompile + + # OPERATION + violation = is_reserve_balance_violated(evm) + # Return bool encoded as uint256 (32 bytes) + evm.output = U256(1 if violation else 0).to_be_bytes32() diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/ripemd160.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/ripemd160.py new file mode 100644 index 00000000000..c82c9bd534d --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/ripemd160.py @@ -0,0 +1,51 @@ +""" +Ethereum Virtual Machine (EVM) RIPEMD160 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `RIPEMD160` precompiled contract. +""" + +import hashlib + +from ethereum_types.numeric import Uint, ulen + +from ethereum.utils.byte import left_pad_zero_bytes +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import ( + GasCosts, + charge_gas, +) + + +def ripemd160(evm: Evm) -> None: + """ + Writes the ripemd160 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(ulen(data)) // Uint(32) + charge_gas( + evm, + GasCosts.PRECOMPILE_RIPEMD160_BASE + + GasCosts.PRECOMPILE_RIPEMD160_PER_WORD * word_count, + ) + + # OPERATION + hash_bytes = hashlib.new("ripemd160", data).digest() + padded_hash = left_pad_zero_bytes(hash_bytes, 32) + evm.output = padded_hash diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/sha256.py b/src/ethereum/forks/monad_next/vm/precompiled_contracts/sha256.py new file mode 100644 index 00000000000..9d467d7e951 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/precompiled_contracts/sha256.py @@ -0,0 +1,48 @@ +""" +Ethereum Virtual Machine (EVM) SHA256 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `SHA256` precompiled contract. +""" + +import hashlib + +from ethereum_types.numeric import Uint, ulen + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import ( + GasCosts, + charge_gas, +) + + +def sha256(evm: Evm) -> None: + """ + Writes the sha256 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(ulen(data)) // Uint(32) + charge_gas( + evm, + GasCosts.PRECOMPILE_SHA256_BASE + + GasCosts.PRECOMPILE_SHA256_PER_WORD * word_count, + ) + + # OPERATION + evm.output = hashlib.sha256(data).digest() diff --git a/src/ethereum/forks/monad_next/vm/runtime.py b/src/ethereum/forks/monad_next/vm/runtime.py new file mode 100644 index 00000000000..0aa5ddd5e20 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/runtime.py @@ -0,0 +1,69 @@ +""" +Ethereum Virtual Machine (EVM) Runtime Operations. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Runtime related operations used while executing EVM code. +""" + +from typing import Set + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint, ulen + +from .instructions import Ops + + +def get_valid_jump_destinations(code: Bytes) -> Set[Uint]: + """ + Analyze the EVM code to obtain the set of valid jump destinations. + + Valid jump destinations are defined as follows: + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + + Note - Jump destinations are 0-indexed. + + Parameters + ---------- + code : + The EVM code which is to be executed. + + Returns + ------- + valid_jump_destinations: `Set[Uint]` + The set of valid jump destinations in the code. + + """ + valid_jump_destinations = set() + pc = Uint(0) + + while pc < ulen(code): + try: + current_opcode = Ops(code[pc]) + except ValueError: + # Skip invalid opcodes, as they don't affect the jumpdest + # analysis. Nevertheless, such invalid opcodes would be caught + # and raised when the interpreter runs. + pc += Uint(1) + continue + + if current_opcode == Ops.JUMPDEST: + valid_jump_destinations.add(pc) + elif Ops.PUSH1.value <= current_opcode.value <= Ops.PUSH32.value: + # If PUSH-N opcodes are encountered, skip the current opcode along + # with the trailing data segment corresponding to the PUSH-N + # opcodes. + push_data_size = current_opcode.value - Ops.PUSH1.value + 1 + pc += Uint(push_data_size) + + pc += Uint(1) + + return valid_jump_destinations diff --git a/src/ethereum/forks/monad_next/vm/stack.py b/src/ethereum/forks/monad_next/vm/stack.py new file mode 100644 index 00000000000..a87b0a47079 --- /dev/null +++ b/src/ethereum/forks/monad_next/vm/stack.py @@ -0,0 +1,58 @@ +""" +Ethereum Virtual Machine (EVM) Stack. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the stack operators for the EVM. +""" + +from typing import List + +from ethereum_types.numeric import U256 + +from .exceptions import StackOverflowError, StackUnderflowError + + +def pop(stack: List[U256]) -> U256: + """ + Pops the top item off of `stack`. + + Parameters + ---------- + stack : + EVM stack. + + Returns + ------- + value : `U256` + The top element on the stack. + + """ + if len(stack) == 0: + raise StackUnderflowError + + return stack.pop() + + +def push(stack: List[U256], value: U256) -> None: + """ + Pushes `value` onto `stack`. + + Parameters + ---------- + stack : + EVM stack. + + value : + Item to be pushed onto `stack`. + + """ + if len(stack) == 1024: + raise StackOverflowError + + return stack.append(value) diff --git a/src/ethereum/paged_storage_trie.py b/src/ethereum/paged_storage_trie.py new file mode 100644 index 00000000000..efa43ac69d9 --- /dev/null +++ b/src/ethereum/paged_storage_trie.py @@ -0,0 +1,156 @@ +""" +Shared primitives for the MIP-8 page-based storage commitment. + +The storage root commits to ``{page_index: page_commit(page)}`` pairs in a +keccak256 MPT, where each page groups 128 storage slots and is committed with +a BLAKE3 "Induced Subtree Merkle Commit" (ISMC). These primitives are imported +by both the execution spec fork and the testing framework so the two compute +identical storage roots. +""" + +from types import SimpleNamespace +from typing import Dict, List, Mapping, MutableMapping + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.blake3 import ( + CHUNK_END, + CHUNK_START, + DERIVE_KEY_MATERIAL, + IV, + blake3_hash, + compress, + words_to_bytes, +) +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.merkle_patricia_trie import ( + bytes_to_nibble_list, + encode_internal_node, + patricialize, +) + +PAGE_SIZE = 4096 +WORDS_PER_PAGE = 128 + + +def page_commit(page: bytes) -> Bytes: + """ + ISMC_Commit per MIP-8. + + Direct port of the pseudocode at MIP-8 § Page Commitment Function. + """ + assert len(page) == PAGE_SIZE + + slot_bitmap = 0 + for i in range(WORDS_PER_PAGE): + if page[i * 32 : (i + 1) * 32] != bytes(32): + slot_bitmap |= 1 << i + + # Zero pages should be omitted at a higher level. + assert slot_bitmap != 0 + + pair_bitmap = 0 + for i in range(64): + if (slot_bitmap >> (2 * i)) & 0b11: + pair_bitmap |= 1 << i + + pair_leaf_domain = b"ultra_merkle_pair_leaf_domain___" + leaf_iv = compress( + IV, pair_leaf_domain + bytes(32), 64, 0, DERIVE_KEY_MATERIAL + )[:8] + + active_nodes: List[SimpleNamespace] = [] + for i in range(64): + if (pair_bitmap >> i) & 1: + pair_data = page[i * 64 : (i + 1) * 64] + leaf_hash = words_to_bytes( + compress(leaf_iv, pair_data, 64, 0, DERIVE_KEY_MATERIAL)[:8] + ) + active_nodes.append(SimpleNamespace(index=i, value=leaf_hash)) + + for level in range(6): + next_level_nodes: List[SimpleNamespace] = [] + i = 0 + while i < len(active_nodes): + current_node = active_nodes[i] + if i + 1 < len(active_nodes): + next_node = active_nodes[i + 1] + if (current_node.index >> (level + 1)) == ( + next_node.index >> (level + 1) + ): + parent_value = words_to_bytes( + compress( + IV, + current_node.value + next_node.value, + 64, + 0, + CHUNK_START | CHUNK_END, + )[:8] + ) + next_level_nodes.append( + SimpleNamespace( + index=current_node.index, value=parent_value + ) + ) + i += 2 + continue + next_level_nodes.append(current_node) + i += 1 + active_nodes = next_level_nodes + if len(active_nodes) == 1: + break + + subtree_root = active_nodes[0].value + seal_payload = slot_bitmap.to_bytes(16, "little") + subtree_root + return Bytes(blake3_hash(seal_payload)) + + +def _prepare_storage_trie( + storage: Mapping[Bytes32, U256], +) -> Mapping[Bytes, Bytes]: + """ + Group slots into pages, compute BLAKE3 page commitments, and return a + keccak256-secured mapping suitable for standard MPT construction. + """ + pages: Dict[U256, bytearray] = {} + + for preimage, value in storage.items(): + slot = U256.from_be_bytes(preimage) + page_idx = slot >> U256(7) + offset = int(slot & U256(0x7F)) + + if page_idx not in pages: + pages[page_idx] = bytearray(PAGE_SIZE) + + value_bytes = value.to_be_bytes32() + start = offset * 32 + pages[page_idx][start : start + 32] = value_bytes + + mapped: MutableMapping[Bytes, Bytes] = {} + for page_idx, page_data in pages.items(): + commitment = page_commit(bytes(page_data)) + key = keccak256(page_idx.to_be_bytes32()) + # Difference (8) — Storage MPT leaf value framing: the leaf holds + # the RLP-string framing (`0xa0 || commitment`) of the commitment. + mapped[bytes_to_nibble_list(key)] = rlp.encode(commitment) + + return mapped + + +def storage_root_paged(storage: Mapping[Bytes32, U256]) -> Hash32: + """ + Compute the storage root over a keccak256 MPT whose leaves are BLAKE3 + page commitments (MIP-8). + + ``storage`` maps each 32-byte slot key to its `U256` value. + """ + obj = _prepare_storage_trie(storage) + + root_node = encode_internal_node(patricialize(obj, Uint(0))) + if len(rlp.encode(root_node)) < 32: + return keccak256(rlp.encode(root_node)) + else: + assert isinstance(root_node, Bytes) + return Hash32(root_node) diff --git a/src/ethereum_spec_tools/evm_tools/utils.py b/src/ethereum_spec_tools/evm_tools/utils.py index 0b0855bae73..a3bc8a9b7e8 100644 --- a/src/ethereum_spec_tools/evm_tools/utils.py +++ b/src/ethereum_spec_tools/evm_tools/utils.py @@ -27,6 +27,9 @@ W = TypeVar("W", Uint, U64, U256) EXCEPTION_MAPS = { + "MONAD_NEXT": { + "fork_blocks": [("monad_next", 0)], + }, "MONAD_NINE": { "fork_blocks": [("monad_nine", 0)], }, diff --git a/tests/berlin/eip2930_access_list/test_acl.py b/tests/berlin/eip2930_access_list/test_acl.py index 7f0f3a82498..8388b4d9ce2 100644 --- a/tests/berlin/eip2930_access_list/test_acl.py +++ b/tests/berlin/eip2930_access_list/test_acl.py @@ -17,6 +17,7 @@ Transaction, TransactionException, ) +from execution_testing.forks import MONAD_NEXT REFERENCE_SPEC_GIT_PATH = "EIPS/eip-2930.md" REFERENCE_SPEC_VERSION = "c9db53a936c5c9cbe2db32ba0d1b86c4c6e73534" @@ -49,7 +50,13 @@ def test_account_storage_warm_cold_state( """Test type 1 transaction.""" env = Environment() - storage_reader_contract = pre.deploy_contract(Op.SLOAD(1) + Op.STOP) + # MIP-8 uses page-level access tracking (128 slots/page). + # Use slot on a different page so warm/cold distinction holds. + sload_slot = 129 if fork >= MONAD_NEXT else 1 + + storage_reader_contract = pre.deploy_contract( + Op.SLOAD(sload_slot) + Op.STOP + ) # Overhead: PUSH args for CALL (popped_stack_items - 1) # + GAS opcode + PUSH for SLOAD overhead_cost = ( @@ -74,7 +81,7 @@ def test_account_storage_warm_cold_state( if account_warm: access_list_address = storage_reader_contract if storage_key_warm: - access_list_storage_key = Hash(1) + access_list_storage_key = Hash(sload_slot) access_lists: List[AccessList] = [ AccessList( diff --git a/tests/monad_ten/__init__.py b/tests/monad_ten/__init__.py new file mode 100644 index 00000000000..8f80d5f36d9 --- /dev/null +++ b/tests/monad_ten/__init__.py @@ -0,0 +1 @@ +"""MONAD_TEN fork tests.""" diff --git a/tests/monad_ten/mip8_pageified_storage/__init__.py b/tests/monad_ten/mip8_pageified_storage/__init__.py new file mode 100644 index 00000000000..61d3ab4a80e --- /dev/null +++ b/tests/monad_ten/mip8_pageified_storage/__init__.py @@ -0,0 +1 @@ +"""Cross-client MIP-8 Tests of Monad EVM's pageified storage model.""" diff --git a/tests/monad_ten/mip8_pageified_storage/helpers.py b/tests/monad_ten/mip8_pageified_storage/helpers.py new file mode 100644 index 00000000000..ebe084d5f01 --- /dev/null +++ b/tests/monad_ten/mip8_pageified_storage/helpers.py @@ -0,0 +1,137 @@ +""" +Helper types, functions and classes for testing MIP-8 pageified storage. +""" + +from dataclasses import dataclass, field + +import pytest +from execution_testing import Op +from execution_testing.forks.helpers import Fork + +# Tuples (v_original, v_current, v_new) covering all zero-ness and +# same-ness combinations of the slot value at tx start (v_original), +# right before the measured SSTORE (v_current), and after (v_new). +# Letters X, Y, Z represent distinct nonzero values. +STATE_TRANSITIONS = [ + pytest.param(0, 0, 0, id="0_0_0"), + pytest.param(0, 0, 1, id="0_0_X"), + pytest.param(0, 1, 0, id="0_X_0"), + pytest.param(0, 1, 1, id="0_X_X"), + pytest.param(0, 1, 2, id="0_X_Y"), + pytest.param(5, 0, 0, id="X_0_0"), + pytest.param(5, 0, 5, id="X_0_X"), + pytest.param(5, 0, 6, id="X_0_Y"), + pytest.param(5, 5, 0, id="X_X_0"), + pytest.param(5, 5, 5, id="X_X_X"), + pytest.param(5, 5, 6, id="X_X_Y"), + pytest.param(5, 6, 0, id="X_Y_0"), + pytest.param(5, 6, 5, id="X_Y_X"), + pytest.param(5, 6, 6, id="X_Y_Y"), + pytest.param(5, 6, 7, id="X_Y_Z"), +] + + +def page_index(slot: int) -> int: + """Return the page index for a given storage slot.""" + return slot >> 7 + + +def fresh_sstore_cold(fork: Fork) -> int: + """Gas for a fresh-slot SSTORE on a cold page.""" + return Op.SSTORE( + key_warm=False, + page_load_warm=False, + page_write_warm=False, + current_value=0, + new_value=1, + current_state_growth=0, + net_state_growth=0, + ).gas_cost(fork) + + +def fresh_sstore_warm(fork: Fork) -> int: + """Gas for a fresh-slot SSTORE on a warm page.""" + return Op.SSTORE( + key_warm=True, + page_load_warm=True, + page_write_warm=True, + current_value=0, + new_value=1, + current_state_growth=0, + net_state_growth=0, + ).gas_cost(fork) + + +def generous_gas(fork: Fork) -> int: + """ + Return gas enough for typical MIP-8 tests. + + Covers a handful of fresh SSTOREs plus measurement chain. + Tests doing full-page sweeps add `full_page_sweep_gas(fork)`. + Tests with child CREATE/CREATE2 use `generous_gas_with_create()`. + """ + return fork.gas_costs().TX_BASE + 8 * fresh_sstore_cold(fork) + + +def generous_gas_with_create(fork: Fork) -> int: + """ + Return gas sized so CREATE/CREATE2 children leave the parent + (1/64 of forwarded gas) enough for a post-call cold SSTORE + plus measurement overhead. + """ + return fork.gas_costs().TX_BASE + 64 * (fresh_sstore_cold(fork) + 5_000) + + +def full_page_sweep_gas(fork: Fork) -> int: + """ + Gas for SSTORE on every slot of a single page. + + First slot pays cold I/O + state growth; remaining 127 pay + only BASE + state growth. + """ + return fresh_sstore_cold(fork) + 127 * fresh_sstore_warm(fork) + + +def generous_gas_with_page_sweep(fork: Fork) -> int: + """ + Generous tx gas budget for tests that perform full-page SSTORE sweeps + with surrounding setup and sub-calls. + """ + return fork.gas_costs().TX_BASE + 600 * fresh_sstore_cold(fork) + + +@dataclass +class TxPageState: + """Simulated single-page state under the MIP-8 SSTORE algorithm.""" + + slots: dict[int, int] = field(default_factory=dict) + current_growth: int = 0 + peak_growth: int = 0 + read_warm: bool = False + write_warm: bool = False + + +def simulate_sstore( + page: TxPageState, slot: int, new_value: int, fork: Fork +) -> int: + """Apply SSTORE(slot, new_value) to `page`; return its gas cost.""" + old_value = page.slots.get(slot, 0) + cost = Op.SSTORE( + page_load_warm=page.read_warm, + page_write_warm=page.write_warm, + current_value=old_value, + new_value=new_value, + current_state_growth=page.current_growth, + net_state_growth=page.peak_growth, + ).gas_cost(fork) + page.read_warm = True + if old_value != new_value and not page.write_warm: + page.write_warm = True + if old_value == 0 and new_value != 0: + page.current_growth += 1 + elif old_value != 0 and new_value == 0: + page.current_growth -= 1 + if page.current_growth > page.peak_growth: + page.peak_growth = page.current_growth + page.slots[slot] = new_value + return cost diff --git a/tests/monad_ten/mip8_pageified_storage/spec.py b/tests/monad_ten/mip8_pageified_storage/spec.py new file mode 100644 index 00000000000..97e290a1770 --- /dev/null +++ b/tests/monad_ten/mip8_pageified_storage/spec.py @@ -0,0 +1,32 @@ +"""Defines MIP-8 pageified storage specification constants.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Defines the reference spec version and git path.""" + + git_path: str + version: str + + +ref_spec_8 = ReferenceSpec( + "MIPS/MIP-8.md", "90af59b5e09538c5fb55b48a656a20e73fdb9373" +) + + +@dataclass(frozen=True) +class Spec: + """ + Parameters from the pageified storage specification as defined + at MIP-8. + """ + + SLOTS_PER_PAGE = 128 + PAGE_SIZE_BYTES = 4096 + + GAS_PAGE_BASE_COST = 100 + GAS_PAGE_LOAD_COST = 8_000 + GAS_PAGE_WRITE_COST = 2_800 + GAS_PAGE_STATE_GROWTH_COST = 17_000 diff --git a/tests/monad_ten/mip8_pageified_storage/test_cross_call.py b/tests/monad_ten/mip8_pageified_storage/test_cross_call.py new file mode 100644 index 00000000000..64d0a39318a --- /dev/null +++ b/tests/monad_ten/mip8_pageified_storage/test_cross_call.py @@ -0,0 +1,1684 @@ +""" +Tests cross-call page warming propagation under MIP-8. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Bytecode, + CodeGasMeasure, + Conditional, + Initcode, + Op, + StateTestFiller, + Transaction, + While, +) +from execution_testing.base_types.conversions import NumberConvertible +from execution_testing.forks.helpers import Fork +from execution_testing.test_types.helpers import compute_create_address + +from .helpers import ( + TxPageState, + generous_gas, + generous_gas_with_create, + generous_gas_with_page_sweep, + simulate_sstore, +) +from .spec import Spec, ref_spec_8 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8.git_path +REFERENCE_SPEC_VERSION = ref_spec_8.version + +slot_result = 0x100 +slot_gas_measured = 0x101 +slot_gas_measured_2 = 0x102 +slot_caller = 0x103 +value_code_worked = 0x1234 + +pytestmark = [ + pytest.mark.valid_from("MONAD_NEXT"), +] + + +@pytest.mark.with_all_call_opcodes( + selector=lambda call_opcode: call_opcode != Op.STATICCALL, +) +def test_call_propagates_warm_to_child( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + call_opcode: Op, +) -> None: + """ + Self-call propagates page warming to the child frame. + + Parent warms page 0 of parent_address via SLOAD(0), then + self-calls (Op.ADDRESS) with 1 byte of calldata to branch + into the measure leg. The child SLOAD(1) targets the same + (parent_addr, page 0) which is already warm. + + STATICCALL is covered separately by + `test_staticcall_propagates_warm_to_child` since it cannot + SSTORE inside the child branch to record the measurement. + """ + overhead = Op.PUSH1(0).gas_cost(fork) + measure_code = CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + parent_code = Conditional( + condition=Op.CALLDATASIZE, + if_true=measure_code, + if_false=( + Op.SLOAD(0) + + Op.SSTORE( + slot_result, + call_opcode( + address=Op.ADDRESS, + args_offset=0, + args_size=1, + ), + ) + ), + ) + parent_address = pre.deploy_contract(parent_code) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + expected_gas = Op.SLOAD(page_load_warm=True).gas_cost(fork) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={ + slot_result: 1, + slot_gas_measured: expected_gas, + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "success_mode,is_selfdestruct", + [ + pytest.param(Op.STOP, False, id="stop"), + pytest.param(Op.RETURN(0, 0), False, id="return"), + pytest.param(Op.SELFDESTRUCT(0xBEEF), True, id="selfdestruct"), + ], +) +@pytest.mark.with_all_call_opcodes +def test_call_child_warming_propagates_to_parent( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + call_opcode: Op, + success_mode: Op, + is_selfdestruct: bool, +) -> None: + """ + Self-call propagates page warming back to parent on success. + + Child branch warms page 1 via SLOAD(128) and exits with + `success_mode`. Parent SLOAD(129) hits warm regardless of + call opcode: self-call keeps the page key address equal to + parent_address for every opcode (DELEGATECALL/CALLCODE + inherit, CALL/STATICCALL target parent_address explicitly). + + STATICCALL + SELFDESTRUCT is a static-context violation that + aborts the child like a revert — warming lost, call returns 0. + """ + aborts = is_selfdestruct and call_opcode == Op.STATICCALL + + overhead = Op.PUSH2(0).gas_cost(fork) + parent_code = Conditional( + condition=Op.CALLDATASIZE, + if_true=Op.SLOAD(128) + success_mode, + if_false=( + Op.SSTORE( + slot_result, + call_opcode( + address=Op.ADDRESS, + args_offset=0, + args_size=1, + # Cap child gas for when STATICCALL+SELFDESTRUCT + # consumes all child gas on the static error. + gas=generous_gas(fork), + ), + ) + + CodeGasMeasure( + code=Op.SLOAD(129), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ), + ) + parent_address = pre.deploy_contract(parent_code) + + tx = Transaction( + gas_limit=2 * generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + expected_gas = Op.SLOAD(page_load_warm=not aborts).gas_cost(fork) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={ + slot_result: 0 if aborts else 1, + slot_gas_measured: expected_gas, + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "revert_cause", + [ + Op.REVERT(0, 0), + Op.INVALID, + pytest.param(Op.MLOAD(8 * 1024 * 1024), id="oom"), + pytest.param(While(body=Bytecode()), id="oog"), + ], +) +@pytest.mark.with_all_call_opcodes +def test_call_child_warming_lost_on_revert( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + call_opcode: Op, + revert_cause: Op, +) -> None: + """ + Child frame warming is dropped on revert/halt. + + Child branch warms page 1 via SLOAD(128), then reverts. + Parent SLOAD(129) on page 1 is cold for all call opcodes. + """ + overhead = Op.PUSH2(0).gas_cost(fork) + parent_code = Conditional( + condition=Op.CALLDATASIZE, + if_true=Op.SLOAD(128) + revert_cause, + if_false=( + Op.SSTORE( + slot_result, + call_opcode( + address=Op.ADDRESS, + args_offset=0, + args_size=1, + # Cap child gas so parent retains enough + # for CodeGasMeasure after child OOGs. + gas=generous_gas(fork), + ), + ) + + CodeGasMeasure( + code=Op.SLOAD(129), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ), + ) + parent_address = pre.deploy_contract(parent_code) + + tx = Transaction( + gas_limit=2 * generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={ + slot_result: 0, + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +def test_delegatecall_self_propagates_child_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + DELEGATECALL to self warms pages and they propagate back to + the caller frame. + """ + overhead = Op.PUSH2(0).gas_cost(fork) + + inner_code = Op.SLOAD(128) + Op.STOP + outer_code = Op.DELEGATECALL( + gas=Op.GAS, + address=Op.ADDRESS, + args_offset=0, + args_size=1, + ret_offset=0, + ret_size=0, + ) + CodeGasMeasure( + code=Op.SLOAD(129), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + + contract_address = pre.deploy_contract( + Conditional( + condition=Op.CALLDATASIZE, + if_true=inner_code, + if_false=outer_code, + ) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + contract_address: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=True).gas_cost( + fork + ) + }, + ), + }, + tx=tx, + ) + + +def test_page_warming_persists_across_subcalls( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Page warming map is tx-scoped: warming from sub-call N + persists into sub-call N+1. + """ + child_address = pre.deploy_contract( + Conditional( + condition=Op.CALLDATASIZE, + if_true=CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ), + if_false=Op.SLOAD(0), + ) + ) + parent_address = pre.deploy_contract( + Op.CALL(address=child_address) + + Op.CALL(address=child_address, args_offset=0, args_size=1) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + child_address: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=True).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +def test_staticcall_propagates_warm_to_child( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Self-STATICCALL propagates page warming; STATICCALL gas + measured at the parent (child can't SSTORE). + + Parent warms page 0 via SLOAD(0), self-STATICCALLs with 1 + byte calldata. Child SLOAD(1) hits warm (parent_addr, 0). + Total measured = STATICCALL warm-account cost + the child + execution path (Conditional preamble + warm SLOAD + STOP). + """ + # Overhead absorbs all non-measured opcodes: STATICCALL's + # 4 PUSH1 stack args + ADDRESS + GAS, plus the child path + # (Conditional preamble + JUMPDEST + SLOAD key PUSH + STOP). + overhead = ( + Op.PUSH1(0) * 4 + + Op.ADDRESS + + Op.GAS + + Op.CALLDATASIZE + + Op.PUSH1(0) + + Op.PC + + Op.ADD + + Op.JUMPI + + Op.JUMPDEST + + Op.PUSH1(0) + + Op.STOP + ).gas_cost(fork) + + parent_code = Conditional( + condition=Op.CALLDATASIZE, + if_true=Op.SLOAD(1) + Op.STOP, + if_false=( + Op.SLOAD(0) + + CodeGasMeasure( + code=Op.STATICCALL( + address=Op.ADDRESS, + args_offset=0, + args_size=1, + ), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ), + ) + parent_address = pre.deploy_contract(parent_code) + + expected_gas = Op.STATICCALL(address_warm=True).gas_cost(fork) + Op.SLOAD( + page_load_warm=True + ).gas_cost(fork) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + state_test( + pre=pre, + post={ + parent_address: Account( + storage={slot_gas_measured: expected_gas}, + ), + }, + tx=tx, + ) + + +def test_staticcall_failed_sstore_no_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + STATICCALL into a child that attempts SSTORE aborts the child + (static-context violation). Any page warming the failed SSTORE + would have created is lost together with the aborted frame. + """ + child_address = pre.deploy_contract( + Conditional( + condition=Op.CALLDATASIZE, + if_true=CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ), + if_false=Op.SSTORE(0, 1), + ) + ) + + parent_address = pre.deploy_contract( + Op.STATICCALL(address=child_address, gas=100_000) + + Op.CALL(address=child_address, args_offset=0, args_size=1) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + child_address: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.with_all_create_opcodes +def test_create_does_not_propagate_warm_to_child( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, +) -> None: + """ + Created contract does not inherit parent's warm pages. + + Parent warms page 0 via SLOAD(0), then CREATE/CREATE2. + Initcode does SLOAD(1) — cold page. + """ + initcode = Initcode( + deploy_code=Op.STOP, + initcode_prefix=CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + stop=False, + ), + ) + + parent_code = ( + Op.SLOAD(0) + + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + create_opcode(size=Op.CALLDATASIZE) + + Op.SSTORE(slot_result, value_code_worked) + ) + parent_address = pre.deploy_contract(parent_code) + + new_contract_address = compute_create_address( + address=parent_address, + nonce=1, + initcode=initcode, + opcode=create_opcode, + ) + + tx = Transaction( + gas_limit=generous_gas_with_create(fork), + to=parent_address, + sender=pre.fund_eoa(), + data=bytes(initcode), + ) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={slot_result: value_code_worked}, + ), + new_contract_address: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "initcode_body", + [ + # SLOAD warms (new, page 1) then halts via STOP / RETURN. + pytest.param( + Op.SLOAD(128), + id="sload", + ), + pytest.param( + Op.SLOAD(128) + Op.RETURN(0, 0), + id="sload_then_return", + ), + # RETURN deploying minimal runtime code. + pytest.param( + Op.SLOAD(128) + Op.RETURN(0, 1), + id="sload_then_return_nonempty", + ), + # Multi-page + pytest.param( + Op.SSTORE(0, 1) + Op.SSTORE(128, 1), + id="multipage_sstore", + ), + # page-load-warm but page-write-cold path. + pytest.param( + Op.SLOAD(0) + Op.SSTORE(0, 1), + id="sload_before_sstore", + ), + # SELFDESTRUCT as initcode exit (EIP-6780 same-tx destroy). + pytest.param( + Op.SLOAD(128) + Op.SELFDESTRUCT(0xBEEF), + id="sload_then_selfdestruct", + ), + ], +) +@pytest.mark.with_all_create_opcodes +def test_create_child_warming_does_not_propagate_to_parent( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + initcode_body: Op, +) -> None: + """ + Pages warmed inside initcode stay cold in parent. + """ + initcode_bytes = bytes(initcode_body) + padded = initcode_bytes + b"\x00" * ((-len(initcode_bytes)) % 32) + assert len(padded) == 32, "initcode must fit one PUSH32" + + parent_code = ( + Op.MSTORE(0, Op.PUSH32(padded[:32])) + + create_opcode(size=len(initcode_bytes)) + + Op.SSTORE(slot_result, value_code_worked) + + CodeGasMeasure( + code=Op.SLOAD(129), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + parent_address = pre.deploy_contract(parent_code) + + tx = Transaction( + gas_limit=generous_gas_with_create(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={ + slot_result: value_code_worked, + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "revert_cause", + [ + Op.REVERT(0, 0), + Op.INVALID, + pytest.param(Op.MLOAD(8 * 1024 * 1024), id="oom"), + pytest.param(While(body=Bytecode()), id="oog"), + ], +) +@pytest.mark.with_all_create_opcodes +def test_create_child_warming_lost_on_revert( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + revert_cause: Op, +) -> None: + """ + Failed CREATE does not propagate warm pages. + """ + overhead = Op.PUSH2(0).gas_cost(fork) + measure_contract = pre.deploy_contract( + CodeGasMeasure( + code=Op.SLOAD(129), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + stop=False, + ) + # Sanity check to ensure it's not the initcode frame + # storing the measured cold gas. + + Op.SSTORE(slot_caller, Op.CALLER) + ) + + initcode_code = ( + Op.SLOAD(128) + Op.CALL(address=measure_contract) + revert_cause + ) + + parent_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + create_opcode(size=Op.CALLDATASIZE) + + Op.CALL(address=measure_contract) + + Op.SSTORE(slot_result, value_code_worked) + ) + parent_address = pre.deploy_contract(parent_code) + + tx = Transaction( + gas_limit=fork.transaction_gas_limit_cap(), + to=parent_address, + sender=pre.fund_eoa(), + data=bytes(initcode_code), + ) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={slot_result: value_code_worked}, + ), + measure_contract: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + # Sanity check to ensure it's not the initcode frame + # storing the measured cold gas. + slot_caller: parent_address, + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "revert_cause", + [ + Op.REVERT(0, 0), + Op.INVALID, + pytest.param(Op.MLOAD(8 * 1024 * 1024), id="oom"), + pytest.param(While(body=Bytecode()), id="oog"), + ], +) +@pytest.mark.with_all_create_opcodes +def test_create_child_state_growth_lost_on_revert( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + revert_cause: Op, +) -> None: + """ + Failed CREATE rolls back state-growth counters. + """ + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + measure_contract = pre.deploy_contract( + CodeGasMeasure( + code=Op.SSTORE(0, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + stop=False, + ) + # Sanity check to ensure it's not the initcode frame + # storing the measured fresh-growth SSTORE cost. + + Op.SSTORE(slot_caller, Op.CALLER) + ) + + initcode_code = Op.CALL(address=measure_contract) + revert_cause + + parent_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + create_opcode(size=Op.CALLDATASIZE) + + Op.CALL(address=measure_contract) + + Op.SSTORE(slot_result, value_code_worked) + ) + parent_address = pre.deploy_contract(parent_code) + + tx = Transaction( + gas_limit=fork.transaction_gas_limit_cap(), + to=parent_address, + sender=pre.fund_eoa(), + data=bytes(initcode_code), + ) + + expected_gas = simulate_sstore(TxPageState(), 0, 1, fork) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={slot_result: value_code_worked}, + ), + measure_contract: Account( + storage={ + 0: 1, + slot_gas_measured: expected_gas, + # Sanity check to ensure it's not the initcode frame + # storing the measured fresh-growth SSTORE cost. + slot_caller: parent_address, + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.with_all_create_opcodes +def test_initcode_warming_persists_to_post_deploy( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, +) -> None: + """ + Initcode SSTORE warms (new_addr, page 0). Within the same tx, + the factory CALLs the newly deployed contract — the page + warming persists, so the runtime's SLOAD(1) on the same page + is WARM. + """ + runtime = CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + initcode = Initcode( + deploy_code=runtime, + initcode_prefix=Op.SSTORE(0, value_code_worked), + ) + + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.CALL(address=create_opcode(size=Op.CALLDATASIZE)) + + Op.SSTORE(slot_result, value_code_worked) + ) + factory_address = pre.deploy_contract(factory_code) + + new_contract_address = compute_create_address( + address=factory_address, + nonce=1, + initcode=initcode, + opcode=create_opcode, + ) + + tx = Transaction( + gas_limit=generous_gas_with_create(fork), + to=factory_address, + sender=pre.fund_eoa(), + data=bytes(initcode), + ) + + expected_gas = Op.SLOAD(page_load_warm=True).gas_cost(fork) + + state_test( + pre=pre, + post={ + factory_address: Account( + storage={slot_result: value_code_worked}, + ), + new_contract_address: Account( + storage={ + 0: value_code_worked, + slot_gas_measured: expected_gas, + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.with_all_create_opcodes +def test_initcode_state_growth_persists_to_post_deploy( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, +) -> None: + """ + State-growth counter set by initcode SSTORE survives into the + post-deploy runtime call: a runtime SSTORE on a fresh slot of + the same page pays STATE_GROWTH_COST since current exceeds the + peak inherited from initcode. + """ + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + runtime = CodeGasMeasure( + code=Op.SSTORE(1, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + ) + initcode = Initcode( + deploy_code=runtime, + initcode_prefix=Op.SSTORE(0, 1), + ) + + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.CALL(address=create_opcode(size=Op.CALLDATASIZE)) + + Op.SSTORE(slot_result, value_code_worked) + ) + factory_address = pre.deploy_contract(factory_code) + + new_contract_address = compute_create_address( + address=factory_address, + nonce=1, + initcode=initcode, + opcode=create_opcode, + ) + + tx = Transaction( + gas_limit=generous_gas_with_create(fork), + to=factory_address, + sender=pre.fund_eoa(), + data=bytes(initcode), + ) + + page = TxPageState() + simulate_sstore(page, 0, 1, fork) + expected_gas = simulate_sstore(page, 1, 1, fork) + + state_test( + pre=pre, + post={ + factory_address: Account( + storage={slot_result: value_code_worked}, + ), + new_contract_address: Account( + storage={ + 0: 1, + 1: 1, + slot_gas_measured: expected_gas, + }, + ), + }, + tx=tx, + ) + + +def test_creation_tx_initcode_sload_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + SLOAD inside a top-level creation-tx initcode follows + cold/warm rules on the new contract's pages. + """ + overhead = Op.PUSH1(0).gas_cost(fork) + initcode = Initcode( + deploy_code=Op.STOP, + initcode_prefix=( + CodeGasMeasure( + code=Op.SLOAD(0), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + stop=False, + ) + + CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured_2, + stop=False, + ) + ), + ) + sender = pre.fund_eoa() + new_contract_address = compute_create_address( + address=sender, nonce=sender.nonce, opcode=Op.CREATE + ) + + tx = Transaction( + gas_limit=generous_gas_with_create(fork), + to=None, + data=bytes(initcode), + sender=sender, + ) + + state_test( + pre=pre, + post={ + new_contract_address: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + slot_gas_measured_2: Op.SLOAD( + page_load_warm=True + ).gas_cost(fork), + }, + ), + }, + tx=tx, + ) + + +def test_creation_tx_initcode_sstore_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + SSTORE inside a top-level creation-tx initcode follows + cold/warm + state-growth rules on the new contract's pages. + """ + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + initcode = Initcode( + deploy_code=Op.STOP, + initcode_prefix=( + CodeGasMeasure( + code=Op.SSTORE(0, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + stop=False, + ) + + CodeGasMeasure( + code=Op.SSTORE(1, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured_2, + stop=False, + ) + ), + ) + sender = pre.fund_eoa() + new_contract_address = compute_create_address( + address=sender, nonce=sender.nonce, opcode=Op.CREATE + ) + + page = TxPageState() + expected_first_gas = simulate_sstore(page, 0, 1, fork) + expected_second_gas = simulate_sstore(page, 1, 1, fork) + + tx = Transaction( + gas_limit=generous_gas_with_create(fork), + to=None, + data=bytes(initcode), + sender=sender, + ) + + state_test( + pre=pre, + post={ + new_contract_address: Account( + storage={ + 0: 1, + 1: 1, + slot_gas_measured: expected_first_gas, + slot_gas_measured_2: expected_second_gas, + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "call_kind", + ["call", "callcode", "delegatecall_chain", "call_chain"], +) +def test_cross_account_page_propagation( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + call_kind: str, +) -> None: + """ + Page warming is keyed by (storage-context-address, page). + """ + inner = pre.deploy_contract(Op.SSTORE(0, value_code_worked)) + expected_warm: bool + + if call_kind == "call": + outer_call = Op.CALL(address=inner) + expected_warm = False + elif call_kind == "callcode": + outer_call = Op.CALLCODE(address=inner) + expected_warm = True + elif call_kind == "delegatecall_chain": + mid = pre.deploy_contract(Op.DELEGATECALL(address=inner) + Op.STOP) + outer_call = Op.DELEGATECALL(address=mid) + expected_warm = True + else: # call_chain + mid = pre.deploy_contract(Op.CALL(address=inner) + Op.STOP) + outer_call = Op.CALL(address=mid) + expected_warm = False + + overhead = Op.PUSH1(0).gas_cost(fork) + outer = pre.deploy_contract( + outer_call + + CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=outer, + sender=pre.fund_eoa(), + ) + + expected_storage: dict[int, int] = { + slot_gas_measured: Op.SLOAD(page_load_warm=expected_warm).gas_cost( + fork + ), + } + if expected_warm: + # CALLCODE/DELEGATECALL: inner's SSTORE wrote to outer's + # storage context. + expected_storage[0] = value_code_worked + + state_test( + pre=pre, + post={outer: Account(storage=expected_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "call_kind", + ["callcode", "delegatecall"], +) +def test_cross_account_caller_warm_propagates( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + call_kind: str, +) -> None: + """CALLCODE/DELEGATECALL preserve caller's storage-context pages.""" + overhead = Op.PUSH1(0).gas_cost(fork) + inner = pre.deploy_contract( + CodeGasMeasure( + code=Op.SLOAD(0), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + + sub_call = ( + Op.CALLCODE(address=inner) + if call_kind == "callcode" + else Op.DELEGATECALL(address=inner) + ) + outer = pre.deploy_contract(Op.SLOAD(0) + sub_call) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=outer, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + outer: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=True).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +def test_delegated_eoa_owns_pages( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Pages warmed inside delegated-EOA code are keyed by the EOA + address. + """ + overhead = Op.PUSH1(0).gas_cost(fork) + delegate_code = Conditional( + condition=Op.CALLDATASIZE, + if_true=CodeGasMeasure( + code=Op.SLOAD(0), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured_2, + ), + if_false=CodeGasMeasure( + code=Op.SLOAD(0), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ), + ) + delegate_target = pre.deploy_contract(delegate_code) + eoa1 = pre.fund_eoa(delegation=delegate_target) + eoa2 = pre.fund_eoa(delegation=delegate_target) + + runner_code = ( + Op.CALL(address=delegate_target) + + Op.CALL(address=eoa1) + + Op.CALL(address=eoa2) + + Op.CALL(address=eoa2, args_size=1) + ) + runner_address = pre.deploy_contract(runner_code) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=runner_address, + sender=pre.fund_eoa(), + ) + + cold = Op.SLOAD(page_load_warm=False).gas_cost(fork) + warm = Op.SLOAD(page_load_warm=True).gas_cost(fork) + + state_test( + pre=pre, + post={ + delegate_target: Account(storage={slot_gas_measured: cold}), + eoa1: Account(storage={slot_gas_measured: cold}), + eoa2: Account( + storage={ + slot_gas_measured: cold, + slot_gas_measured_2: warm, + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "revert_cause", + [ + Op.REVERT(0, 0), + Op.INVALID, + pytest.param(Op.MLOAD(8 * 1024 * 1024), id="oom"), + pytest.param(While(body=Bytecode()), id="oog"), + ], +) +@pytest.mark.with_all_call_opcodes +def test_parent_warming_survives_subcall_revert( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + call_opcode: Op, + revert_cause: Op, +) -> None: + """ + Pages warmed by the parent itself survive a sub-call revert; + pages warmed only inside the reverted sub-call do not. + """ + child_address = pre.deploy_contract(Op.SLOAD(384) + revert_cause) + + overhead = Op.PUSH2(0).gas_cost(fork) + parent_address = pre.deploy_contract( + Op.SLOAD(0) + + call_opcode(address=child_address, gas=100_000) + + CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + stop=False, + ) + + CodeGasMeasure( + code=Op.SLOAD(385), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured_2, + ) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=True).gas_cost( + fork + ), + slot_gas_measured_2: Op.SLOAD( + page_load_warm=False + ).gas_cost(fork), + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize("op", [Op.SSTORE, Op.SLOAD]) +def test_call_value_stipend_storage_oog( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + op: Op, +) -> None: + """ + A value-bearing CALL grants the child a 2300-gas stipend. + Both cold-page SSTORE (~10900) and cold-page SLOAD (~8100) + exceed the stipend; the child OOGs. + """ + if op == Op.SSTORE: + child_code = Op.SSTORE(0, 1) + else: + child_code = Op.SLOAD(0) + + child_address = pre.deploy_contract(child_code) + + parent_address = pre.deploy_contract( + Op.SSTORE( + slot_result, + Op.CALL(gas=0, value=1, address=child_address), + ), + balance=10**16, + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + parent_address: Account(storage={slot_result: 0}), + # Child storage unchanged — SSTORE never committed. + child_address: Account(storage={}), + }, + tx=tx, + ) + + +def test_call_insufficient_gas_no_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Tiny forwarded gas OOGs the child immediately; parent's pages + that were never touched stay cold. + """ + child_address = pre.deploy_contract(Op.SLOAD(0)) + + overhead = Op.PUSH2(0).gas_cost(fork) + parent_address = pre.deploy_contract( + Op.SLOAD(0) + + Op.CALL(address=child_address, gas=10) + + CodeGasMeasure( + code=Op.SLOAD(128), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + parent_address: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +def test_selfdestruct_preserves_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + EIP-6780's not-same-tx SELFDESTRUCT leaves pages warmed. + """ + beneficiary = pre.fund_eoa(amount=10**16) + + child_address = pre.deploy_contract( + Conditional( + condition=Op.CALLDATASIZE, + if_true=CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ), + if_false=(Op.SLOAD(0) + Op.SELFDESTRUCT(beneficiary)), + ), + storage={0: 99, 1: 88}, + ) + + parent_address = pre.deploy_contract( + Op.CALL(address=child_address) + + Op.CALL(address=child_address, args_offset=0, args_size=1) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=parent_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + child_address: Account( + storage={ + 0: 99, + 1: 88, + slot_gas_measured: Op.SLOAD(page_load_warm=True).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize("call_op", [Op.DELEGATECALL, Op.CALL, Op.CREATE]) +@pytest.mark.parametrize("prestate_clear_child", [0, 1, 32]) +@pytest.mark.parametrize("prestate_clear_parent", [0, 1, 32]) +@pytest.mark.parametrize("state_clear_child", [0, 1, 32]) +@pytest.mark.parametrize("state_growth_child", [0, 1, 32]) +@pytest.mark.parametrize("state_clear_parent", [0, 1, 32]) +@pytest.mark.parametrize("state_growth_parent", [0, 1, 32]) +def test_state_growth_counters_inside_subcall( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + state_growth_parent: int, + state_clear_parent: int, + state_growth_child: int, + state_clear_child: int, + prestate_clear_parent: int, + prestate_clear_child: int, + call_op: Op, +) -> None: + """ + Test state costs in child after a DELEGATECALL or plain-CALL. + """ + parent_prestate_slots = list(range(prestate_clear_parent)) + a = prestate_clear_parent + child_prestate_slots = list(range(a, a + prestate_clear_child)) + b = a + prestate_clear_child + parent_growth_slots = list(range(b, b + state_growth_parent)) + parent_clear_slots = list(range(b, b + state_clear_parent)) + c = b + state_growth_parent + child_growth_slots = list(range(c, c + state_growth_child)) + child_clear_slots = list(range(c, c + state_clear_child)) + del a, b, c + + if call_op == Op.DELEGATECALL: + page = TxPageState( + slots=dict.fromkeys( + parent_prestate_slots + child_prestate_slots, 1 + ) + ) + elif call_op == Op.CALL: + page = TxPageState(slots=dict.fromkeys(child_prestate_slots, 1)) + else: + page = TxPageState() + + parent_pre = Bytecode() + for i in parent_prestate_slots: + parent_pre += Op.SSTORE(i, 0) + if call_op == Op.DELEGATECALL: + simulate_sstore(page, i, 0, fork) + for i in parent_growth_slots: + parent_pre += Op.SSTORE(i, 1) + if call_op == Op.DELEGATECALL: + simulate_sstore(page, i, 1, fork) + for i in parent_clear_slots: + parent_pre += Op.SSTORE(i, 0) + if call_op == Op.DELEGATECALL: + simulate_sstore(page, i, 0, fork) + + child_code = Bytecode() + for i in child_prestate_slots: + child_code += Op.SSTORE(i, 0) + simulate_sstore(page, i, 0, fork) + for i in child_growth_slots: + child_code += Op.SSTORE(i, 1) + simulate_sstore(page, i, 1, fork) + for i in child_clear_slots: + child_code += Op.SSTORE(i, 0) + simulate_sstore(page, i, 0, fork) + + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + measure_offset = Spec.SLOTS_PER_PAGE + expected_storage: dict[int, int] = {} + for i in range(Spec.SLOTS_PER_PAGE): + cost = simulate_sstore(page, i, 1, fork) + child_code += CodeGasMeasure( + code=Op.SSTORE(i, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=measure_offset + i, + stop=False, + ) + expected_storage[measure_offset + i] = cost + child_code += Op.RETURN(0, 0) if call_op == Op.CREATE else Op.STOP + for i in range(Spec.SLOTS_PER_PAGE): + expected_storage[i] = 1 + + tx_data: bytes = b"" + if call_op == Op.CREATE: + tx_data = bytes(child_code) + sub_call_code = Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.CREATE( + value=0, offset=0, size=Op.CALLDATASIZE + ) + else: + child_storage: dict[NumberConvertible, NumberConvertible] = ( + dict.fromkeys(child_prestate_slots, 1) + if call_op == Op.CALL + else {} + ) + child_address = pre.deploy_contract(child_code, storage=child_storage) + sub_call_code = call_op(address=child_address) + + parent_storage: dict[NumberConvertible, NumberConvertible] = ( + dict.fromkeys(parent_prestate_slots + child_prestate_slots, 1) + if call_op == Op.DELEGATECALL + else dict.fromkeys(parent_prestate_slots, 1) + ) + parent_address = pre.deploy_contract( + parent_pre + sub_call_code, storage=parent_storage + ) + + if call_op == Op.DELEGATECALL: + target_address = parent_address + elif call_op == Op.CALL: + target_address = child_address + else: + target_address = compute_create_address( + address=parent_address, nonce=1, opcode=Op.CREATE + ) + + tx = Transaction( + gas_limit=generous_gas_with_page_sweep(fork), + to=parent_address, + sender=pre.fund_eoa(), + data=tx_data, + ) + + state_test( + pre=pre, + post={target_address: Account(storage=expected_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize("call_op", [Op.DELEGATECALL, Op.CALL, Op.CREATE]) +@pytest.mark.parametrize( + "child_exit,exit_succeeds", + [ + pytest.param(Op.REVERT(0, 0), False, id="revert"), + pytest.param(Op.STOP, True, id="stop"), + pytest.param(Op.INVALID, False, id="invalid"), + pytest.param(Op.SELFDESTRUCT(0xBEEF), True, id="selfdestruct"), + ], +) +@pytest.mark.parametrize("prestate_clear_child", [0, 1, 32]) +@pytest.mark.parametrize("prestate_clear_parent", [0, 1, 32]) +@pytest.mark.parametrize("state_clear_child", [0, 1, 32]) +@pytest.mark.parametrize("state_growth_child", [0, 1, 32]) +@pytest.mark.parametrize("state_clear_parent", [0, 1, 32]) +@pytest.mark.parametrize("state_growth_parent", [0, 1, 32]) +def test_state_growth_counters_after_subcall( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + state_growth_parent: int, + state_clear_parent: int, + state_growth_child: int, + state_clear_child: int, + prestate_clear_parent: int, + prestate_clear_child: int, + child_exit: Op, + exit_succeeds: bool, + call_op: Op, +) -> None: + """ + State costs in parent after a DELEGATECALLed or plain-CALLed child, + with the child ending in REVERT / STOP / INVALID / SELFDESTRUCT. + """ + # Split slots into 4 consecutive sequences to set/clear. + parent_prestate_slots = list(range(prestate_clear_parent)) + a = prestate_clear_parent + child_prestate_slots = list(range(a, a + prestate_clear_child)) + b = a + prestate_clear_child + parent_growth_slots = list(range(b, b + state_growth_parent)) + parent_clear_slots = list(range(b, b + state_clear_parent)) + c = b + state_growth_parent + child_growth_slots = list(range(c, c + state_growth_child)) + child_clear_slots = list(range(c, c + state_clear_child)) + del a, b, c + prestate_slots = parent_prestate_slots + child_prestate_slots + + page = TxPageState(slots=dict.fromkeys(prestate_slots, 1)) + + parent_pre = Bytecode() + for i in parent_prestate_slots: + parent_pre += Op.SSTORE(i, 0) + simulate_sstore(page, i, 0, fork) + for i in parent_growth_slots: + parent_pre += Op.SSTORE(i, 1) + simulate_sstore(page, i, 1, fork) + for i in parent_clear_slots: + parent_pre += Op.SSTORE(i, 0) + simulate_sstore(page, i, 0, fork) + + child_persists = exit_succeeds and call_op == Op.DELEGATECALL + child_code = Bytecode() + for i in child_prestate_slots: + child_code += Op.SSTORE(i, 0) + if child_persists: + simulate_sstore(page, i, 0, fork) + for i in child_growth_slots: + child_code += Op.SSTORE(i, 1) + if child_persists: + simulate_sstore(page, i, 1, fork) + for i in child_clear_slots: + child_code += Op.SSTORE(i, 0) + if child_persists: + simulate_sstore(page, i, 0, fork) + child_code += child_exit + + tx_data: bytes = b"" + if call_op == Op.CREATE: + tx_data = bytes(child_code) + if child_exit == Op.INVALID: + # CREATE forwards 63/64 of remaining gas; INVALID would + # drain it, starving parent's post-subcall measurements. + # Wrap CREATE in a CALL frame to cap forwarded gas. + wrapper_address = pre.deploy_contract( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.CREATE(value=0, offset=0, size=Op.CALLDATASIZE) + + Op.STOP + ) + sub_call_code = Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.CALL( + gas=generous_gas_with_page_sweep(fork) // 4, + address=wrapper_address, + args_size=Op.CALLDATASIZE, + ) + else: + sub_call_code = Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.CREATE( + value=0, offset=0, size=Op.CALLDATASIZE + ) + else: + child_address = pre.deploy_contract(child_code) + # Cap child gas so INVALID's all-gas-consume leaves room + # for parent's post-subcall measurements. + sub_call_code = call_op( + address=child_address, + gas=generous_gas_with_page_sweep(fork) // 4, + ) + + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + measure_offset = Spec.SLOTS_PER_PAGE + measure_code = Bytecode() + expected_storage: dict[int, int] = {} + for i in range(Spec.SLOTS_PER_PAGE): + cost = simulate_sstore(page, i, 1, fork) + measure_code += CodeGasMeasure( + code=Op.SSTORE(i, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=measure_offset + i, + stop=(i == Spec.SLOTS_PER_PAGE - 1), + ) + expected_storage[measure_offset + i] = cost + for i in range(Spec.SLOTS_PER_PAGE): + expected_storage[i] = 1 + + parent_address = pre.deploy_contract( + parent_pre + sub_call_code + measure_code, + storage=dict.fromkeys(prestate_slots, 1), + ) + + tx = Transaction( + gas_limit=generous_gas_with_page_sweep(fork), + to=parent_address, + sender=pre.fund_eoa(), + data=tx_data, + ) + + state_test( + pre=pre, + post={parent_address: Account(storage=expected_storage)}, + tx=tx, + ) diff --git a/tests/monad_ten/mip8_pageified_storage/test_fork_transition.py b/tests/monad_ten/mip8_pageified_storage/test_fork_transition.py new file mode 100644 index 00000000000..838cc196f30 --- /dev/null +++ b/tests/monad_ten/mip8_pageified_storage/test_fork_transition.py @@ -0,0 +1,497 @@ +""" +Tests MONAD_NINE -> MONAD_NEXT fork transition for MIP-8 storage. +""" + +import pytest +from execution_testing import ( + AccessList, + Account, + Alloc, + Block, + BlockchainTestFiller, + Bytecode, + CodeGasMeasure, + Conditional, + Hash, + Op, + Storage, + Transaction, +) +from execution_testing.forks import MONAD_NEXT, MONAD_NINE +from execution_testing.forks.helpers import Fork + +from .helpers import ( + STATE_TRANSITIONS, + TxPageState, + generous_gas, + simulate_sstore, +) +from .spec import ref_spec_8 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8.git_path +REFERENCE_SPEC_VERSION = ref_spec_8.version + +value_code_worked = 0x1234 +slot_gas_measured = 0x10 + + +@pytest.mark.valid_at_transition_to("MONAD_NEXT") +def test_storage_persists_at_fork( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Storage written pre-fork is readable post-fork. + """ + sender = pre.fund_eoa() + contract_code = ( + Op.SSTORE(1, Op.SLOAD(0)) + Op.SSTORE(0, value_code_worked) + Op.STOP + ) + contract_address = pre.deploy_contract(contract_code) + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=0, + gas_limit=generous_gas(fork), + ), + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=1, + gas_limit=generous_gas(fork), + ), + ], + ), + ] + + blockchain_test( + pre=pre, + blocks=blocks, + post={ + contract_address: Account( + storage={0: value_code_worked, 1: value_code_worked}, + ), + }, + ) + + +@pytest.mark.valid_at_transition_to("MONAD_NEXT") +def test_page_warming_activates_at_fork( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Page-level warming activates in post-fork block. + + In MONAD_NINE (slot-level): warming slot 0 does NOT warm + slot 1 — each slot tracked independently. + In MONAD_NEXT (page-level): warming slot 0 warms entire + page 0, so slot 1 is also warm. + + Pre-fork block: SLOAD(0) then measure SLOAD(1) gas. + Post-fork block: same — but SLOAD(1) should be cheaper + because page warming kicks in. + """ + sender = pre.fund_eoa() + overhead = Op.PUSH1(0).gas_cost(fork) + + contract_address = pre.deploy_contract( + Op.SLOAD(0) + + CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=Op.TIMESTAMP, + ) + ) + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=0, + gas_limit=generous_gas(fork), + ), + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=1, + gas_limit=generous_gas(fork), + ), + ], + ), + ] + + blockchain_test( + pre=pre, + blocks=blocks, + post={ + contract_address: Account( + storage={ + # Pre-fork (MONAD_NINE): slot-level, SLOAD(1) + # is cold (different slot from SLOAD(0)) + 14_999: Op.SLOAD(key_warm=False).gas_cost(MONAD_NINE), + # Post-fork (MONAD_NEXT): page-level, SLOAD(1) + # is warm (same page as SLOAD(0)) + 15_000: Op.SLOAD(page_load_warm=True).gas_cost(MONAD_NEXT), + }, + ), + }, + ) + + +@pytest.mark.valid_at_transition_to("MONAD_NEXT") +def test_existing_storage_warms_page_at_fork( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Storage written pre-fork is read post-fork with page-level + warming. + + Single contract, calldata-branched: + - Block 1 (pre-fork, empty calldata): SSTORE slots 0 and 1. + - Block 2 (post-fork, 1-byte calldata): SLOAD(0) (cold page + load) + POP, then measure SLOAD(1) — same page, WARM under + MIP-8. + The measurement only matches if the pre-fork SSTOREs persisted. + """ + sender = pre.fund_eoa() + overhead = Op.PUSH1(0).gas_cost(fork) + + contract_address = pre.deploy_contract( + Conditional( + condition=Op.CALLDATASIZE, + if_true=( + Op.SLOAD(0) + + CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ), + if_false=Op.SSTORE(0, 0xAA) + Op.SSTORE(1, 0xBB) + Op.STOP, + ) + ) + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=0, + gas_limit=generous_gas(fork), + ), + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=1, + gas_limit=generous_gas(fork), + data=b"\x01", + ), + ], + ), + ] + + blockchain_test( + pre=pre, + blocks=blocks, + post={ + contract_address: Account( + storage={ + 0: 0xAA, + 1: 0xBB, + slot_gas_measured: Op.SLOAD(page_load_warm=True).gas_cost( + MONAD_NEXT + ), + }, + ), + }, + ) + + +@pytest.mark.parametrize("scheme", ["1pre_2post", "2pre_1post"]) +@pytest.mark.parametrize("orig,curr,new", STATE_TRANSITIONS) +@pytest.mark.valid_at_transition_to("MONAD_NEXT") +def test_sstore_state_transitions_at_fork( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + orig: int, + curr: int, + new: int, + scheme: str, +) -> None: + """ + SSTORE state-transition matrix split across the MONAD_NINE → + MONAD_NEXT fork. + + The 0 → orig → curr → new sequence is materialized as up to 3 + SSTOREs, distributed across the two blocks: + - `1pre_2post`: SSTORE 1 (0→orig) pre-fork; SSTORE 2 (orig→curr, + setup — only if orig != curr) and SSTORE 3 (curr→new, measured) + post-fork. Measured SSTORE runs on a warm page (when setup ran). + - `2pre_1post`: SSTORE 1 and (if orig != curr) SSTORE 2 pre-fork; + only SSTORE 3 (curr→new, measured) post-fork — cold page. + """ + slot = 0 + sender = pre.fund_eoa() + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + + measured = CodeGasMeasure( + code=Op.SSTORE(slot, new), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + ) + + page = TxPageState(slots={slot: orig if scheme == "1pre_2post" else curr}) + if scheme == "1pre_2post": + pre_branch = Op.SSTORE(slot, orig) + post_branch = Op.SSTORE(slot, curr) + measured + simulate_sstore(page, slot, curr, MONAD_NEXT) + else: # 2pre_1post + pre_branch = Op.SSTORE(slot, orig) + Op.SSTORE(slot, curr) + post_branch = measured + + contract_address = pre.deploy_contract( + Conditional( + condition=Op.CALLDATASIZE, + if_true=post_branch, + if_false=pre_branch, + ) + ) + + expected_gas = simulate_sstore(page, slot, new, MONAD_NEXT) + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=0, + gas_limit=generous_gas(fork), + ), + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=1, + gas_limit=generous_gas(fork), + data=b"\x01", + ), + ], + ), + ] + + expected_storage = {slot_gas_measured: expected_gas} + if new != 0: + expected_storage[slot] = new + + blockchain_test( + pre=pre, + blocks=blocks, + post={contract_address: Account(storage=expected_storage)}, + ) + + +@pytest.mark.valid_at_transition_to("MONAD_NEXT") +def test_access_list_warming_at_fork( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + EIP-2930 access-list semantics differ across the MIP-8 fork. + + Pre-fork (MONAD_NINE, slot-level): AL warms only the declared + slot; SLOAD on a different slot of the same page is cold. + Post-fork (MONAD_NEXT, page-level): AL warms the entire page. + """ + sender = pre.fund_eoa() + overhead = Op.PUSH1(0).gas_cost(fork) + + contract_address = pre.deploy_contract( + CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=Op.TIMESTAMP, + ) + ) + + al = [AccessList(address=contract_address, storage_keys=[Hash(0)])] + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + ty=1, + to=contract_address, + sender=sender, + nonce=0, + gas_limit=generous_gas(fork), + access_list=al, + ), + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + ty=1, + to=contract_address, + sender=sender, + nonce=1, + gas_limit=generous_gas(fork), + access_list=al, + ), + ], + ), + ] + + blockchain_test( + pre=pre, + blocks=blocks, + post={ + contract_address: Account( + storage={ + # Pre-fork: slot 1 NOT in AL → cold (slot-level). + 14_999: Op.SLOAD(key_warm=False).gas_cost(MONAD_NINE), + # Post-fork: slot 1 shares page with AL's slot 0 → warm. + 15_000: Op.SLOAD(page_load_warm=True).gas_cost(MONAD_NEXT), + }, + ), + }, + ) + + +@pytest.mark.valid_at_transition_to("MONAD_NEXT") +def test_blockhash_stable_across_fork( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + BLOCKHASH of pre-fork blocks stays the same when queried post-fork. + + MIP-8 changes the state-root commitment; pre-fork block hashes must + not change when read post-fork. + """ + sender = pre.fund_eoa() + + def slot_blockhash(i: int) -> Bytecode: + return Op.ADD(Op.MUL(Op.TIMESTAMP, 16), i) + + def slot_nonzero(i: int) -> Bytecode: + return Op.ADD(Op.MUL(Op.TIMESTAMP, 16), 4 + i) + + def prev_slot(i: int) -> Bytecode: + return Op.ADD(Op.MUL(Op.SUB(Op.TIMESTAMP, 1), 16), i) + + slot_stable = Op.MUL(Op.TIMESTAMP, 16) + + def stable_i(i: int) -> Bytecode: + return Op.OR( + Op.ISZERO(Op.SLOAD(prev_slot(i))), + Op.EQ(Op.SLOAD(prev_slot(i)), Op.BLOCKHASH(i)), + ) + + contract_code = ( + Op.SSTORE(slot_blockhash(1), Op.BLOCKHASH(1)) + + Op.SSTORE(slot_blockhash(2), Op.BLOCKHASH(2)) + + Op.SSTORE(slot_blockhash(3), Op.BLOCKHASH(3)) + + Op.SSTORE(slot_nonzero(1), Op.ISZERO(Op.ISZERO(Op.BLOCKHASH(1)))) + + Op.SSTORE(slot_nonzero(2), Op.ISZERO(Op.ISZERO(Op.BLOCKHASH(2)))) + + Op.SSTORE(slot_nonzero(3), Op.ISZERO(Op.ISZERO(Op.BLOCKHASH(3)))) + + Op.SSTORE( + slot_stable, + Op.AND(Op.AND(stable_i(1), stable_i(2)), stable_i(3)), + ) + ) + contract_address = pre.deploy_contract(contract_code) + + timestamps = [14_998, 14_999, 15_000, 15_001] + blocks = [ + Block( + timestamp=ts, + txs=[ + Transaction( + to=contract_address, + sender=sender, + nonce=i, + gas_limit=generous_gas(fork), + ), + ], + ) + for i, ts in enumerate(timestamps) + ] + + # Per-timestamp tuple is (is_nonzero(BLOCKHASH(1)), + # is_nonzero(BLOCKHASH(2)), is_nonzero(BLOCKHASH(3))) computed + # during that block. BLOCKHASH(n) is non-zero iff n is a past + # block (1 <= n < current_block_number). + # ts=14_998 -> block 1: queries blocks 1,2,3 — all current/future + # ts=14_999 -> block 2: block 1 is past, 2 is current, 3 future + # ts=15_000 -> block 3 (post-fork): blocks 1,2 past, 3 current + # ts=15_001 -> block 4 (post-fork): blocks 1,2,3 all past + nonzero_pattern = { + 14_998: (0, 0, 0), + 14_999: (1, 0, 0), + 15_000: (1, 1, 0), + 15_001: (1, 1, 1), + } + # Per-block slot layout (offset within ts*16 base): + storage = Storage() + for ts in timestamps: + # ts*16: is BLOCKHASH stable + storage[ts * 16] = 1 + for i in (1, 2, 3): + flag = nonzero_pattern[ts][i - 1] + if flag: + # ts*16 + 1..+3 : BLOCKHASH(1..3) value + storage.set_expect_any(ts * 16 + i) + # ts*16 + 5..+7 : is_nonzero(BLOCKHASH(1..3)) + storage[ts * 16 + 4 + i] = flag + + blockchain_test( + pre=pre, + blocks=blocks, + post={contract_address: Account(storage=storage)}, + ) diff --git a/tests/monad_ten/mip8_pageified_storage/test_sload_gas.py b/tests/monad_ten/mip8_pageified_storage/test_sload_gas.py new file mode 100644 index 00000000000..208b7923886 --- /dev/null +++ b/tests/monad_ten/mip8_pageified_storage/test_sload_gas.py @@ -0,0 +1,903 @@ +""" +Tests page-level SLOAD gas costs under MIP-8. +""" + +from typing import Callable + +import pytest +from execution_testing import ( + AccessList, + Account, + Address, + Alloc, + Block, + BlockchainTestFiller, + Bytecode, + CodeGasMeasure, + Conditional, + Hash, + Op, + StateTestFiller, + Transaction, + gas_test, + oog_test, +) +from execution_testing.forks.helpers import Fork + +from .helpers import TxPageState, generous_gas, page_index, simulate_sstore +from .spec import Spec, ref_spec_8 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8.git_path +REFERENCE_SPEC_VERSION = ref_spec_8.version + +slot_code_worked = 0x01 +slot_gas_measured = 0x02 +slot_gas_measured_2 = 0x03 +slot_gas_measured_3 = 0x04 +value_code_worked = 0x1234 + +pytestmark = [ + pytest.mark.valid_from("MONAD_NEXT"), +] + + +@pytest.mark.parametrize( + "slot", + [ + 0, + 1, + 127, + 128, + 255, + 256, + # Final page (page index 2**249 - 1) — page-arithmetic boundary. + 2**256 - 128, + 2**256 - 1, + ], +) +def test_sload_cold_page( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + slot: int, +) -> None: + """SLOAD on never-accessed page is cold; second SLOAD warm.""" + gas_test( + fork=fork, + state_test=state_test, + pre=pre, + setup_code=Op.PUSH32(slot), + subject_code=Op.SLOAD, + tear_down_code=Op.POP + Op.STOP, + cold_gas=Op.SLOAD(page_load_warm=False).gas_cost(fork), + warm_gas=Op.SLOAD(page_load_warm=True).gas_cost(fork), + ) + + +# Slots covering distinct binary subtree branches within a page. +_PAGE_BRANCH_SLOTS = [0, 1, 2, 16, 32, 64, 96, 127] + + +@pytest.mark.parametrize("warming_mode", ["sload", "acl"]) +@pytest.mark.parametrize("warmed_page", [1, 2**7 - 2]) +@pytest.mark.parametrize("warmed_offset", _PAGE_BRANCH_SLOTS) +@pytest.mark.parametrize("target_offset", _PAGE_BRANCH_SLOTS) +@pytest.mark.parametrize( + "target_page_diff", + [0, 1, -1], + ids=["same_page", "next_page", "prev_page"], +) +def test_sload_cross_page_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + warmed_page: int, + warmed_offset: int, + target_offset: int, + target_page_diff: int, + warming_mode: str, +) -> None: + """ + Cold/warm SLOAD across page boundaries. + + Warm one slot (warmed_page, warmed_offset). Read slot + on a target page (warmed_page + target_page_diff). Result + warm if both slots share a page, cold otherwise. + """ + target_page = warmed_page + target_page_diff + + warmed_slot = warmed_page * Spec.SLOTS_PER_PAGE + warmed_offset + target_slot = target_page * Spec.SLOTS_PER_PAGE + target_offset + + same_page = page_index(warmed_slot) == page_index(target_slot) + expected_gas = Op.SLOAD(page_load_warm=same_page).gas_cost(fork) + + measure = CodeGasMeasure( + code=Op.SLOAD(target_slot), + overhead_cost=Op.PUSH2(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + + if warming_mode == "sload": + contract_address = pre.deploy_contract(Op.SLOAD(warmed_slot) + measure) + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + else: + contract_address = pre.deploy_contract(measure) + tx = Transaction( + ty=1, + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + access_list=[ + AccessList( + address=contract_address, + storage_keys=[Hash(warmed_slot)], + ), + ], + ) + + state_test( + pre=pre, + post={ + contract_address: Account( + storage={slot_gas_measured: expected_gas}, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "warm_slot,measured_slot", + [ + pytest.param( + 2**256 - Spec.SLOTS_PER_PAGE, + 2**256 - 1, + id="first_then_last", + ), + pytest.param( + 2**256 - 1, + 2**256 - Spec.SLOTS_PER_PAGE, + id="last_then_first", + ), + ], +) +def test_sload_max_slot_page_boundary( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + warm_slot: int, + measured_slot: int, +) -> None: + """Verify SLOAD page arithmetic at the slot-key field boundary.""" + expected_gas = Op.SLOAD(page_load_warm=True).gas_cost(fork) + + contract_address = pre.deploy_contract( + Op.SLOAD(warm_slot) + + CodeGasMeasure( + code=Op.SLOAD(measured_slot), + overhead_cost=Op.PUSH32(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + contract_address: Account( + storage={slot_gas_measured: expected_gas}, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "across", + ["tx", "block"], +) +def test_sload_page_not_warm_across_txs( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + across: str, +) -> None: + """ + Page warming does not persist across txs or blocks. + + tx1 warms page 0 via SLOAD(0); tx2 measures SLOAD(1) without + warming. Both tx the same contract, branched by calldata. + + `across=tx`: both txs in same block. + `across=block`: txs in two separate blocks. + """ + contract_address = pre.deploy_contract( + Conditional( + condition=Op.CALLDATASIZE, + if_true=CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ), + if_false=Op.SLOAD(0), + ) + ) + + sender = pre.fund_eoa() + tx_setup = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=sender, + ) + tx_measure = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=sender, + data=b"\x01", + ) + + if across == "tx": + blocks = [Block(txs=[tx_setup, tx_measure])] + else: + blocks = [Block(txs=[tx_setup]), Block(txs=[tx_measure])] + + expected_gas = Op.SLOAD(page_load_warm=False).gas_cost(fork) + blockchain_test( + pre=pre, + blocks=blocks, + post={ + contract_address: Account( + storage={slot_gas_measured: expected_gas}, + ), + }, + ) + + +def test_sstore_warms_sload( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + SSTORE on a cold page pays LOAD_COST and adds the page to + read_accessed_pages, so a subsequent SLOAD on the same page + is warm. + """ + contract_address = pre.deploy_contract( + Op.SSTORE(0, 42) + + CodeGasMeasure( + code=Op.SLOAD(1), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + state_test( + pre=pre, + post={ + contract_address: Account( + storage={ + 0: 42, + slot_gas_measured: Op.SLOAD(page_load_warm=True).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +def test_tstore_and_sload( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """TLOAD/TSTORE independent of SLOAD/SSTORE.""" + overhead = Op.PUSH1(0).gas_cost(fork) + sstore_overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + + page = TxPageState(read_warm=True) + simulate_sstore(page, slot_gas_measured, 1, fork) + expected_sstore_gas = simulate_sstore(page, 0, 1, fork) + + contract_address = pre.deploy_contract( + Op.TSTORE(0, 99) + + Op.TLOAD(0) + + CodeGasMeasure( + code=Op.SLOAD(0), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured, + stop=False, + ) + + CodeGasMeasure( + code=Op.SSTORE(0, 1), + overhead_cost=sstore_overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured_2, + stop=False, + ) + + CodeGasMeasure( + code=Op.TLOAD(0), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=slot_gas_measured_3, + ) + ) + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + state_test( + pre=pre, + post={ + contract_address: Account( + storage={ + 0: 1, + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + slot_gas_measured_2: expected_sstore_gas, + slot_gas_measured_3: Op.TLOAD.gas_cost(fork), + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize("at_limit", [True, False]) +def test_max_cold_sload_pages_in_tx( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + at_limit: bool, +) -> None: + """ + Maximum cold SLOAD pages fitting in tx gas limit. + + at_limit=True: N = max → success, marker present. + at_limit=False: N = max + 1 → OOG, marker missing. + """ + tx_gas_cap = fork.transaction_gas_limit_cap() + assert tx_gas_cap is not None + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", contract_creation=False + ) + per_iter_gas = (Op.SLOAD(0, page_load_warm=False) + Op.POP).gas_cost(fork) + fresh_sstore_gas = Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=0, + new_value=value_code_worked, + ).gas_cost(fork) + marker_cost = (Op.PUSH2(0) + Op.PUSH3(0)).gas_cost(fork) + fresh_sstore_gas + available = tx_gas_cap - intrinsic - marker_cost + max_n = available // per_iter_gas + + # sanity check we're testing anything at all + assert max_n > 10 + + n = max_n if at_limit else max_n + 1 + marker_slot = (max_n + 100) * Spec.SLOTS_PER_PAGE + + code = Bytecode() + for i in range(n): + code += Op.POP(Op.SLOAD(i * Spec.SLOTS_PER_PAGE)) + code += Op.SSTORE(marker_slot, value_code_worked) + + contract_address = pre.deploy_contract(code) + tx = Transaction( + gas_limit=tx_gas_cap, + to=contract_address, + sender=pre.fund_eoa(), + ) + post_storage = {marker_slot: value_code_worked} if at_limit else {} + state_test( + pre=pre, + post={contract_address: Account(storage=post_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize("at_limit", [True, False]) +def test_max_consecutive_sload_slots_in_tx( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + at_limit: bool, +) -> None: + """ + Maximum consecutive SLOAD slots fitting in tx gas limit. + + Counted loop reads slots N-1 down to 0. First slot per page + is cold, subsequent on same page are warm. Marker SSTORE + after loop proves all reads succeeded. + """ + tx_gas_cap = fork.transaction_gas_limit_cap() + assert tx_gas_cap is not None + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", contract_creation=False + ) + prefix = Op.PUSH3(0) # placeholder; rebuilt below with real n_slots + loop_dest = len(prefix) + + def _loop_body(page_warm: bool) -> Bytecode: + return ( + Op.JUMPDEST + + Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.SLOAD(page_load_warm=page_warm) + + Op.POP + + Op.DUP1 + + Op.PUSH3(loop_dest) + + Op.JUMPI + ) + + cold_iter = _loop_body(False).gas_cost(fork) + warm_iter = _loop_body(True).gas_cost(fork) + + setup_overhead = Op.PUSH3(0).gas_cost(fork) + fresh_sstore_gas = Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=0, + new_value=value_code_worked, + ).gas_cost(fork) + marker_cost = (Op.PUSH2(0) + Op.PUSH3(0) + Op.POP).gas_cost( + fork + ) + fresh_sstore_gas + available = tx_gas_cap - intrinsic - setup_overhead - marker_cost + + max_n = 0 + used = 0 + seen_pages: set[int] = set() + while True: + page = max_n // Spec.SLOTS_PER_PAGE + is_cold = page not in seen_pages + iter_cost = cold_iter if is_cold else warm_iter + if used + iter_cost > available: + break + used += iter_cost + seen_pages.add(page) + max_n += 1 + + # sanity check we're testing anything at all + assert max_n > 10 + + n_slots = max_n if at_limit else max_n + 1 + marker_slot = (max_n // Spec.SLOTS_PER_PAGE + 100) * Spec.SLOTS_PER_PAGE + + code = ( + Op.PUSH3(n_slots) + + _loop_body(False) + + Op.POP + + Op.SSTORE(marker_slot, value_code_worked) + ) + + contract_address = pre.deploy_contract(code) + tx = Transaction( + gas_limit=tx_gas_cap, + to=contract_address, + sender=pre.fund_eoa(), + ) + post_storage = {marker_slot: value_code_worked} if at_limit else {} + state_test( + pre=pre, + post={contract_address: Account(storage=post_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize("read_pattern", ["same_slot", "same_page"]) +@pytest.mark.parametrize("at_limit", [True, False]) +def test_max_warm_sload_iters_in_tx( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + at_limit: bool, + read_pattern: str, +) -> None: + """ + Max SLOAD iterations fitting in tx gas, all targeting the + same warmed page. + + `same_slot`: SLOAD(0) every iter. + `same_page`: SLOAD(counter & 0x7F) every iter — slot rotates + within page 0 across the loop range. + + First iter pays cold-page load; rest are warm. + """ + tx_gas_cap = fork.transaction_gas_limit_cap() + assert tx_gas_cap is not None + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", contract_creation=False + ) + + prefix = Op.PUSH3(0) # placeholder + loop_dest = len(prefix) + + def _loop_body(page_warm: bool) -> Bytecode: + if read_pattern == "same_slot": + sload_seq = Op.SLOAD(0, page_load_warm=page_warm) + else: + # Slot = counter & 0x7F — counter still needs DUP for AND. + sload_seq = Op.SLOAD( + Op.AND(Op.DUP1, Spec.SLOTS_PER_PAGE - 1), + page_load_warm=page_warm, + ) + return ( + Op.JUMPDEST + + sload_seq + + Op.POP + + Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.PUSH3(loop_dest) + + Op.JUMPI + ) + + cold_iter = _loop_body(False).gas_cost(fork) + warm_iter = _loop_body(True).gas_cost(fork) + + setup_overhead = Op.PUSH3(0).gas_cost(fork) + fresh_sstore_gas = Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=0, + new_value=value_code_worked, + ).gas_cost(fork) + marker_cost = (Op.PUSH2(0) + Op.PUSH3(0) + Op.POP).gas_cost( + fork + ) + fresh_sstore_gas + available = tx_gas_cap - intrinsic - setup_overhead - marker_cost + + # First iter cold (one cold page touch), rest warm. + assert available >= cold_iter + max_n = 1 + (available - cold_iter) // warm_iter + + assert max_n > 1000 + + n_slots = max_n if at_limit else max_n + 1 + # Marker on a separate page so it doesn't affect page-0 warming + # accounting until after the loop. + marker_slot = 100 * Spec.SLOTS_PER_PAGE + + code = ( + Op.PUSH3(n_slots) + + _loop_body(False) + + Op.POP + + Op.SSTORE(marker_slot, value_code_worked) + ) + + contract_address = pre.deploy_contract(code) + tx = Transaction( + gas_limit=tx_gas_cap, + to=contract_address, + sender=pre.fund_eoa(), + ) + post_storage = {marker_slot: value_code_worked} if at_limit else {} + state_test( + pre=pre, + post={contract_address: Account(storage=post_storage)}, + tx=tx, + ) + + +def test_page_warming_per_account( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Warming page X in account A doesn't warm page X in account B. + + Page key is (address, page_index): (A, 0) and (B, 0) are + independent. + """ + contract_b = pre.deploy_contract( + CodeGasMeasure( + code=Op.SLOAD(0), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + contract_a = pre.deploy_contract( + Op.SLOAD(0) + Op.SSTORE(slot_code_worked, Op.CALL(address=contract_b)) + ) + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_a, + sender=pre.fund_eoa(), + ) + state_test( + pre=pre, + post={ + contract_a: Account(storage={slot_code_worked: 1}), + contract_b: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "al_shape", + [ + "duplicate_keys", + "empty_keys", + "wrong_contract", + "wrong_eoa", + "wrong_precompile", + ], +) +def test_sload_acl_edge_cases( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + al_shape: str, +) -> None: + """ + Access-list edge cases for page warming. + + `duplicate_keys`: same (address, slot) listed twice — page is + warm for the measured SLOAD (warming idempotent; second entry + still pays its 1900 gas). + `empty_keys`: AL entry for the target address with no storage + keys — warms the account but no pages. + `wrong_contract` / `wrong_eoa` / `wrong_precompile`: AL entry + declares a slot on a non-target address. The tx-target's + identical slot remains cold. + """ + contract_address = pre.deploy_contract( + CodeGasMeasure( + code=Op.SLOAD(0), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + sender = pre.fund_eoa() + if al_shape == "duplicate_keys": + access_list = [ + AccessList( + address=contract_address, + storage_keys=[Hash(0), Hash(0)], + ), + ] + expected_warm = True + elif al_shape == "empty_keys": + access_list = [ + AccessList(address=contract_address, storage_keys=[]), + ] + expected_warm = False + else: + if al_shape == "wrong_contract": + other = pre.deploy_contract(Op.STOP) + elif al_shape == "wrong_eoa": + other = sender + else: # wrong_precompile + other = Address(0x01) + access_list = [ + AccessList(address=other, storage_keys=[Hash(0)]), + ] + expected_warm = False + + tx = Transaction( + ty=1, + gas_limit=generous_gas(fork), + to=contract_address, + sender=sender, + access_list=access_list, + ) + + expected_gas = Op.SLOAD(page_load_warm=expected_warm).gas_cost(fork) + state_test( + pre=pre, + post={ + contract_address: Account( + storage={slot_gas_measured: expected_gas}, + ), + }, + tx=tx, + ) + + +def test_sload_acl_multipage_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Multi-key access list warms each declared page independently. + + AL declares two keys on page 0 (slot 0) and page 2 (slot 256). + Page 1 has no AL entry. The contract measures cold/warm SLOAD + cost on each of the three pages: + + - page 0 (slot 1, sibling of declared slot 0): WARM. + - page 1 (slot 128, no AL entry): COLD. + - page 2 (slot 257, sibling of declared slot 256): WARM. + """ + p0_slot = 1 + p1_slot = Spec.SLOTS_PER_PAGE + p2_slot = 2 * Spec.SLOTS_PER_PAGE + 1 + + # Measurement slots on far-away pages so the recording SSTORE + # does not warm the pages under measurement. + p0_meas = 1000 * Spec.SLOTS_PER_PAGE + p1_meas = 1001 * Spec.SLOTS_PER_PAGE + p2_meas = 1002 * Spec.SLOTS_PER_PAGE + + overhead = Op.PUSH2(0).gas_cost(fork) + contract_address = pre.deploy_contract( + CodeGasMeasure( + code=Op.SLOAD(p0_slot), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=p0_meas, + stop=False, + ) + + CodeGasMeasure( + code=Op.SLOAD(p1_slot), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=p1_meas, + stop=False, + ) + + CodeGasMeasure( + code=Op.SLOAD(p2_slot), + overhead_cost=overhead, + extra_stack_items=1, + sstore_key=p2_meas, + ) + ) + + warm = Op.SLOAD(page_load_warm=True).gas_cost(fork) + cold = Op.SLOAD(page_load_warm=False).gas_cost(fork) + + tx = Transaction( + ty=1, + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + access_list=[ + AccessList( + address=contract_address, + storage_keys=[Hash(0), Hash(2 * Spec.SLOTS_PER_PAGE)], + ), + ], + ) + state_test( + pre=pre, + post={ + contract_address: Account( + storage={p0_meas: warm, p1_meas: cold, p2_meas: warm}, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "probe_code", + [ + pytest.param(lambda addr: Op.BALANCE(addr), id="balance"), + pytest.param(lambda addr: Op.EXTCODESIZE(addr), id="extcodesize"), + pytest.param(lambda addr: Op.EXTCODEHASH(addr), id="extcodehash"), + pytest.param( + lambda addr: Op.EXTCODECOPY(addr, 0, 0, 32), id="extcodecopy" + ), + ], +) +def test_account_probe_does_not_warm_pages( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + probe_code: Callable[[Address], Bytecode], +) -> None: + """ + Address-level probes (BALANCE/EXTCODESIZE/EXTCODEHASH/EXTCODECOPY) + warm the account for EIP-2929 but never warm its storage pages. + """ + target_address = pre.deploy_contract( + CodeGasMeasure( + code=Op.SLOAD(0), + overhead_cost=Op.PUSH1(0).gas_cost(fork), + extra_stack_items=1, + sstore_key=slot_gas_measured, + ) + ) + + caller = pre.deploy_contract( + probe_code(target_address) + + Op.SSTORE(slot_code_worked, Op.CALL(address=target_address)) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=caller, + sender=pre.fund_eoa(), + ) + + state_test( + pre=pre, + post={ + caller: Account(storage={slot_code_worked: 1}), + target_address: Account( + storage={ + slot_gas_measured: Op.SLOAD(page_load_warm=False).gas_cost( + fork + ), + }, + ), + }, + tx=tx, + ) + + +@pytest.mark.parametrize( + "scenario", + [ + "cold", + "warm_via_sload", + "warm_via_sstore", + "cross_page_cold", + ], +) +def test_sload_oog( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + scenario: str, +) -> None: + """SLOAD OOG across page-warming variants.""" + setup: Bytecode | None = None + if scenario == "cold": + subject = Op.SLOAD(0) + elif scenario == "warm_via_sload": + setup = Op.SLOAD(0) + subject = Op.SLOAD(0, page_load_warm=True) + elif scenario == "warm_via_sstore": + setup = Op.SSTORE(1, 1) + subject = Op.SLOAD(0, page_load_warm=True) + else: # cross_page_cold + setup = Op.SLOAD(0) + subject = Op.SLOAD(Spec.SLOTS_PER_PAGE) + + oog_test( + fork=fork, + state_test=state_test, + pre=pre, + setup_code=setup, + subject_code=subject, + ) diff --git a/tests/monad_ten/mip8_pageified_storage/test_sstore_gas.py b/tests/monad_ten/mip8_pageified_storage/test_sstore_gas.py new file mode 100644 index 00000000000..fe9a98ea392 --- /dev/null +++ b/tests/monad_ten/mip8_pageified_storage/test_sstore_gas.py @@ -0,0 +1,956 @@ +""" +Tests page-level SSTORE gas costs under MIP-8. +""" + +from typing import cast + +import pytest +from execution_testing import ( + AccessList, + Account, + Alloc, + Block, + BlockchainTestFiller, + Bytecode, + CodeGasMeasure, + Conditional, + Hash, + Op, + Opcode, + StateTestFiller, + Transaction, + gas_test, + oog_test, +) +from execution_testing.base_types.conversions import NumberConvertible +from execution_testing.forks.helpers import Fork + +from .helpers import ( + STATE_TRANSITIONS, + TxPageState, + full_page_sweep_gas, + generous_gas, + page_index, + simulate_sstore, +) +from .spec import Spec, ref_spec_8 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8.git_path +REFERENCE_SPEC_VERSION = ref_spec_8.version + +slot_gas_measured = 0x100 +value_code_worked = 0x1234 + +pytestmark = [pytest.mark.valid_from("MONAD_NEXT")] + + +def _setup_current(slot: int, orig: int, curr: int) -> Bytecode: + """Bytecode that drives the slot to `curr` from initial `orig`.""" + if orig == curr: + return Bytecode() + return Op.SSTORE(slot, curr) + + +@pytest.mark.parametrize( + "target_loc", ["same_slot", "same_page", "different_page"] +) +@pytest.mark.parametrize("orig,curr,new", STATE_TRANSITIONS) +def test_sstore_state_transitions( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + orig: int, + curr: int, + new: int, + target_loc: str, +) -> None: + """ + Measured SSTORE on a slot whose page warming varies by + `target_loc`. Setup drives slot 0 (page 0) from `orig` + to `curr`. + """ + setup_slot = 0 + setup = _setup_current(setup_slot, orig, curr) + + if target_loc == "same_slot": + target_slot = setup_slot + same_page = True + elif target_loc == "same_page": + target_slot = 1 + same_page = True + else: + target_slot = Spec.SLOTS_PER_PAGE + same_page = False + + page = TxPageState(slots={setup_slot: orig} if orig != 0 else {}) + if same_page and orig != curr: + simulate_sstore(page, setup_slot, curr, fork) + elif not same_page: + # Target on a different page — fresh page state. + page = TxPageState() + expected_gas = simulate_sstore(page, target_slot, new, fork) + + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + contract_address = pre.deploy_contract( + setup + + CodeGasMeasure( + code=Op.SSTORE(target_slot, new), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + ), + storage={setup_slot: orig} if orig != 0 else {}, + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + + expected_storage = {slot_gas_measured: expected_gas} + if target_loc == "same_slot": + if new != 0: + expected_storage[setup_slot] = new + else: + if new != 0: + expected_storage[target_slot] = new + if curr != 0: + expected_storage[setup_slot] = curr + + state_test( + pre=pre, + post={contract_address: Account(storage=expected_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "curr,new", + [ + pytest.param(0, 0, id="0_0"), + pytest.param(0, 1, id="0_X"), + pytest.param(5, 5, id="X_X"), + pytest.param(5, 0, id="X_0"), + pytest.param(5, 6, id="X_Y"), + ], +) +def test_sstore_cold_then_warm( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + curr: int, + new: int, +) -> None: + """ + SSTORE on cold page transitions curr→new; subsequent calls run + on a warm page where SSTORE(slot, new) becomes a noop. Covers + both curr==new (noop on the first call) and curr!=new (write). + """ + slot = 1 + page = TxPageState(slots={slot: curr} if curr != 0 else {}) + cold_gas = simulate_sstore(page, slot, new, fork) + # After cold call, slot holds `new`; the second SSTORE(slot, new) + # is a warm noop. + warm_gas = simulate_sstore(page, slot, new, fork) + gas_test( + fork=fork, + state_test=state_test, + pre=pre, + setup_code=Op.PUSH2(new) + Op.PUSH2(slot), + subject_code=Op.SSTORE, + tear_down_code=Op.STOP, + cold_gas=cold_gas, + warm_gas=warm_gas, + subject_storage={slot: curr} if curr != 0 else None, + # SSTORE's 2300-gas stipend fires before the gas charge for + # any sub-stipend warm cost (e.g. the noop BASE = 100 gas), + # which would also OOG the sanity run. + out_of_gas_testing=False, + ) + + +_PAGE_BRANCH_SLOTS = [0, 1, 2, 16, 32, 64, 96, 127] + + +@pytest.mark.parametrize("warming_mode", ["sstore", "sload", "acl"]) +@pytest.mark.parametrize("new_equals_current", [True, False]) +@pytest.mark.parametrize("warmed_page", [1, 2**7 - 2]) +@pytest.mark.parametrize("warmed_offset", _PAGE_BRANCH_SLOTS) +@pytest.mark.parametrize("target_offset", _PAGE_BRANCH_SLOTS) +@pytest.mark.parametrize( + "target_page_diff", + [0, 1, -1], + ids=["same_page", "next_page", "prev_page"], +) +def test_sstore_cross_page_warming( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + warmed_page: int, + warmed_offset: int, + target_offset: int, + target_page_diff: int, + new_equals_current: bool, + warming_mode: str, +) -> None: + """ + Cold/warm SSTORE across page boundaries. + """ + target_page = warmed_page + target_page_diff + + warmed_slot = warmed_page * Spec.SLOTS_PER_PAGE + warmed_offset + target_slot = target_page * Spec.SLOTS_PER_PAGE + target_offset + + same_page = page_index(warmed_slot) == page_index(target_slot) + same_slot = target_slot == warmed_slot + + write_setup = warming_mode == "sstore" + + warm_value = 2 + # Only the SSTORE setup actually populates warmed_slot. + current_value = warm_value if (same_slot and write_setup) else 0 + new_value = current_value if new_equals_current else 1 + + page = TxPageState() + if same_page: + if write_setup: + simulate_sstore(page, warmed_slot, warm_value, fork) + else: + # SLOAD or ACL warm the read set only. + page.read_warm = True + expected_gas = simulate_sstore(page, target_slot, new_value, fork) + + # Measurement slot far from the matrix to avoid collision + # with target_slot (which ranges up to page ~127). + gas_slot = 200 * Spec.SLOTS_PER_PAGE + + overhead = (Op.PUSH1(0) + Op.PUSH2(0)).gas_cost(fork) + measure = CodeGasMeasure( + code=Op.SSTORE(target_slot, new_value), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=gas_slot, + ) + + if warming_mode == "sstore": + contract_address = pre.deploy_contract( + Op.SSTORE(warmed_slot, warm_value) + measure + ) + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + elif warming_mode == "sload": + contract_address = pre.deploy_contract(Op.SLOAD(warmed_slot) + measure) + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + else: + contract_address = pre.deploy_contract(measure) + tx = Transaction( + ty=1, + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + access_list=[ + AccessList( + address=contract_address, + storage_keys=[Hash(warmed_slot)], + ), + ], + ) + + expected_storage = {gas_slot: expected_gas} + if same_slot: + expected_storage[warmed_slot] = new_value + else: + if write_setup: + expected_storage[warmed_slot] = warm_value + expected_storage[target_slot] = new_value + + state_test( + pre=pre, + post={contract_address: Account(storage=expected_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "curr,new", + [ + pytest.param(42, 42, id="same"), + pytest.param(42, 99, id="different"), + ], +) +@pytest.mark.parametrize( + "across", ["same_tx", "diff_tx_same_block", "diff_block"] +) +def test_sstore_same_value_no_page_write( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + across: str, + curr: int, + new: int, +) -> None: + """ + Pre-populated slot 0 holds `curr`. A setup SSTORE on slot 1 + warms page 0; the measured SSTORE on slot 0 writes `new` — + same value (noop, BASE only) or different (cold I/O on first + measure-tx touch). + + `across` controls whether setup runs in the same tx, a prior + tx in the same block, or a prior block. + """ + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + measure_code = CodeGasMeasure( + code=Op.SSTORE(0, new), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + ) + + if across == "same_tx": + contract_address = pre.deploy_contract( + Op.SSTORE(1, 1) + measure_code, + storage={0: curr}, + ) + else: + contract_address = pre.deploy_contract( + Conditional( + condition=Op.CALLDATASIZE, + if_true=measure_code, + if_false=Op.SSTORE(1, 1), + ), + storage={0: curr}, + ) + + page = TxPageState(slots={0: curr}) + if across == "same_tx": + simulate_sstore(page, 1, 1, fork) + expected_gas = simulate_sstore(page, 0, new, fork) + + tx_setup = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + tx_measure = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + data=b"\x01", + ) + + if across == "same_tx": + blocks = [Block(txs=[tx_measure])] + elif across == "diff_tx_same_block": + blocks = [Block(txs=[tx_setup, tx_measure])] + else: + blocks = [Block(txs=[tx_setup]), Block(txs=[tx_measure])] + + expected_storage = { + 0: new, + 1: 1, + slot_gas_measured: expected_gas, + } + + blockchain_test( + pre=pre, + blocks=blocks, + post={contract_address: Account(storage=expected_storage)}, + ) + + +@pytest.mark.parametrize("at_limit", [True, False]) +def test_max_cold_sstore_pages_in_tx( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + at_limit: bool, +) -> None: + """ + Maximum cold SSTORE pages fitting in tx gas limit. + """ + tx_gas_cap = fork.transaction_gas_limit_cap() + assert tx_gas_cap is not None + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", contract_creation=False + ) + fresh_sstore = Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=0, + new_value=1, + current_state_growth=0, + net_state_growth=0, + ).gas_cost(fork) + per_iter_gas = (Op.PUSH1(0) + Op.PUSH3(0)).gas_cost(fork) + fresh_sstore + available = tx_gas_cap - intrinsic + + max_n = available // per_iter_gas + + # sanity check we're testing anything at all + assert max_n > 10 + n = max_n if at_limit else max_n + 1 + + code = Bytecode() + for i in range(n): + code += Op.SSTORE(i * Spec.SLOTS_PER_PAGE, 1) + + contract_address = pre.deploy_contract(code) + tx = Transaction( + gas_limit=tx_gas_cap, + to=contract_address, + sender=pre.fund_eoa(), + ) + if at_limit: + post_storage = {i * Spec.SLOTS_PER_PAGE: 1 for i in range(n)} + else: + post_storage = {} + state_test( + pre=pre, + post={contract_address: Account(storage=post_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize("at_limit", [True, False]) +def test_max_consecutive_sstore_slots_in_tx( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + at_limit: bool, +) -> None: + """ + Maximum consecutive SSTORE slots fitting in tx gas limit. + + Counted loop writes slot=counter for counter=N..1. + First slot per page pays full I/O; subsequent on same page + pay only BASE+STATE_GROWTH. + """ + tx_gas_cap = fork.transaction_gas_limit_cap() + assert tx_gas_cap is not None + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", contract_creation=False + ) + prefix = Op.PUSH3(0) # placeholder; rebuilt below with real n_slots + loop_dest = len(prefix) + + def _loop_body(sstore: Opcode) -> Bytecode: + return ( + Op.JUMPDEST + + Op.DUP1 + + Op.DUP1 + + sstore + + Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.PUSH3(loop_dest) + + Op.JUMPI + ) + + fresh_cold = _loop_body( + cast( + Opcode, + Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=0, + new_value=1, + current_state_growth=0, + net_state_growth=0, + ), + ) + ).gas_cost(fork) + fresh_warm = _loop_body( + cast( + Opcode, + Op.SSTORE( + page_load_warm=True, + page_write_warm=True, + current_value=0, + new_value=1, + current_state_growth=1, + net_state_growth=1, + ), + ) + ).gas_cost(fork) + + setup_and_cleanup = (Op.PUSH3(0) + Op.POP).gas_cost(fork) + available = tx_gas_cap - intrinsic - setup_and_cleanup + + max_n = 0 + used = 0 + seen_pages: set[int] = set() + while True: + slot = max_n + page = slot // Spec.SLOTS_PER_PAGE + iter_cost = fresh_cold if page not in seen_pages else fresh_warm + if used + iter_cost > available: + break + used += iter_cost + seen_pages.add(page) + max_n += 1 + + # sanity check we're testing anything at all + assert max_n > 10 + + n_slots = max_n if at_limit else max_n + 1 + code = Op.PUSH3(n_slots) + _loop_body(Op.SSTORE) + Op.POP + + contract_address = pre.deploy_contract(code) + tx = Transaction( + gas_limit=tx_gas_cap, + to=contract_address, + sender=pre.fund_eoa(), + ) + # Loop wrote slot=counter for counter=N..1: slots 1..N each set + # to themselves. + if at_limit: + post_storage = {i: i for i in range(1, max_n + 1)} + else: + post_storage = {} + state_test( + pre=pre, + post={contract_address: Account(storage=post_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize("write_pattern", ["same_slot", "same_page"]) +@pytest.mark.parametrize("at_limit", [True, False]) +def test_max_warm_sstore_iters_in_tx( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + at_limit: bool, + write_pattern: str, +) -> None: + """ + Max SSTORE iterations fitting in tx gas, all on the same + pre-populated page so each SSTORE is a noop (curr==new). + + `same_slot`: SSTORE(0, 1) every iter. + `same_page`: SSTORE(counter & 0x7F, 1) every iter — slot + rotates within page 0. + + Pre-populated storage makes every iter a same-value SSTORE: + BASE_COST only, no page I/O, no state growth. + """ + tx_gas_cap = fork.transaction_gas_limit_cap() + assert tx_gas_cap is not None + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"", contract_creation=False + ) + + prefix = Op.PUSH3(0) # placeholder + loop_dest = len(prefix) + + def _loop_body(sstore: Opcode) -> Bytecode: + if write_pattern == "same_slot": + sstore_seq = sstore(0, 1) + else: + sstore_seq = sstore(Op.AND(Op.DUP1, Spec.SLOTS_PER_PAGE - 1), 1) + return ( + Op.JUMPDEST + + sstore_seq + + Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.PUSH3(loop_dest) + + Op.JUMPI + ) + + cold_noop_sstore = cast( + Opcode, + Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=1, + new_value=1, + current_state_growth=0, + net_state_growth=0, + ), + ) + warm_noop_sstore = cast( + Opcode, + Op.SSTORE( + page_load_warm=True, + page_write_warm=False, + current_value=1, + new_value=1, + current_state_growth=0, + net_state_growth=0, + ), + ) + cold_iter = _loop_body(cold_noop_sstore).gas_cost(fork) + warm_iter = _loop_body(warm_noop_sstore).gas_cost(fork) + + setup_overhead = Op.PUSH3(0).gas_cost(fork) + marker_slot = 100 * Spec.SLOTS_PER_PAGE + marker_sstore = Op.SSTORE( + page_load_warm=False, + page_write_warm=False, + current_value=0, + new_value=value_code_worked, + ).gas_cost(fork) + marker_cost = (Op.PUSH2(0) + Op.PUSH3(0) + Op.POP).gas_cost( + fork + ) + marker_sstore + available = tx_gas_cap - intrinsic - setup_overhead - marker_cost + + assert available >= cold_iter + max_n = 1 + (available - cold_iter) // warm_iter + + # sanity check we're testing anything at all + assert max_n > 10 + + n_slots = max_n if at_limit else max_n + 1 + + # Pre-populate so every SSTORE is a noop (curr=1, new=1). + prepop: dict[NumberConvertible, NumberConvertible] + if write_pattern == "same_slot": + prepop = {0: 1} + else: + prepop = dict.fromkeys(range(Spec.SLOTS_PER_PAGE), 1) + + code = ( + Op.PUSH3(n_slots) + + _loop_body(Op.SSTORE) + + Op.POP + + Op.SSTORE(marker_slot, value_code_worked) + ) + + contract_address = pre.deploy_contract(code, storage=prepop) + tx = Transaction( + gas_limit=tx_gas_cap, + to=contract_address, + sender=pre.fund_eoa(), + ) + if at_limit: + post_storage = dict(prepop) + post_storage[marker_slot] = value_code_worked + else: + post_storage = prepop # tx OOG, no state changes commit + state_test( + pre=pre, + post={contract_address: Account(storage=post_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize("measure_slot", [0, Spec.SLOTS_PER_PAGE]) +def test_sstore_no_growth_below_peak( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + measure_slot: int, +) -> None: + """ + STATE_GROWTH_COST charged only when current exceeds peak. + """ + setup_page = 0 + setup = Op.SSTORE(0, 1) + Op.SSTORE(1, 1) + Op.SSTORE(0, 0) + on_setup_page = measure_slot // Spec.SLOTS_PER_PAGE == setup_page + + page = TxPageState() + if on_setup_page: + simulate_sstore(page, 0, 1, fork) + simulate_sstore(page, 1, 1, fork) + simulate_sstore(page, 0, 0, fork) + + overhead = (Op.PUSH2(0) + Op.PUSH1(0)).gas_cost(fork) + contract_address = pre.deploy_contract( + setup + + CodeGasMeasure( + code=Op.SSTORE(measure_slot, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + ) + ) + expected_gas = simulate_sstore(page, measure_slot, 1, fork) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + + slot_storage = {1: 1, measure_slot: 1} + state_test( + pre=pre, + post={ + contract_address: Account( + storage={**slot_storage, slot_gas_measured: expected_gas}, + ), + }, + tx=tx, + ) + + +def test_sstore_peak_holds_after_full_page_clear( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Fill page (128 slots), clear all, refill last slot: + no STATE_GROWTH charge on refill (peak already 128). + """ + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + + fill_code = Bytecode() + for i in range(Spec.SLOTS_PER_PAGE): + fill_code += Op.SSTORE(i, 1) + + clear_code = Bytecode() + for i in range(Spec.SLOTS_PER_PAGE): + clear_code += Op.SSTORE(i, 0) + + refill_first = Bytecode() + for i in range(Spec.SLOTS_PER_PAGE - 1): + refill_first += Op.SSTORE(i, 1) + + page = TxPageState() + for i in range(Spec.SLOTS_PER_PAGE): + simulate_sstore(page, i, 1, fork) + for i in range(Spec.SLOTS_PER_PAGE): + simulate_sstore(page, i, 0, fork) + for i in range(Spec.SLOTS_PER_PAGE - 1): + simulate_sstore(page, i, 1, fork) + + contract_address = pre.deploy_contract( + fill_code + + clear_code + + refill_first + + CodeGasMeasure( + code=Op.SSTORE(Spec.SLOTS_PER_PAGE - 1, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + ) + ) + expected_gas = simulate_sstore(page, Spec.SLOTS_PER_PAGE - 1, 1, fork) + + # Fill + clear + refill = 3 full-page sweeps over the same page. + tx = Transaction( + gas_limit=generous_gas(fork) + 3 * full_page_sweep_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + + expected_storage = dict.fromkeys(range(Spec.SLOTS_PER_PAGE), 1) + expected_storage[slot_gas_measured] = expected_gas + + state_test( + pre=pre, + post={contract_address: Account(storage=expected_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "refill_slot", + [ + pytest.param(0, id="same_slot"), + pytest.param(1, id="different_slot_same_page"), + ], +) +def test_sstore_no_growth_after_clear( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + refill_slot: int, +) -> None: + """ + Cleared slot replacement in the same page bypasses + STATE_GROWTH (per MIP-8 backwards-compat clause). + + Pre-state: slot 0 holds value 1, occupying one slot in page 0. + Setup SSTORE(0, 0) clears slot 0 in the same tx: counters + initialize to (0, 0) on first write, then current decrements + to -1, peak stays 0. + + Measured SSTORE(refill_slot, 1) refills either the same slot + or a sibling slot on the same page. Counter goes to 0; since + 0 is not strictly greater than peak=0, no STATE_GROWTH is + charged. The page remains write-warm so only BASE is paid. + """ + overhead = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + contract_address = pre.deploy_contract( + Op.SSTORE(0, 0) + + CodeGasMeasure( + code=Op.SSTORE(refill_slot, 1), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + ), + storage={0: 1}, + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + + page = TxPageState(slots={0: 1}) + simulate_sstore(page, 0, 0, fork) # setup clear + expected_gas = simulate_sstore(page, refill_slot, 1, fork) + + expected_storage = {refill_slot: 1, slot_gas_measured: expected_gas} + state_test( + pre=pre, + post={contract_address: Account(storage=expected_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "warm_slot,measured_slot", + [ + pytest.param( + 2**256 - Spec.SLOTS_PER_PAGE, + 2**256 - 1, + id="first_then_last", + ), + pytest.param( + 2**256 - 1, + 2**256 - Spec.SLOTS_PER_PAGE, + id="last_then_first", + ), + pytest.param(2**256 - 1, 2**256 - 1, id="last_then_last"), + ], +) +def test_sstore_max_slot_page_boundary( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + warm_slot: int, + measured_slot: int, +) -> None: + """ + Verify page arithmetic at the slot-key field boundary. + """ + same_slot = warm_slot == measured_slot + page = TxPageState() + simulate_sstore(page, warm_slot, 1, fork) + expected_gas = simulate_sstore(page, measured_slot, 2, fork) + + overhead = (Op.PUSH1(0) + Op.PUSH32(0)).gas_cost(fork) + contract_address = pre.deploy_contract( + Op.SSTORE(warm_slot, 1) + + CodeGasMeasure( + code=Op.SSTORE(measured_slot, 2), + overhead_cost=overhead, + extra_stack_items=0, + sstore_key=slot_gas_measured, + ) + ) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=pre.fund_eoa(), + ) + + expected_storage = { + warm_slot: 2 if same_slot else 1, + measured_slot: 2, + slot_gas_measured: expected_gas, + } + + state_test( + pre=pre, + post={contract_address: Account(storage=expected_storage)}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "scenario", + [ + "cold_fresh_growth", + "warm_fresh_growth", + "cold_update_no_growth", + "cold_noop", + "warm_update_no_growth", + "partial_warm_via_sload", + ], +) +def test_sstore_oog( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + scenario: str, +) -> None: + """SSTORE OOG across page-IO and state-growth variants.""" + subject_storage: dict | None = None + setup: Bytecode | None = None + expected_gas: int | None = None + if scenario == "cold_fresh_growth": + subject = Op.SSTORE(0, 1) + elif scenario == "warm_fresh_growth": + setup = Op.SSTORE(0, 1) + subject = Op.SSTORE( + 1, + 1, + page_load_warm=True, + page_write_warm=True, + current_state_growth=1, + net_state_growth=1, + ) + elif scenario == "cold_update_no_growth": + subject = Op.SSTORE(0, 2, current_value=1, new_value=2) + subject_storage = {0: 1} + elif scenario == "cold_noop": + subject = Op.SSTORE(0, 1, current_value=1, new_value=1) + subject_storage = {0: 1} + elif scenario == "partial_warm_via_sload": + # SLOAD warms the read page but not the write page; SSTORE + # on the same page then pays BASE + WRITE + STATE_GROWTH + # (no LOAD). + setup = Op.SLOAD(0) + subject = Op.SSTORE( + 1, + 1, + page_load_warm=True, + page_write_warm=False, + ) + else: # warm_update_no_growth + setup = Op.SSTORE(1, 1) + subject = Op.SSTORE( + 0, + 2, + page_load_warm=True, + page_write_warm=True, + current_value=1, + new_value=2, + current_state_growth=1, + net_state_growth=1, + ) + subject_storage = {0: 1} + # Warm SSTORE cost (BASE only) sits below EIP-2200's 2300 + # stipend; size expected_gas so OOG-by-1 fires via stipend. + pre_op = (Op.PUSH1(0) + Op.PUSH1(0)).gas_cost(fork) + expected_gas = ( + setup.gas_cost(fork) + pre_op + fork.gas_costs().CALL_STIPEND + 1 + ) + + oog_test( + fork=fork, + state_test=state_test, + pre=pre, + setup_code=setup, + subject_code=subject, + subject_storage=subject_storage, + expected_gas=expected_gas, + ) diff --git a/tests/monad_ten/mip8_pageified_storage/test_sstore_refunds.py b/tests/monad_ten/mip8_pageified_storage/test_sstore_refunds.py new file mode 100644 index 00000000000..967c7f49af6 --- /dev/null +++ b/tests/monad_ten/mip8_pageified_storage/test_sstore_refunds.py @@ -0,0 +1,74 @@ +""" +Tests SSTORE refund behavior upheld with MIP-8. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Bytecode, + Op, + StateTestFiller, + Transaction, + TransactionReceipt, +) +from execution_testing.base_types.conversions import NumberConvertible +from execution_testing.forks.helpers import Fork + +from .helpers import full_page_sweep_gas, generous_gas +from .spec import Spec, ref_spec_8 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8.git_path +REFERENCE_SPEC_VERSION = ref_spec_8.version + +sender_initial_balance = 20 * 10**18 # well above RESERVE_BALANCE (10 MON) +pre_storage_value = 99 + + +@pytest.mark.valid_from("MONAD_NINE") +def test_sstore_refund_removed( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + MIP-8 has no SSTORE refunds just as MONAD_NINE. + + Assert exact cumulative_gas_used via TransactionReceipt and + sender balance change matches the computed gas charge. + """ + pre_storage: dict[NumberConvertible, NumberConvertible] = dict.fromkeys( + range(Spec.SLOTS_PER_PAGE), pre_storage_value + ) + + code = Bytecode() + for i in range(Spec.SLOTS_PER_PAGE): + code += Op.SSTORE(i, 0) + + contract_address = pre.deploy_contract(code, storage=pre_storage) + + sender = pre.fund_eoa(amount=sender_initial_balance) + gas_price = 10**9 + gas_limit = generous_gas(fork) + full_page_sweep_gas(fork) + + tx = Transaction( + gas_limit=gas_limit, + gas_price=gas_price, + to=contract_address, + sender=sender, + expected_receipt=TransactionReceipt( + cumulative_gas_used=gas_limit, + ), + ) + + state_test( + pre=pre, + post={ + contract_address: Account(storage={}), + sender: Account( + balance=sender_initial_balance - gas_limit * gas_price, + nonce=1, + ), + }, + tx=tx, + )