diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 95c8b62bee..291bacd29f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -17,6 +17,8 @@ Test fixtures for use by clients are available for each release on the [Github r #### `consume` +- ✨ Add Besu `evmtool` support for `consume direct` via `state-test` and `block-test` subcommands ([#2219](https://github.com/ethereum/execution-specs/pull/2219)). + #### `execute` - ✨ Add transaction batching to avoid RPC overload when executing tests with many transactions. Transactions are now sent in configurable batches (default: 750) with progress logging. Use `--max-tx-per-batch` to configure the batch size ([#1907](https://github.com/ethereum/execution-specs/pull/1907)). diff --git a/docs/running_tests/consume/direct.md b/docs/running_tests/consume/direct.md index 8901f8e9a2..8cf337ba93 100644 --- a/docs/running_tests/consume/direct.md +++ b/docs/running_tests/consume/direct.md @@ -11,11 +11,17 @@ uv run consume direct --bin= [OPTIONS] !!! warning "Limited Client Support" - Currently, only the following clients can be used with `consume direct`: + Not all clients are supported. The `consume direct` command requires a client-specific test + interface. See the table below for currently supported clients. - - go-ethereum `statetest` and `blocktest` - - Nethermind `nethtest` - - evmone `evmone-statetest` and `evmone-blockchaintest` +## Supported Clients + +| Client | Binary | State Tests | Block Tests | +|--------|--------|-------------|-------------| +| go-ethereum | `evm` | `statetest` | `blocktest` | +| Besu | `evmtool` | `state-test` | `block-test` | +| Nethermind | `nethtest` | `nethtest` | `nethtest --blockTest` | +| evmone | `evmone-statetest`, `evmone-blockchaintest` | `evmone-statetest` | `evmone-blockchaintest` | ## Advantages @@ -25,7 +31,7 @@ uv run consume direct --bin= [OPTIONS] ## Limitations -- **Limited client support**: Only go-ethereum, Nethermind and evmone +- **Limited client support**: Not all clients are supported (see [Supported Clients](#supported-clients) above). - **Module scope**: Tests EVM, respectively block import, in isolation, not full client behavior. - **Interface dependency**: Requires client-specific test interfaces. @@ -37,6 +43,12 @@ Only run state tests (by using a mark filter, `-m`) from a local `fixtures` fold uv run consume direct --input ./fixtures -m state_test --bin=evm ``` +or Besu: + +```bash +uv run consume direct --input ./fixtures -m state_test --bin=evmtool +``` + or Nethermind: ```bash diff --git a/packages/testing/src/execution_testing/client_clis/__init__.py b/packages/testing/src/execution_testing/client_clis/__init__.py index 578dc6e5c3..e44839fe57 100644 --- a/packages/testing/src/execution_testing/client_clis/__init__.py +++ b/packages/testing/src/execution_testing/client_clis/__init__.py @@ -11,7 +11,7 @@ TransactionExceptionWithMessage, TransitionToolOutput, ) -from .clis.besu import BesuTransitionTool +from .clis.besu import BesuFixtureConsumer, BesuTransitionTool from .clis.ethereumjs import EthereumJSTransitionTool from .clis.evmone import ( EvmOneBlockchainFixtureConsumer, @@ -31,6 +31,7 @@ FixtureConsumerTool.set_default_tool(GethFixtureConsumer) __all__ = ( + "BesuFixtureConsumer", "BesuTransitionTool", "BlockExceptionWithMessage", "CLINotFoundInPathError", diff --git a/packages/testing/src/execution_testing/client_clis/clis/besu.py b/packages/testing/src/execution_testing/client_clis/clis/besu.py index b5698ef396..07b5ca4990 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/besu.py +++ b/packages/testing/src/execution_testing/client_clis/clis/besu.py @@ -3,11 +3,14 @@ import json import os import re +import shlex +import shutil import subprocess import tempfile import textwrap +from functools import cache from pathlib import Path -from typing import ClassVar, Dict, Optional +from typing import Any, ClassVar, Dict, List, Optional import requests @@ -17,21 +20,98 @@ ExceptionMapper, TransactionException, ) +from execution_testing.fixtures import ( + BlockchainFixture, + FixtureFormat, + StateFixture, +) from execution_testing.forks import Fork from ..cli_types import TransitionToolOutput +from ..ethereum_cli import EthereumCLI +from ..fixture_consumer_tool import FixtureConsumerTool from ..transition_tool import ( TransitionTool, dump_files_to_directory, model_dump_config, ) +BESU_BIN_DETECT_PATTERN = re.compile(r"^Besu evm .*$") + + +class BesuEvmTool(EthereumCLI): + """Besu `evmtool` base class.""" + + default_binary = Path("evmtool") + detect_binary_pattern = BESU_BIN_DETECT_PATTERN + cached_version: Optional[str] = None + trace: bool + + def __init__( + self, + binary: Optional[Path] = None, + trace: bool = False, + ): + """Initialize the BesuEvmTool class.""" + self.binary = binary if binary else self.default_binary + self.trace = trace + + def _run_command(self, command: List[str]) -> subprocess.CompletedProcess: + """Run a command and return the result.""" + try: + return subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + except subprocess.CalledProcessError as e: + raise Exception("Command failed with non-zero status.") from e + except Exception as e: + raise Exception("Unexpected exception calling evmtool.") from e + + def _consume_debug_dump( + self, + command: List[str], + result: subprocess.CompletedProcess, + fixture_path: Path, + debug_output_path: Path, + ) -> None: + """Dump debug output for a consume command.""" + assert all(isinstance(x, str) for x in command), ( + f"Not all elements of 'command' list are strings: {command}" + ) + assert len(command) > 0 + + debug_fixture_path = str(debug_output_path / "fixtures.json") + command[-1] = debug_fixture_path + + consume_direct_call = " ".join(shlex.quote(arg) for arg in command) + + consume_direct_script = textwrap.dedent( + f"""\ + #!/bin/bash + {consume_direct_call} + """ + ) + dump_files_to_directory( + str(debug_output_path), + { + "consume_direct_args.py": command, + "consume_direct_returncode.txt": result.returncode, + "consume_direct_stdout.txt": result.stdout, + "consume_direct_stderr.txt": result.stderr, + "consume_direct.sh+x": consume_direct_script, + }, + ) + shutil.copyfile(fixture_path, debug_fixture_path) + class BesuTransitionTool(TransitionTool): """Besu EvmTool Transition tool frontend wrapper class.""" default_binary = Path("evm") - detect_binary_pattern = re.compile(r"^Besu evm .*$") + detect_binary_pattern = BESU_BIN_DETECT_PATTERN binary: Path cached_version: Optional[str] = None trace: bool @@ -395,3 +475,186 @@ class BesuExceptionMapper(ExceptionMapper): r"calculated:\s*(0x[a-f0-9]+)\s+header:\s*(0x[a-f0-9]+)" ), } + + +class BesuFixtureConsumer( + BesuEvmTool, + FixtureConsumerTool, + fixture_formats=[StateFixture, BlockchainFixture], +): + """Besu's implementation of the fixture consumer.""" + + def consume_blockchain_test( + self, + fixture_path: Path, + fixture_name: Optional[str] = None, + debug_output_path: Optional[Path] = None, + ) -> None: + """ + Consume a single blockchain test. + + Besu's ``evmtool block-test`` accepts ``--test-name`` to + select a specific fixture from the file. + """ + subcommand = "block-test" + subcommand_options: List[str] = [] + if debug_output_path: + subcommand_options += ["--json"] + + if fixture_name: + subcommand_options += [ + "--test-name", + fixture_name, + ] + + command = ( + [str(self.binary)] + + [subcommand] + + subcommand_options + + [str(fixture_path)] + ) + + result = self._run_command(command) + + if debug_output_path: + self._consume_debug_dump( + command, result, fixture_path, debug_output_path + ) + + if result.returncode != 0: + raise Exception( + f"Unexpected exit code:\n{' '.join(command)}\n\n" + f"Error:\n{result.stderr}" + ) + + # Parse text output for failures + stdout = result.stdout + if "Failed:" in stdout: + failed_match = re.search(r"Failed:\s+(\d+)", stdout) + if failed_match and int(failed_match.group(1)) > 0: + raise Exception(f"Blockchain test failed:\n{stdout}") + + @cache # noqa + def consume_state_test_file( + self, + fixture_path: Path, + debug_output_path: Optional[Path] = None, + ) -> List[Dict[str, Any]]: + """ + Consume an entire state test file. + + Besu's ``evmtool state-test`` outputs one JSON object per + line (NDJSON) with a ``test`` field instead of ``name``. + This method normalizes the output to match the expected + format. + """ + subcommand = "state-test" + subcommand_options: List[str] = [] + if debug_output_path: + subcommand_options += ["--json"] + + command = ( + [str(self.binary)] + + [subcommand] + + subcommand_options + + [str(fixture_path)] + ) + result = self._run_command(command) + + if debug_output_path: + self._consume_debug_dump( + command, result, fixture_path, debug_output_path + ) + + if result.returncode != 0: + raise Exception( + f"Unexpected exit code:\n{' '.join(command)}\n\n" + f"Error:\n{result.stderr}" + ) + + # Parse NDJSON output, normalize "test" -> "name" + results: List[Dict[str, Any]] = [] + for line in result.stdout.strip().splitlines(): + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + if "test" in entry and "name" not in entry: + entry["name"] = entry["test"] + results.append(entry) + except json.JSONDecodeError as e: + raise Exception( + f"Failed to parse Besu state-test output as JSON.\n" + f"Offending line:\n{line}\n\n" + f"Error: {e}" + ) from e + return results + + def consume_state_test( + self, + fixture_path: Path, + fixture_name: Optional[str] = None, + debug_output_path: Optional[Path] = None, + ) -> None: + """ + Consume a single state test. + + Uses the cached result from ``consume_state_test_file`` + and selects the requested fixture by name. + """ + file_results = self.consume_state_test_file( + fixture_path=fixture_path, + debug_output_path=debug_output_path, + ) + if fixture_name: + test_result = [ + r for r in file_results if r["name"] == fixture_name + ] + assert len(test_result) < 2, ( + f"Multiple test results for {fixture_name}" + ) + assert len(test_result) == 1, ( + f"Test result for {fixture_name} missing" + ) + assert test_result[0]["pass"], ( + f"State test failed: " + f"{test_result[0].get('error', 'unknown error')}" + ) + else: + if any(not r["pass"] for r in file_results): + exception_text = "State test failed: \n" + "\n".join( + f"{r['name']}: " + r.get("error", "unknown error") + for r in file_results + if not r["pass"] + ) + raise Exception(exception_text) + + def consume_fixture( + self, + fixture_format: FixtureFormat, + fixture_path: Path, + fixture_name: Optional[str] = None, + debug_output_path: Optional[Path] = None, + ) -> None: + """ + Execute the appropriate Besu fixture consumer for the + fixture at ``fixture_path``. + """ + if fixture_format == BlockchainFixture: + self.consume_blockchain_test( + fixture_path=fixture_path, + fixture_name=fixture_name, + debug_output_path=debug_output_path, + ) + elif fixture_format == StateFixture: + self.consume_state_test( + fixture_path=fixture_path, + fixture_name=fixture_name, + debug_output_path=debug_output_path, + ) + else: + raise Exception( + f"Fixture format {fixture_format.format_name} " + f"not supported by {self.binary}" + ) diff --git a/packages/testing/src/execution_testing/specs/benchmark.py b/packages/testing/src/execution_testing/specs/benchmark.py index 2ba70e8059..96b8eacf5f 100644 --- a/packages/testing/src/execution_testing/specs/benchmark.py +++ b/packages/testing/src/execution_testing/specs/benchmark.py @@ -343,9 +343,14 @@ def model_post_init(self, __context: Any, /) -> None: blocks: List[Block] = self.setup_blocks - if self.fixed_opcode_count is not None and self.code_generator is None: + if ( + self.fixed_opcode_count is not None + and self.code_generator is None + and self.target_opcode is None + ): pytest.skip( - "Cannot run fixed opcode count tests without a code generator" + "Cannot run fixed opcode count tests without a " + "code generator or a target opcode set" ) if self.code_generator is not None: @@ -534,7 +539,9 @@ def generate( self.target_opcode is not None and self.fixed_opcode_count is not None ): - self._verify_target_opcode_count(blockchain_test._opcode_count) + self._verify_target_opcode_count( + blockchain_test._benchmark_opcode_count + ) return fixture else: raise Exception(f"Unsupported fixture format: {fixture_format}") diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 41f42f597d..7eb221363c 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -14,7 +14,13 @@ ) import pytest -from pydantic import ConfigDict, Field, field_validator, model_serializer +from pydantic import ( + ConfigDict, + Field, + PrivateAttr, + field_validator, + model_serializer, +) from execution_testing.base_types import ( Address, @@ -32,6 +38,7 @@ Result, TransitionTool, ) +from execution_testing.client_clis.cli_types import OpcodeCount from execution_testing.exceptions import ( BlockException, EngineAPIError, @@ -498,6 +505,8 @@ class BlockchainTest(BaseTest): verification is only performed based on the state root. """ + _benchmark_opcode_count: OpcodeCount | None = PrivateAttr(None) + supported_fixture_formats: ClassVar[ Sequence[FixtureFormat | LabeledFixtureFormat] ] = [ @@ -691,6 +700,10 @@ def generate_block_data( f"({expected_benchmark_gas_used}), difference: {diff}" ) + self._benchmark_opcode_count = ( + transition_tool_output.result.opcode_count + ) + requests_list: List[Bytes] | None = None if self.fork.header_requests_required( block_number=header.number, timestamp=header.timestamp diff --git a/tests/benchmark/compute/instruction/test_account_query.py b/tests/benchmark/compute/instruction/test_account_query.py index 8b13e34486..60819b5e17 100644 --- a/tests/benchmark/compute/instruction/test_account_query.py +++ b/tests/benchmark/compute/instruction/test_account_query.py @@ -13,7 +13,7 @@ """ import math -from typing import Any, Dict +from typing import Any, Dict, List import pytest from execution_testing import ( @@ -31,6 +31,7 @@ IteratingBytecode, JumpLoopGenerator, Op, + ParameterSet, TestPhaseManager, Transaction, While, @@ -404,27 +405,73 @@ def test_ext_account_query_cold( ) +def generate_account_query_params() -> List[ParameterSet]: + """ + Generate valid parameter combinations for test_account_query. + + Returns tuples of: (opcode, access_warm, mem_size, code_size, value_sent) + """ + all_mem_sizes = [0, 32, 256, 1024] + all_code_sizes = [0, 32, 256, 1024] + all_access_warm = [True, False] + all_value_sent = [0, 1] + + params = [] + + # BALANCE, EXTCODESIZE, EXTCODEHASH: + # only mem_size=0, code_size=0, value_sent=0 + for opcode in [Op.BALANCE, Op.EXTCODESIZE, Op.EXTCODEHASH]: + for access_warm in all_access_warm: + params.append(pytest.param(opcode, access_warm, 0, 0, 0)) + + # EXTCODECOPY: all mem_size, all code_size, value_sent=0 + for access_warm in all_access_warm: + for mem_size in all_mem_sizes: + for code_size in all_code_sizes: + params.append( + pytest.param( + Op.EXTCODECOPY, access_warm, mem_size, code_size, 0 + ) + ) + # Add None (max_code_size) separately with custom ID + params.append( + pytest.param( + Op.EXTCODECOPY, + access_warm, + mem_size, + None, + 0, + id=f"EXTCODECOPY-{access_warm}-{mem_size}-max_code_size-0", + ) + ) + + # CALL, CALLCODE: all mem_size, code_size=0, all value_sent + for opcode in [Op.CALL, Op.CALLCODE]: + for access_warm in all_access_warm: + for mem_size in all_mem_sizes: + for value_sent in all_value_sent: + params.append( + pytest.param( + opcode, access_warm, mem_size, 0, value_sent + ) + ) + + # STATICCALL, DELEGATECALL: all mem_size, code_size=0, value_sent=0 + for opcode in [Op.STATICCALL, Op.DELEGATECALL]: + for access_warm in all_access_warm: + for mem_size in all_mem_sizes: + params.append( + pytest.param(opcode, access_warm, mem_size, 0, 0) + ) + + return params + + +@pytest.mark.repricing @pytest.mark.parametrize( - "opcode", - [ - Op.BALANCE, - # CALL* - Op.CALL, - Op.CALLCODE, - Op.DELEGATECALL, - Op.STATICCALL, - # EXTCODE* - Op.EXTCODESIZE, - Op.EXTCODEHASH, - Op.EXTCODECOPY, - ], -) -@pytest.mark.parametrize("access_warm", [True, False]) -@pytest.mark.parametrize("mem_size", [0, 32, 256, 1024]) -@pytest.mark.parametrize( - "code_size", [0, 32, 256, 1024, pytest.param(None, id="max_code_size")] + "opcode,access_warm,mem_size,code_size,value_sent", + generate_account_query_params(), ) -@pytest.mark.parametrize("value_sent", [0, 1]) def test_account_query( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -435,22 +482,9 @@ def test_account_query( code_size: int, value_sent: int, gas_benchmark_value: int, + fixed_opcode_count: int | None, ) -> None: """Benchmark scenario of accessing max-code size bytecode.""" - if opcode in (Op.EXTCODESIZE, Op.EXTCODEHASH, Op.BALANCE) and ( - mem_size != 0 or code_size != 0 - ): - pytest.skip(f"No memory size configuration for {opcode}") - - if opcode not in (Op.CALL, Op.CALLCODE) and value_sent > 0: - pytest.skip(f"No value configuration for {opcode}") - - if ( - opcode in (Op.CALL, Op.CALLCODE, Op.STATICCALL, Op.DELEGATECALL) - and code_size != 0 - ): - pytest.skip(f"No code size configuration for {opcode}") - attack_gas_limit = gas_benchmark_value # Create the max-sized fork-dependent contract factory. @@ -567,14 +601,19 @@ def access_list_generator( attack_address = pre.deploy_contract(code=attack_code, balance=10**21) # Calculate the number of contracts to be targeted. - num_contracts = sum( - attack_code.tx_iterations_by_gas_limit( - fork=fork, - gas_limit=attack_gas_limit, - calldata=calldata, - access_list=access_list_generator, + if fixed_opcode_count is not None: + # Fixed opcode count mode + num_contracts = int(fixed_opcode_count * 1000) + else: + # Gas limit mode + num_contracts = sum( + attack_code.tx_iterations_by_gas_limit( + fork=fork, + gas_limit=attack_gas_limit, + calldata=calldata, + access_list=access_list_generator, + ) ) - ) # Deploy num_contracts via multiple txs (each capped by tx gas limit). with TestPhaseManager.setup(): @@ -589,16 +628,28 @@ def access_list_generator( with TestPhaseManager.execution(): attack_sender = pre.fund_eoa() - attack_txs = list( - attack_code.transactions_by_gas_limit( - fork=fork, - gas_limit=attack_gas_limit, - sender=attack_sender, - to=attack_address, - calldata=calldata, - access_list=access_list_generator, + if fixed_opcode_count is not None: + attack_txs = list( + attack_code.transactions_by_total_iteration_count( + fork=fork, + total_iterations=int(fixed_opcode_count * 1000), + sender=attack_sender, + to=attack_address, + calldata=calldata, + access_list=access_list_generator, + ) + ) + else: + attack_txs = list( + attack_code.transactions_by_gas_limit( + fork=fork, + gas_limit=attack_gas_limit, + sender=attack_sender, + to=attack_address, + calldata=calldata, + access_list=access_list_generator, + ) ) - ) total_gas_cost = sum(tx.gas_cost for tx in attack_txs) post = {} @@ -616,5 +667,6 @@ def access_list_generator( Block(txs=contracts_deployment_txs), Block(txs=attack_txs), ], + target_opcode=opcode, expected_benchmark_gas_used=total_gas_cost, ) diff --git a/tests/benchmark/compute/scenario/test_unchunkified_bytecode.py b/tests/benchmark/compute/scenario/test_unchunkified_bytecode.py index 21441a73b9..f226895ec2 100644 --- a/tests/benchmark/compute/scenario/test_unchunkified_bytecode.py +++ b/tests/benchmark/compute/scenario/test_unchunkified_bytecode.py @@ -22,6 +22,7 @@ from ..helpers import CustomSizedContractFactory +@pytest.mark.repricing @pytest.mark.parametrize( "opcode", [ @@ -40,6 +41,7 @@ def test_unchunkified_bytecode( fork: Fork, opcode: Op, gas_benchmark_value: int, + fixed_opcode_count: float | None, ) -> None: """Benchmark scenario of accessing max-code size bytecode.""" # The attack gas limit represents the transaction gas limit cap or @@ -115,13 +117,18 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: attack_address = pre.deploy_contract(code=attack_code) # Calculate the number of contracts to be targeted. - num_contracts = sum( - attack_code.tx_iterations_by_gas_limit( - fork=fork, - gas_limit=attack_gas_limit, - calldata=calldata, + if fixed_opcode_count is not None: + # Fixed opcode count mode + num_contracts = int(fixed_opcode_count * 1000) + else: + # Gas limit mode + num_contracts = sum( + attack_code.tx_iterations_by_gas_limit( + fork=fork, + gas_limit=attack_gas_limit, + calldata=calldata, + ) ) - ) # Deploy num_contracts via multiple txs (each capped by tx gas limit). with TestPhaseManager.setup(): @@ -136,15 +143,27 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: with TestPhaseManager.execution(): attack_sender = pre.fund_eoa() - attack_txs = list( - attack_code.transactions_by_gas_limit( - fork=fork, - gas_limit=attack_gas_limit, - sender=attack_sender, - to=attack_address, - calldata=calldata, + if fixed_opcode_count is not None: + # Fixed opcode count mode. + attack_txs = list( + attack_code.transactions_by_total_iteration_count( + fork=fork, + total_iterations=int(fixed_opcode_count * 1000), + sender=attack_sender, + to=attack_address, + calldata=calldata, + ) + ) + else: + attack_txs = list( + attack_code.transactions_by_gas_limit( + fork=fork, + gas_limit=attack_gas_limit, + sender=attack_sender, + to=attack_address, + calldata=calldata, + ) ) - ) total_gas_cost = sum(tx.gas_cost for tx in attack_txs) post = {} @@ -161,5 +180,6 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: Block(txs=contracts_deployment_txs), Block(txs=attack_txs), ], + target_opcode=opcode, expected_benchmark_gas_used=total_gas_cost, ) diff --git a/tests/byzantium/eip198_modexp_precompile/test_modexp.py b/tests/byzantium/eip198_modexp_precompile/test_modexp.py index abf9ec6a1f..beff496046 100644 --- a/tests/byzantium/eip198_modexp_precompile/test_modexp.py +++ b/tests/byzantium/eip198_modexp_precompile/test_modexp.py @@ -399,6 +399,56 @@ ), id="mod-264-even-ctz-48", ), + pytest.param( + Bytes( + "0x00000000000000000000000000000000" + "0000000000000000100000000000000000000000000000000" + "0000000000000000000000000000000100000000000000000" + "000000000000000000000000000000005" + ), + ModExpOutput( + call_success=False, + returned_data="0000000000000000000000000000000000000000000000000000000000000000", + ), + id="truncated_lengths_1", + ), + pytest.param( + Bytes( + "0x00000000000000000000000000000000000000000" + "0000000000000000000000100000000000000000000" + "00000000000000000000000000000005" + ), + ModExpOutput( + call_success=False, + returned_data="0000000000000000000000000000000000000000000000000000000000000000", + ), + id="truncated_lengths_2", + ), + pytest.param( + Bytes( + "0x00000000000000000000000000000000000000000000000000000500" + ), + ModExpOutput( + call_success=False, + returned_data="0000000000000000000000000000000000000000000000000000000000000000", + ), + id="truncated_lengths_3", + ), + pytest.param( + Bytes( + "0x00000000000000000000000000000000000" + "000000000000000000000" + "0000000100000000000000000000000000000" + "000000000000000000000" + "0000000000000200000000000000000000000" + "000000000000000000000" + "000000000000000000050201" + ), + ModExpOutput( + returned_data="0x0000000000", + ), + id="truncated_input_4", + ), ], ids=lambda param: param.__repr__(), # only required to remove parameter # names (input/output)