Skip to content

Instantly share code, notes, and snippets.

@Aminechakr
Created June 26, 2026 12:35
Show Gist options
  • Select an option

  • Save Aminechakr/146971bc8c0fc4741c5b7b8426188c42 to your computer and use it in GitHub Desktop.

Select an option

Save Aminechakr/146971bc8c0fc4741c5b7b8426188c42 to your computer and use it in GitHub Desktop.
pip install web3 eth-keys
#!/usr/bin/env python3
"""
EIP-7702 validation on Besu private chains.
Two tests:
1. Type-4 tx works and delegation is written on Prague
2. Type-4 tx is rejected without Prague
Requirements: pip install web3 eth-keys
"""
from web3 import Web3
from eth_keys import keys as eth_keys
# ── Configuration ──────────────────────────────────────────────────────────────
PRAGUE_RPC = "http://localhost:8545" # Besu node with Prague enabled
NO_PRAGUE_RPC = "http://localhost:8546" # Besu node without Prague
CHAIN_ID = 1337 # your private chain ID
# Two funded accounts on the test chain (replace with yours)
SENDER_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
DELEGATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
# ── Minimal RLP encoder ────────────────────────────────────────────────────────
def _len_prefix(length, offset):
if length < 56:
return bytes([offset + length])
lb = length.to_bytes((length.bit_length() + 7) // 8, 'big')
return bytes([offset + 55 + len(lb)]) + lb
def rlp(item):
if isinstance(item, int):
b = b'' if item == 0 else item.to_bytes((item.bit_length() + 7) // 8, 'big')
return rlp(b)
if isinstance(item, (bytes, bytearray)):
if len(item) == 1 and item[0] < 0x80:
return bytes(item)
return _len_prefix(len(item), 0x80) + bytes(item)
payload = b''.join(rlp(i) for i in item)
return _len_prefix(len(payload), 0xc0) + payload
# ── Helpers ────────────────────────────────────────────────────────────────────
def h2b(hex_str):
return bytes.fromhex(hex_str.lower().replace("0x", ""))
def sign(private_key_hex, data):
pk = eth_keys.PrivateKey(h2b(private_key_hex))
sig = pk.sign_msg_hash(Web3.keccak(primitive=data))
return sig.v, sig.r, sig.s # v = 0 or 1, r/s = ints
# ── EIP-7702 ───────────────────────────────────────────────────────────────────
def sign_authorization(chain_id, target_address, delegate_nonce, delegate_key_hex):
"""
Delegate signs: keccak256(0x05 || rlp([chain_id, address, nonce]))
This authorises their EOA to run code at target_address.
"""
addr = h2b(target_address)
v, r, s = sign(delegate_key_hex, bytes([0x05]) + rlp([chain_id, addr, delegate_nonce]))
return [chain_id, addr, delegate_nonce, v, r, s]
def send_type4_tx(w3, to_address, auth_list, sender_key_hex, gas=150_000):
"""
Build, sign and send a type-4 (EIP-7702) transaction.
Signing hash: keccak256(0x04 || rlp([chain_id, nonce, ...fields..., auth_list]))
"""
sender = w3.eth.account.from_key(sender_key_hex).address
nonce = w3.eth.get_transaction_count(sender)
fields = [CHAIN_ID, nonce, 10**9, 10**10, gas, h2b(to_address), 0, b'', [], auth_list]
v, r, s = sign(sender_key_hex, bytes([0x04]) + rlp(fields))
raw = bytes([0x04]) + rlp(fields + [v, r, s])
return w3.eth.wait_for_transaction_receipt(w3.eth.send_raw_transaction(raw))
# ── Simple test contract ───────────────────────────────────────────────────────
#
# Does only one thing when called: SSTORE(slot 0, value 1)
# This lets us verify the contract ran in the EOA's context,
# because the storage write lands on the EOA's address.
#
# Runtime (6 bytes): PUSH1 1, PUSH1 0, SSTORE, STOP → 600160005500
# Init (12 bytes): CODECOPY + RETURN → 6006600c60003960066000f3
SSTORE_CONTRACT = bytes.fromhex("6006600c60003960066000f3600160005500")
def deploy_contract(w3):
sender = w3.eth.account.from_key(SENDER_KEY).address
tx = {
'type': 2,
'nonce': w3.eth.get_transaction_count(sender),
'gas': 200_000,
'maxFeePerGas': 10**10,
'maxPriorityFeePerGas': 10**9,
'data': SSTORE_CONTRACT,
'chainId': CHAIN_ID,
}
signed = w3.eth.account.sign_transaction(tx, SENDER_KEY)
receipt = w3.eth.wait_for_transaction_receipt(w3.eth.send_raw_transaction(signed.raw_transaction))
assert receipt.contractAddress, "Contract deployment failed"
return receipt.contractAddress
# ── Tests ──────────────────────────────────────────────────────────────────────
def test_prague():
print("\n── Test 1: EIP-7702 on Prague chain ──────────────────────────────")
w3 = Web3(Web3.HTTPProvider(PRAGUE_RPC))
assert w3.is_connected(), f"Cannot reach {PRAGUE_RPC}"
delegate_addr = w3.eth.account.from_key(DELEGATE_KEY).address
# Deploy target contract
contract = deploy_contract(w3)
print(f"Contract: {contract}")
print(f"Delegate: {delegate_addr}")
assert w3.eth.get_code(delegate_addr) == b'', "EOA already has code before test"
print("Code before: empty ✓")
# Delegate signs authorisation: my EOA → run contract code
auth = sign_authorization(
chain_id=CHAIN_ID,
target_address=contract,
delegate_nonce=w3.eth.get_transaction_count(delegate_addr),
delegate_key_hex=DELEGATE_KEY,
)
# Type-4 tx: sets delegation and calls the delegate EOA in one shot
receipt = send_type4_tx(w3, to_address=delegate_addr, auth_list=[auth], sender_key_hex=SENDER_KEY)
assert receipt.status == 1, "Transaction reverted"
print("Type-4 tx: accepted ✓")
# EOA code slot must now hold 0xef0100 + contract_address (23 bytes)
code = w3.eth.get_code(delegate_addr)
expected = bytes.fromhex("ef0100") + h2b(contract)
assert code == expected, f"Got {code.hex()}, expected {expected.hex()}"
print(f"Code after: 0x{code.hex()} ✓")
# Contract ran inside EOA context → EOA's storage slot 0 must be 1
slot = int.from_bytes(w3.eth.get_storage_at(delegate_addr, 0), 'big')
assert slot == 1, f"Expected storage[0]=1, got {slot}"
print(f"Storage[0]: {slot} (contract executed in EOA context) ✓")
print("PASS ✓")
def test_no_prague():
print("\n── Test 2: type-4 rejected without Prague ────────────────────────")
w3 = Web3(Web3.HTTPProvider(NO_PRAGUE_RPC))
assert w3.is_connected(), f"Cannot reach {NO_PRAGUE_RPC}"
delegate_addr = w3.eth.account.from_key(DELEGATE_KEY).address
# Authorization content doesn't matter here — the tx type itself must be rejected
auth = sign_authorization(CHAIN_ID, "0x" + "ab" * 20, 0, DELEGATE_KEY)
try:
send_type4_tx(w3, to_address=delegate_addr, auth_list=[auth], sender_key_hex=SENDER_KEY)
print("FAIL: transaction was accepted — is Prague actually disabled on this node?")
except Exception as e:
print("PASS: transaction rejected as expected ✓")
print(f" {e}")
if __name__ == "__main__":
test_prague()
test_no_prague()
What it checks, end to end:
Step RPC call What it proves
Deploy contract eth_sendRawTransaction (type 2) Target code is on-chain
eth_getCode(EOA) before eth_getCode EOA starts with no code
Send type-4 tx eth_sendRawTransaction (type 4) Prague accepts EIP-7702 tx
eth_getCode(EOA) after eth_getCode 0xef0100 + contract_addr written to code slot
eth_getStorageAt(EOA, 0) eth_getStorageAt Contract executed in EOA context
Same tx on non-Prague eth_sendRawTransaction Node rejects unknown tx type
To run against your chains — update the three lines at the top: PRAGUE_RPC, NO_PRAGUE_RPC, CHAIN_ID, and the two private keys of funded accounts.
The script I wrote already is — it's pure Python making JSON-RPC calls to your Besu node via web3. No besu evmtool, no CLI, nothing else.
Only install needed:
pip install web3
eth-keys ships as a dependency of web3, nothing extra.
Then:
python validate_eip7702.py
Every step — deploy, sign, send, read code slot, read storage — goes through eth_sendRawTransaction / eth_getCode / eth_getStorageAt over HTTP to your node. The script I posted above is already the final version, just update these three lines:
PRAGUE_RPC = "http://<your-prague-node>:8545"
NO_PRAGUE_RPC = "http://<your-no-prague-node>:8545"
CHAIN_ID = <your-chain-id>
And the two private keys for funded accounts on your test chain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment