test
!/usr/bin/env python3
Compute the EIP-1559 (type 0x02) transaction hash like Ledger:
keccak256(0x02 || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas,
gasLimit, to, value, data, accessList]))
Dependencies:
pip install rlp eth-utils
Optional (for ABI encoding interactive mode):
pip install eth-abi
Notes on "missing" handling (Ledger-compatible defaults):
- Integers (chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, value):
blank input -> 0 (RLP encodes 0 as empty byte string)
- 'to' (destination): blank/0x -> empty bytes (contract creation)
- 'data': blank/0x -> empty bytes
- accessList: blank -> empty list
import sys import json from typing import List, Tuple
try: import rlp except ImportError: sys.exit("Missing dependency: rlp. Install with: pip install rlp")
Keccak-256
def _keccak_impl(): try: from eth_utils import keccak as _k return lambda b: _k(b) except Exception: try: import sha3 # pysha3 def _k(b): h = sha3.keccak_256() h.update(b) return h.digest() return _k except Exception: sys.exit("Missing keccak: install eth-utils or pysha3 (sha3).") keccak256 = _keccak_impl()
def strip_0x(s: str) -> str: return s[2:] if s.lower().startswith("0x") else s
def int_to_minimal_bytes(x: int) -> bytes: if x == 0: return b"" size = (x.bit_length() + 7) // 8 return x.to_bytes(size, "big")
def parse_int(inp: str) -> int: s = inp.strip() if s == "": return 0 # allow hex (0x...) or decimal if s.lower().startswith("0x"): return int(s, 16) return int(s, 10)
def parse_hex_bytes(inp: str, allow_empty=True) -> bytes: s = inp.strip() if s == "" and allow_empty: return b"" if s == "": raise ValueError("Expected hex string, got empty") s = strip_0x(s) if len(s) % 2 == 1: s = "0" + s try: return bytes.fromhex(s) except Exception: raise ValueError(f"Invalid hex: {inp}")
def parse_address(inp: str) -> bytes: s = inp.strip() if s == "" or s.lower() == "0x": return b"" # contract creation b = parse_hex_bytes(s, allow_empty=False) if len(b) != 20: raise ValueError("Address must be 20 bytes") return b
def parse_access_list(inp: str): """ Access list JSON format: [ {"address":"0x...", "storageKeys":["0x..","0x.."]}, ... ] Blank -> [] """ s = inp.strip() if s == "": return [] try: arr = json.loads(s) except Exception as e: raise ValueError(f"Invalid JSON for access list: {e}") if not isinstance(arr, list): raise ValueError("Access list must be a JSON array") out = [] for item in arr: if not isinstance(item, dict): raise ValueError("Access list entries must be objects") addr = item.get("address", "") skeys = item.get("storageKeys", []) addr_b = parse_address(addr) if len(addr_b) != 20: raise ValueError("Access list address must be 20 bytes") if not isinstance(skeys, list): raise ValueError("storageKeys must be a list") slots = [] for k in skeys: kb = parse_hex_bytes(k, allow_empty=False) if len(kb) != 32: raise ValueError("Each storage key must be 32 bytes") slots.append(kb) out.append([addr_b, slots]) return out
def encode_access_list_for_rlp(access_list) -> list: # access_list is already [["addrbytes", ["slotbytes", ...]], ...] # Ensure pure python bytes/list form suitable for rlp.encode encoded = [] for addr_b, slots in access_list: encoded.append([addr_b, list(slots)]) return encoded
def interactive_get_calldata() -> bytes: print("Calldata input:") print(" 1) Raw hex (0x...)") print(" 2) ABI function signature + args (requires eth-abi)") mode = input("Choose 1 or 2 [default 1]: ").strip() or "1" if mode == "2": try: from eth_abi import encode as abi_encode from eth_utils import keccak as _k except Exception: print("eth-abi or eth-utils not available; falling back to raw hex mode.") mode = "1"
if mode != "2":
data_hex = input("Enter raw calldata (0x..., blank for none): ").strip()
return parse_hex_bytes(data_hex, allow_empty=True)
# ABI mode
sig = input("Function signature (e.g. transfer(address,uint256)): ").strip()
if "(" not in sig or not sig.endswith(")"):
raise ValueError("Invalid function signature format")
name = sig.split("(")[0]
types_str = sig[len(name) + 1:-1]
types = [t.strip() for t in types_str.split(",")] if types_str.strip() else []
args_raw = input("Enter arguments as JSON array (e.g. [\"0xabc...\", 1000]): ").strip()
try:
args = json.loads(args_raw) if args_raw else []
except Exception as e:
raise ValueError(f"Invalid JSON for arguments: {e}")
if len(args) != len(types):
raise ValueError("Argument count does not match types in signature")
# Compute selector
selector = _k(text=sig)[:4]
# eth-abi encode
try:
enc_args = abi_encode(types, args)
except Exception as e:
raise ValueError(f"ABI encode error: {e}")
return selector + enc_args
def main(): print("Compute EIP-1559 (0x02) transaction hash (Ledger format)")
try:
chain_id = parse_int(input("chainId (blank=0): "))
nonce = parse_int(input("nonce (blank=0): "))
max_priority_fee_per_gas = parse_int(
input("maxPriorityFeePerGas (wei, blank=0): ")
)
max_fee_per_gas = parse_int(
input("maxFeePerGas (wei, blank=0): ")
)
gas_limit = parse_int(input("gasLimit (blank=0): "))
to_b = parse_address(input("destination 'to' (0x..., blank for create): "))
value = parse_int(input("amount 'value' (wei, blank=0): "))
# Calldata
data_b = interactive_get_calldata()
# Access list
print("Access list as JSON (blank for []), example:")
print(' [{"address":"0xabc...","storageKeys":["0x00..00","0x11..11"]}]')
access_list_input = input("accessList JSON (blank=[]): ")
access_list = parse_access_list(access_list_input)
except KeyboardInterrupt:
print("\nAborted.")
return
except Exception as e:
sys.exit(f"Input error: {e}")
# Convert integers to minimal big-endian bytes (0 -> b"")
chain_id_b = int_to_minimal_bytes(chain_id)
nonce_b = int_to_minimal_bytes(nonce)
max_priority_fee_per_gas_b = int_to_minimal_bytes(max_priority_fee_per_gas)
max_fee_per_gas_b = int_to_minimal_bytes(max_fee_per_gas)
gas_limit_b = int_to_minimal_bytes(gas_limit)
value_b = int_to_minimal_bytes(value)
# Build the RLP payload list in exact EIP-1559 order
payload = [
chain_id_b,
nonce_b,
max_priority_fee_per_gas_b,
max_fee_per_gas_b,
gas_limit_b,
to_b,
value_b,
data_b,
encode_access_list_for_rlp(access_list),
]
try:
encoded = rlp.encode(payload)
except Exception as e:
sys.exit(f"RLP encoding error: {e}")
tx_type_byte = b"\x02"
to_hash = tx_type_byte + encoded
h = keccak256(to_hash)
print("\nResult:")
print(f"Typed payload (hex): 0x{to_hash.hex()}")
print(f"Hash (keccak256): 0x{h.hex()}")
if name == "main": main()
Posted Sep 19, 2025, 1:35 PM UTC