diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index 1b6f19c0d56..ebe7bfc9265 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -7,6 +7,7 @@ import math import random +from typing import cast import pytest @@ -19,17 +20,21 @@ Bytecode, Environment, Transaction, - While, ) +from ethereum_test_tools.code.generators import While from ethereum_test_tools.vm.opcode import Opcodes as Op -from ethereum_test_vm import Opcode +from ethereum_test_vm.opcode import Opcode +from tests.cancun.eip4844_blobs.spec import Spec as BlobsSpec +from tests.istanbul.eip152_blake2.common import Blake2bInput +from tests.istanbul.eip152_blake2.spec import Spec as Blake2bSpec +from tests.prague.eip2537_bls_12_381_precompiles import spec as bls12381_spec +from tests.prague.eip2537_bls_12_381_precompiles.spec import BytesConcatenation REFERENCE_SPEC_GIT_PATH = "TODO" REFERENCE_SPEC_VERSION = "TODO" MAX_CODE_SIZE = 24 * 1024 KECCAK_RATE = 136 -ECRECOVER_GAS_COST = 3_000 @pytest.mark.valid_from("Cancun") @@ -116,6 +121,7 @@ def test_worst_keccak( pytest.param(0x04, 15, 3, 1, id="IDENTITY"), ], ) +@pytest.mark.slow() def test_worst_precompile_only_data_input( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -237,23 +243,179 @@ def test_worst_modexp( @pytest.mark.valid_from("Cancun") -def test_worst_ecrecover( +@pytest.mark.parametrize( + "precompile_address,parameters", + [ + pytest.param( + 0x01, + [ + # The inputs below are a valid signature, thus ECRECOVER call won't + # be short-circuited by validations and do actual work. + "38D18ACB67D25C8BB9942764B62F18E17054F66A817BD4295423ADF9ED98873E", + "000000000000000000000000000000000000000000000000000000000000001B", + "38D18ACB67D25C8BB9942764B62F18E17054F66A817BD4295423ADF9ED98873E", + "789D1DD423D25F0772D2748D60F7E4B81BB14D086EBA8E8E8EFB6DCFF8A4AE02", + ], + id="ecrecover", + ), + pytest.param( + 0x06, + [ + "18B18ACFB4C2C30276DB5411368E7185B311DD124691610C5D3B74034E093DC9", + "063C909C4720840CB5134CB9F59FA749755796819658D32EFC0D288198F37266", + "07C2B7F58A84BD6145F00C9C2BC0BB1A187F20FF2C92963A88019E7C6A014EED", + "06614E20C147E940F2D70DA3F74C9A17DF361706A4485C742BD6788478FA17D7", + ], + id="bn128_add", + ), + pytest.param( + 0x07, + [ + "1A87B0584CE92F4593D161480614F2989035225609F08058CCFA3D0F940FEBE3", + "1A2F3C951F6DADCC7EE9007DFF81504B0FCD6D7CF59996EFDC33D92BF7F9F8F6", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ], + id="bn128_mul", + ), + pytest.param( + 0x08, + [ + # TODO: the following are only two inputs, but this can be extended + # to more inputs to amortize costs as much as possible. Additionally, + # there might be worse pairings that can be used. + # + # First pairing + "1C76476F4DEF4BB94541D57EBBA1193381FFA7AA76ADA664DD31C16024C43F59", + "3034DD2920F673E204FEE2811C678745FC819B55D3E9D294E45C9B03A76AEF41", + "209DD15EBFF5D46C4BD888E51A93CF99A7329636C63514396B4A452003A35BF7", + "04BF11CA01483BFA8B34B43561848D28905960114C8AC04049AF4B6315A41678", + "2BB8324AF6CFC93537A2AD1A445CFD0CA2A71ACD7AC41FADBF933C2A51BE344D", + "120A2A4CF30C1BF9845F20C6FE39E07EA2CCE61F0C9BB048165FE5E4DE877550", + # Second pairing + "111E129F1CF1097710D41C4AC70FCDFA5BA2023C6FF1CBEAC322DE49D1B6DF7C", + "103188585E2364128FE25C70558F1560F4F9350BAF3959E603CC91486E110936", + "198E9393920D483A7260BFB731FB5D25F1AA493335A9E71297E485B7AEF312C2", + "1800DEEF121F1E76426A00665E5C4479674322D4F75EDADD46DEBD5CD992F6ED", + "090689D0585FF075EC9E99AD690C3395BC4B313370B38EF355ACDADCD122975B", + "12C85EA5DB8C6DEB4AAB71808DCB408FE3D1E7690C43D37B4CE6CC0166FA7DAA", + ], + id="bn128_pairing", + ), + pytest.param( + Blake2bSpec.BLAKE2_PRECOMPILE_ADDRESS, + [ + Blake2bInput(rounds=0xFFFF, f=True).create_blake2b_tx_data(), + ], + id="blake2f", + ), + pytest.param( + BlobsSpec.POINT_EVALUATION_PRECOMPILE_ADDRESS, + [ + "01E798154708FE7789429634053CBF9F99B619F9F084048927333FCE637F549B", + "564C0A11A0F704F4FC3E8ACFE0F8245F0AD1347B378FBF96E206DA11A5D36306", + "24D25032E67A7E6A4910DF5834B8FE70E6BCFEEAC0352434196BDF4B2485D5A1", + "8F59A8D2A1A625A17F3FEA0FE5EB8C896DB3764F3185481BC22F91B4AAFFCCA25F26936857BC3A7C2539EA8EC3A952B7", + "873033E038326E87ED3E1276FD140253FA08E9FC25FB2D9A98527FC22A2C9612FBEAFDAD446CBC7BCDBDCD780AF2C16A", + ], + id="point_evaluation", + ), + pytest.param( + bls12381_spec.Spec.G1ADD, + [ + bls12381_spec.Spec.G1, + bls12381_spec.Spec.P1, + ], + id="bls12_g1add", + ), + pytest.param( + bls12381_spec.Spec.G1MSM, + [ + (bls12381_spec.Spec.P1 + bls12381_spec.Scalar(bls12381_spec.Spec.Q)) + * (len(bls12381_spec.Spec.G1MSM_DISCOUNT_TABLE) - 1), + ], + id="bls12_g1msm", + ), + pytest.param( + bls12381_spec.Spec.G2ADD, + [ + bls12381_spec.Spec.G2, + bls12381_spec.Spec.P2, + ], + id="bls12_g2add", + ), + pytest.param( + bls12381_spec.Spec.G2MSM, + [ + # TODO: the //2 is required due to a limitation of the max contract size limit. + # In a further iteration we can insert the inputs as calldata or storage and avoid + # having to do PUSHes which has this limtiation. This also applies to G1MSM. + (bls12381_spec.Spec.P2 + bls12381_spec.Scalar(bls12381_spec.Spec.Q)) + * (len(bls12381_spec.Spec.G2MSM_DISCOUNT_TABLE) // 2), + ], + id="bls12_g2msm", + ), + pytest.param( + bls12381_spec.Spec.PAIRING, + [ + bls12381_spec.Spec.G1, + bls12381_spec.Spec.G2, + ], + id="bls12_pairing_check", + ), + pytest.param( + bls12381_spec.Spec.MAP_FP_TO_G1, + [ + bls12381_spec.FP(bls12381_spec.Spec.P - 1), + ], + id="bls12_fp_to_g1", + ), + pytest.param( + bls12381_spec.Spec.MAP_FP2_TO_G2, + [ + bls12381_spec.FP2((bls12381_spec.Spec.P - 1, bls12381_spec.Spec.P - 1)), + ], + id="bls12_fp_to_g2", + ), + ], +) +@pytest.mark.slow() +def test_worst_precompile_fixed_cost( blockchain_test: BlockchainTestFiller, pre: Alloc, - fork: Fork, + precompile_address: Address, + parameters: list[str] | list[BytesConcatenation] | list[bytes], ): - """Test running a block with as many ECRECOVER calls as possible.""" + """Test running a block filled with a precompile with fixed cost.""" env = Environment() - # Calldata - calldata = ( - Op.MSTORE(0 * 32, 0x38D18ACB67D25C8BB9942764B62F18E17054F66A817BD4295423ADF9ED98873E) - + Op.MSTORE(1 * 32, 27) - + Op.MSTORE(2 * 32, 0x38D18ACB67D25C8BB9942764B62F18E17054F66A817BD4295423ADF9ED98873E) - + Op.MSTORE(3 * 32, 0x789D1DD423D25F0772D2748D60F7E4B81BB14D086EBA8E8E8EFB6DCFF8A4AE02) - ) + concatenated_bytes: bytes + if all(isinstance(p, str) for p in parameters): + parameters_str = cast(list[str], parameters) + concatenated_hex_string = "".join(parameters_str) + concatenated_bytes = bytes.fromhex(concatenated_hex_string) + elif all(isinstance(p, (bytes, BytesConcatenation)) for p in parameters): + parameters_bytes_list = [ + bytes(p) for p in cast(list[BytesConcatenation | bytes], parameters) + ] + concatenated_bytes = b"".join(parameters_bytes_list) + else: + raise TypeError( + "parameters must be a list of strings (hex) " + "or a list of byte-like objects (bytes or BytesConcatenation)." + ) - attack_block = Op.POP(Op.STATICCALL(ECRECOVER_GAS_COST, 0x1, 0, 32 * 4, 0, 0)) + padding_length = (32 - (len(concatenated_bytes) % 32)) % 32 + input_bytes = concatenated_bytes + b"\x00" * padding_length + + calldata = Bytecode() + for i in range(0, len(input_bytes), 32): + chunk = input_bytes[i : i + 32] + value_to_store = int.from_bytes(chunk, "big") + calldata += Op.MSTORE(i, value_to_store) + + attack_block = Op.POP( + Op.STATICCALL(Op.GAS, precompile_address, 0, len(concatenated_bytes), 0, 0) + ) code = code_loop_precompile_call(calldata, attack_block) code_address = pre.deploy_contract(code=bytes(code))