r/u_spec_dec•
spec_dec

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

10
Loading comments…