diff --git a/src/ethereum/utils/__init__.py b/src/ethereum/utils/__init__.py index 70f787c315..91d9b27bb1 100644 --- a/src/ethereum/utils/__init__.py +++ b/src/ethereum/utils/__init__.py @@ -1,5 +1,13 @@ """ Utility functions used in this specification. + +This package contains utility modules for common operations: + +- `address`: Address validation, conversion, and checksumming utilities +- `byte`: Byte string padding and manipulation +- `hexadecimal`: Hex string parsing and conversion +- `numeric`: Numeric operations and conversions +- `validation`: Data validation utilities for Ethereum types """ from dataclasses import fields diff --git a/src/ethereum/utils/address.py b/src/ethereum/utils/address.py new file mode 100644 index 0000000000..fbfa3c0bf9 --- /dev/null +++ b/src/ethereum/utils/address.py @@ -0,0 +1,237 @@ +""" +Utility Functions For Ethereum Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Address specific utility functions used across the Ethereum specification. +These functions provide common operations for working with Ethereum addresses, +including validation, checksumming, and conversions. +""" + +from ethereum_types.bytes import Bytes, Bytes20 +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 + +# Standard Ethereum address length in bytes +ADDRESS_BYTE_LENGTH: int = 20 + +# Standard Ethereum address length in hex characters (without 0x prefix) +ADDRESS_HEX_LENGTH: int = 40 + + +def is_valid_address_length(address: Bytes) -> bool: + """ + Check if the given bytes have the correct length for an Ethereum address. + + An Ethereum address must be exactly 20 bytes long. + + Parameters + ---------- + address : + The byte string to validate. + + Returns + ------- + is_valid : `bool` + True if the address has the correct length, False otherwise. + + Examples + -------- + >>> is_valid_address_length(b'\\x00' * 20) + True + >>> is_valid_address_length(b'\\x00' * 19) + False + + """ + return len(address) == ADDRESS_BYTE_LENGTH + + +def is_zero_address(address: Bytes20) -> bool: + """ + Check if the given address is the zero address (all zeros). + + The zero address is commonly used to represent contract creation + transactions or as a burn address. + + Parameters + ---------- + address : + The 20-byte address to check. + + Returns + ------- + is_zero : `bool` + True if the address is all zeros, False otherwise. + + Examples + -------- + >>> is_zero_address(Bytes20(b'\\x00' * 20)) + True + + """ + return address == Bytes20(b"\x00" * ADDRESS_BYTE_LENGTH) + + +def is_precompile_address(address: Bytes20, max_precompile: int = 10) -> bool: + """ + Check if the given address is a precompiled contract address. + + Precompiled contracts are special addresses (typically 0x01 through 0x0a + on mainnet) that contain built-in functionality like cryptographic + operations. + + Parameters + ---------- + address : + The 20-byte address to check. + max_precompile : + The maximum precompile address number (inclusive). + Default is 10 for post-Prague forks. + + Returns + ------- + is_precompile : `bool` + True if the address is a precompile, False otherwise. + + Examples + -------- + >>> addr = Bytes20(b'\\x00' * 19 + b'\\x01') + >>> is_precompile_address(addr) + True + + """ + # Check if the first 19 bytes are zero + if address[:19] != b"\x00" * 19: + return False + + # Check if the last byte is within precompile range + last_byte = address[19] + return 1 <= last_byte <= max_precompile + + +def address_to_uint(address: Bytes20) -> Uint: + """ + Convert an address to its unsigned integer representation. + + Parameters + ---------- + address : + The 20-byte address to convert. + + Returns + ------- + value : `Uint` + The unsigned integer value of the address. + + """ + return Uint.from_be_bytes(address) + + +def uint_to_address(value: Uint | U256) -> Bytes20: + """ + Convert an unsigned integer to a 20-byte address. + + The integer is converted to big-endian bytes and the last 20 bytes + are used as the address (truncating from the left if necessary). + + Parameters + ---------- + value : + The unsigned integer to convert. + + Returns + ------- + address : `Bytes20` + The 20-byte address representation. + + """ + return Bytes20(value.to_be_bytes32()[-ADDRESS_BYTE_LENGTH:]) + + +def to_checksum_address(address: Bytes20) -> str: + """ + Convert an address to its EIP-55 checksummed string representation. + + EIP-55 defines a method for encoding Ethereum addresses with + mixed-case letters that serves as a checksum without increasing + the length of the address. + + Parameters + ---------- + address : + The 20-byte address to convert. + + Returns + ------- + checksummed : `str` + The checksummed address string with '0x' prefix. + + See Also + -------- + https://eips.ethereum.org/EIPS/eip-55 + + Examples + -------- + >>> addr = Bytes20(bytes.fromhex('5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed')) + >>> to_checksum_address(addr) + '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed' + + """ + hex_address = address.hex().lower() + hash_hex = keccak256(hex_address.encode()).hex() + + checksummed = "0x" + for i, char in enumerate(hex_address): + if char in "0123456789": + checksummed += char + elif int(hash_hex[i], 16) >= 8: + checksummed += char.upper() + else: + checksummed += char.lower() + + return checksummed + + +def is_valid_checksum_address(address_str: str) -> bool: + """ + Validate that an address string has a valid EIP-55 checksum. + + Parameters + ---------- + address_str : + The address string to validate (with or without '0x' prefix). + + Returns + ------- + is_valid : `bool` + True if the checksum is valid, False otherwise. + + See Also + -------- + https://eips.ethereum.org/EIPS/eip-55 + + """ + # Remove 0x prefix if present + if address_str.startswith("0x") or address_str.startswith("0X"): + address_str = address_str[2:] + + # Must be exactly 40 hex characters + if len(address_str) != ADDRESS_HEX_LENGTH: + return False + + # Check if it's valid hex + try: + address_bytes = Bytes20(bytes.fromhex(address_str)) + except ValueError: + return False + + # Get the expected checksummed version + expected = to_checksum_address(address_bytes)[2:] # Remove 0x prefix + + return address_str == expected diff --git a/src/ethereum/utils/validation.py b/src/ethereum/utils/validation.py new file mode 100644 index 0000000000..0e781127a1 --- /dev/null +++ b/src/ethereum/utils/validation.py @@ -0,0 +1,240 @@ +""" +Utility Functions For Ethereum Data Validation. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Validation utility functions used across the Ethereum specification. +These functions provide common validation operations for Ethereum data types. +""" + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +# Maximum values for common Ethereum types +MAX_U64: int = 2**64 - 1 +MAX_U256: int = 2**256 - 1 + +# Gas limits +MAX_GAS_LIMIT: int = 2**63 - 1 + +# Nonce limits (EIP-2681) +MAX_NONCE: int = 2**64 - 2 + +# Block number limits +MAX_BLOCK_NUMBER: int = 2**64 - 1 + + +def is_valid_gas_limit(gas_limit: Uint | U256 | int) -> bool: + """ + Check if a gas limit value is within valid bounds. + + Parameters + ---------- + gas_limit : + The gas limit value to validate. + + Returns + ------- + is_valid : `bool` + True if the gas limit is valid, False otherwise. + + Notes + ----- + Gas limit must be positive and not exceed the maximum safe value. + + """ + gas_value = int(gas_limit) + return 0 < gas_value <= MAX_GAS_LIMIT + + +def is_valid_nonce(nonce: Uint | U64 | int) -> bool: + """ + Check if a nonce value is within valid bounds according to EIP-2681. + + Parameters + ---------- + nonce : + The nonce value to validate. + + Returns + ------- + is_valid : `bool` + True if the nonce is valid (less than 2**64 - 1), False otherwise. + + See Also + -------- + https://eips.ethereum.org/EIPS/eip-2681 + + Notes + ----- + According to EIP-2681, the nonce must be strictly less than 2**64 - 1 + to allow for incrementing without overflow. + + """ + nonce_value = int(nonce) + return 0 <= nonce_value <= MAX_NONCE + + +def is_valid_block_number(block_number: Uint | int) -> bool: + """ + Check if a block number is within valid bounds. + + Parameters + ---------- + block_number : + The block number to validate. + + Returns + ------- + is_valid : `bool` + True if the block number is non-negative and within bounds. + + """ + block_value = int(block_number) + return 0 <= block_value <= MAX_BLOCK_NUMBER + + +def is_valid_u256(value: int) -> bool: + """ + Check if an integer value fits within U256 bounds. + + Parameters + ---------- + value : + The integer value to validate. + + Returns + ------- + is_valid : `bool` + True if the value is non-negative and less than 2**256. + + """ + return 0 <= value <= MAX_U256 + + +def is_valid_u64(value: int) -> bool: + """ + Check if an integer value fits within U64 bounds. + + Parameters + ---------- + value : + The integer value to validate. + + Returns + ------- + is_valid : `bool` + True if the value is non-negative and less than 2**64. + + """ + return 0 <= value <= MAX_U64 + + +def is_valid_hash(hash_value: Bytes) -> bool: + """ + Check if a byte sequence is a valid 32-byte hash. + + Parameters + ---------- + hash_value : + The byte sequence to validate. + + Returns + ------- + is_valid : `bool` + True if the value is exactly 32 bytes, False otherwise. + + """ + return len(hash_value) == 32 + + +def is_zero_hash(hash_value: Bytes32) -> bool: + """ + Check if a hash is the zero hash (all zeros). + + Parameters + ---------- + hash_value : + The 32-byte hash to check. + + Returns + ------- + is_zero : `bool` + True if the hash is all zeros, False otherwise. + + """ + return hash_value == Bytes32(b"\x00" * 32) + + +def is_valid_chain_id(chain_id: int) -> bool: + """ + Check if a chain ID is valid according to EIP-2294. + + Parameters + ---------- + chain_id : + The chain ID to validate. + + Returns + ------- + is_valid : `bool` + True if the chain ID is positive and fits in uint64. + + See Also + -------- + https://eips.ethereum.org/EIPS/eip-2294 + + Notes + ----- + Chain IDs must be positive integers. While there's no explicit upper + bound defined, we use uint64 as a practical limit. + + """ + return 0 < chain_id <= MAX_U64 + + +def validate_transaction_value(value: U256 | int) -> bool: + """ + Check if a transaction value is valid. + + Parameters + ---------- + value : + The transaction value in wei. + + Returns + ------- + is_valid : `bool` + True if the value is non-negative and within U256 bounds. + + """ + int_value = int(value) + return 0 <= int_value <= MAX_U256 + + +def is_valid_base_fee(base_fee: U256 | int) -> bool: + """ + Check if a base fee value is valid (post-London fork). + + Parameters + ---------- + base_fee : + The base fee per gas in wei. + + Returns + ------- + is_valid : `bool` + True if the base fee is positive and within U256 bounds. + + Notes + ----- + Base fee must be at least 1 wei after the London fork (EIP-1559). + + """ + fee_value = int(base_fee) + return 0 < fee_value <= MAX_U256 diff --git a/tests/common/test_address_utils.py b/tests/common/test_address_utils.py new file mode 100644 index 0000000000..6a1c02b115 --- /dev/null +++ b/tests/common/test_address_utils.py @@ -0,0 +1,184 @@ +""" +Tests for ethereum.utils.address module. + +These tests verify the address utility functions work correctly. +""" + +import pytest +from ethereum_types.bytes import Bytes20 +from ethereum_types.numeric import Uint + +from ethereum.utils.address import ( + ADDRESS_BYTE_LENGTH, + address_to_uint, + is_precompile_address, + is_valid_address_length, + is_valid_checksum_address, + is_zero_address, + to_checksum_address, + uint_to_address, +) + + +class TestIsValidAddressLength: + """Tests for is_valid_address_length function.""" + + def test_valid_address_length(self) -> None: + """Test with exactly 20 bytes.""" + assert is_valid_address_length(b"\x00" * 20) is True + + def test_too_short(self) -> None: + """Test with less than 20 bytes.""" + assert is_valid_address_length(b"\x00" * 19) is False + + def test_too_long(self) -> None: + """Test with more than 20 bytes.""" + assert is_valid_address_length(b"\x00" * 21) is False + + def test_empty(self) -> None: + """Test with empty bytes.""" + assert is_valid_address_length(b"") is False + + +class TestIsZeroAddress: + """Tests for is_zero_address function.""" + + def test_zero_address(self) -> None: + """Test with actual zero address.""" + zero_addr = Bytes20(b"\x00" * 20) + assert is_zero_address(zero_addr) is True + + def test_non_zero_address(self) -> None: + """Test with non-zero address.""" + non_zero = Bytes20(b"\x01" + b"\x00" * 19) + assert is_zero_address(non_zero) is False + + def test_all_ones(self) -> None: + """Test with all 0xFF bytes.""" + all_ones = Bytes20(b"\xff" * 20) + assert is_zero_address(all_ones) is False + + +class TestIsPrecompileAddress: + """Tests for is_precompile_address function.""" + + def test_precompile_one(self) -> None: + """Test address 0x01 is a precompile.""" + addr = Bytes20(b"\x00" * 19 + b"\x01") + assert is_precompile_address(addr) is True + + def test_precompile_ten(self) -> None: + """Test address 0x0a is a precompile (default max).""" + addr = Bytes20(b"\x00" * 19 + b"\x0a") + assert is_precompile_address(addr) is True + + def test_not_precompile_eleven(self) -> None: + """Test address 0x0b is not a precompile (default max=10).""" + addr = Bytes20(b"\x00" * 19 + b"\x0b") + assert is_precompile_address(addr) is False + + def test_zero_not_precompile(self) -> None: + """Test zero address is not a precompile.""" + addr = Bytes20(b"\x00" * 20) + assert is_precompile_address(addr) is False + + def test_custom_max_precompile(self) -> None: + """Test with custom max_precompile parameter.""" + addr = Bytes20(b"\x00" * 19 + b"\x0f") + assert is_precompile_address(addr, max_precompile=15) is True + assert is_precompile_address(addr, max_precompile=10) is False + + def test_high_address_not_precompile(self) -> None: + """Test that a regular address is not a precompile.""" + # Address with non-zero bytes in first 19 positions + addr = Bytes20(b"\x01" + b"\x00" * 18 + b"\x01") + assert is_precompile_address(addr) is False + + +class TestAddressToUint: + """Tests for address_to_uint function.""" + + def test_zero_address(self) -> None: + """Test converting zero address to uint.""" + addr = Bytes20(b"\x00" * 20) + assert address_to_uint(addr) == Uint(0) + + def test_address_one(self) -> None: + """Test converting address 0x01 to uint.""" + addr = Bytes20(b"\x00" * 19 + b"\x01") + assert address_to_uint(addr) == Uint(1) + + def test_max_address(self) -> None: + """Test converting max address (all 0xFF) to uint.""" + addr = Bytes20(b"\xff" * 20) + expected = Uint(2**160 - 1) + assert address_to_uint(addr) == expected + + +class TestUintToAddress: + """Tests for uint_to_address function.""" + + def test_zero(self) -> None: + """Test converting 0 to address.""" + addr = uint_to_address(Uint(0)) + assert addr == Bytes20(b"\x00" * 20) + + def test_one(self) -> None: + """Test converting 1 to address.""" + addr = uint_to_address(Uint(1)) + assert addr == Bytes20(b"\x00" * 19 + b"\x01") + + def test_roundtrip(self) -> None: + """Test converting address to uint and back.""" + original = Bytes20(b"\xde\xad\xbe\xef" + b"\x00" * 16) + uint_val = address_to_uint(original) + result = uint_to_address(uint_val) + assert result == original + + +class TestToChecksumAddress: + """Tests for to_checksum_address function.""" + + def test_eip55_example(self) -> None: + """Test with EIP-55 example address.""" + # This is an example from EIP-55 + addr_bytes = bytes.fromhex("5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") + addr = Bytes20(addr_bytes) + result = to_checksum_address(addr) + assert result == "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + + def test_zero_address(self) -> None: + """Test checksum of zero address.""" + addr = Bytes20(b"\x00" * 20) + result = to_checksum_address(addr) + assert result == "0x" + "0" * 40 + + +class TestIsValidChecksumAddress: + """Tests for is_valid_checksum_address function.""" + + def test_valid_checksum(self) -> None: + """Test with valid checksummed address.""" + assert is_valid_checksum_address( + "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + ) is True + + def test_invalid_checksum(self) -> None: + """Test with invalid checksum (all lowercase).""" + assert is_valid_checksum_address( + "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" + ) is False + + def test_without_prefix(self) -> None: + """Test address without 0x prefix.""" + assert is_valid_checksum_address( + "5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + ) is True + + def test_wrong_length(self) -> None: + """Test with wrong length address.""" + assert is_valid_checksum_address("0x5aAeb6053F3E94C9b9") is False + + def test_invalid_hex(self) -> None: + """Test with invalid hex characters.""" + assert is_valid_checksum_address("0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG") is False diff --git a/tests/common/test_validation_utils.py b/tests/common/test_validation_utils.py new file mode 100644 index 0000000000..30a61c5663 --- /dev/null +++ b/tests/common/test_validation_utils.py @@ -0,0 +1,244 @@ +""" +Tests for ethereum.utils.validation module. + +These tests verify the validation utility functions work correctly. +""" + +import pytest +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.utils.validation import ( + MAX_BLOCK_NUMBER, + MAX_GAS_LIMIT, + MAX_NONCE, + MAX_U64, + MAX_U256, + is_valid_base_fee, + is_valid_block_number, + is_valid_chain_id, + is_valid_gas_limit, + is_valid_hash, + is_valid_nonce, + is_valid_u64, + is_valid_u256, + is_zero_hash, + validate_transaction_value, +) + + +class TestIsValidGasLimit: + """Tests for is_valid_gas_limit function.""" + + def test_valid_gas_limit(self) -> None: + """Test with a typical valid gas limit.""" + assert is_valid_gas_limit(21000) is True + + def test_maximum_gas_limit(self) -> None: + """Test with maximum gas limit.""" + assert is_valid_gas_limit(MAX_GAS_LIMIT) is True + + def test_zero_gas_limit(self) -> None: + """Test with zero gas limit (invalid).""" + assert is_valid_gas_limit(0) is False + + def test_negative_gas_limit(self) -> None: + """Test with negative gas limit (invalid).""" + assert is_valid_gas_limit(-1) is False + + def test_exceeds_maximum(self) -> None: + """Test with gas limit exceeding maximum.""" + assert is_valid_gas_limit(MAX_GAS_LIMIT + 1) is False + + def test_with_uint(self) -> None: + """Test with Uint type.""" + assert is_valid_gas_limit(Uint(30000000)) is True + + +class TestIsValidNonce: + """Tests for is_valid_nonce function.""" + + def test_zero_nonce(self) -> None: + """Test with zero nonce (valid).""" + assert is_valid_nonce(0) is True + + def test_typical_nonce(self) -> None: + """Test with a typical nonce value.""" + assert is_valid_nonce(100) is True + + def test_maximum_nonce(self) -> None: + """Test with maximum valid nonce (EIP-2681).""" + assert is_valid_nonce(MAX_NONCE) is True + + def test_exceeds_maximum(self) -> None: + """Test with nonce exceeding maximum.""" + assert is_valid_nonce(MAX_NONCE + 1) is False + + def test_negative_nonce(self) -> None: + """Test with negative nonce (invalid).""" + assert is_valid_nonce(-1) is False + + +class TestIsValidBlockNumber: + """Tests for is_valid_block_number function.""" + + def test_genesis_block(self) -> None: + """Test with genesis block (0).""" + assert is_valid_block_number(0) is True + + def test_typical_block(self) -> None: + """Test with a typical block number.""" + assert is_valid_block_number(19000000) is True + + def test_maximum_block(self) -> None: + """Test with maximum block number.""" + assert is_valid_block_number(MAX_BLOCK_NUMBER) is True + + def test_negative_block(self) -> None: + """Test with negative block number (invalid).""" + assert is_valid_block_number(-1) is False + + +class TestIsValidU256: + """Tests for is_valid_u256 function.""" + + def test_zero(self) -> None: + """Test with zero.""" + assert is_valid_u256(0) is True + + def test_maximum(self) -> None: + """Test with maximum U256 value.""" + assert is_valid_u256(MAX_U256) is True + + def test_exceeds_maximum(self) -> None: + """Test with value exceeding maximum.""" + assert is_valid_u256(MAX_U256 + 1) is False + + def test_negative(self) -> None: + """Test with negative value.""" + assert is_valid_u256(-1) is False + + +class TestIsValidU64: + """Tests for is_valid_u64 function.""" + + def test_zero(self) -> None: + """Test with zero.""" + assert is_valid_u64(0) is True + + def test_maximum(self) -> None: + """Test with maximum U64 value.""" + assert is_valid_u64(MAX_U64) is True + + def test_exceeds_maximum(self) -> None: + """Test with value exceeding maximum.""" + assert is_valid_u64(MAX_U64 + 1) is False + + def test_negative(self) -> None: + """Test with negative value.""" + assert is_valid_u64(-1) is False + + +class TestIsValidHash: + """Tests for is_valid_hash function.""" + + def test_valid_hash(self) -> None: + """Test with exactly 32 bytes.""" + assert is_valid_hash(b"\x00" * 32) is True + + def test_too_short(self) -> None: + """Test with less than 32 bytes.""" + assert is_valid_hash(b"\x00" * 31) is False + + def test_too_long(self) -> None: + """Test with more than 32 bytes.""" + assert is_valid_hash(b"\x00" * 33) is False + + def test_empty(self) -> None: + """Test with empty bytes.""" + assert is_valid_hash(b"") is False + + +class TestIsZeroHash: + """Tests for is_zero_hash function.""" + + def test_zero_hash(self) -> None: + """Test with zero hash.""" + zero_hash = Bytes32(b"\x00" * 32) + assert is_zero_hash(zero_hash) is True + + def test_non_zero_hash(self) -> None: + """Test with non-zero hash.""" + non_zero = Bytes32(b"\x01" + b"\x00" * 31) + assert is_zero_hash(non_zero) is False + + def test_all_ones(self) -> None: + """Test with all 0xFF bytes.""" + all_ones = Bytes32(b"\xff" * 32) + assert is_zero_hash(all_ones) is False + + +class TestIsValidChainId: + """Tests for is_valid_chain_id function.""" + + def test_mainnet(self) -> None: + """Test with mainnet chain ID (1).""" + assert is_valid_chain_id(1) is True + + def test_sepolia(self) -> None: + """Test with Sepolia chain ID.""" + assert is_valid_chain_id(11155111) is True + + def test_zero(self) -> None: + """Test with zero (invalid).""" + assert is_valid_chain_id(0) is False + + def test_negative(self) -> None: + """Test with negative value (invalid).""" + assert is_valid_chain_id(-1) is False + + def test_maximum(self) -> None: + """Test with maximum uint64 value.""" + assert is_valid_chain_id(MAX_U64) is True + + +class TestValidateTransactionValue: + """Tests for validate_transaction_value function.""" + + def test_zero_value(self) -> None: + """Test with zero value (valid for contract calls).""" + assert validate_transaction_value(0) is True + + def test_typical_value(self) -> None: + """Test with a typical ETH transfer value.""" + one_eth = 10**18 # 1 ETH in wei + assert validate_transaction_value(one_eth) is True + + def test_maximum_value(self) -> None: + """Test with maximum U256 value.""" + assert validate_transaction_value(MAX_U256) is True + + def test_negative_value(self) -> None: + """Test with negative value (invalid).""" + assert validate_transaction_value(-1) is False + + +class TestIsValidBaseFee: + """Tests for is_valid_base_fee function.""" + + def test_minimum_base_fee(self) -> None: + """Test with minimum valid base fee (1 wei).""" + assert is_valid_base_fee(1) is True + + def test_typical_base_fee(self) -> None: + """Test with a typical base fee (10 gwei).""" + ten_gwei = 10 * 10**9 + assert is_valid_base_fee(ten_gwei) is True + + def test_zero_base_fee(self) -> None: + """Test with zero base fee (invalid post-London).""" + assert is_valid_base_fee(0) is False + + def test_negative_base_fee(self) -> None: + """Test with negative base fee (invalid).""" + assert is_valid_base_fee(-1) is False