Created
June 26, 2026 12:35
-
-
Save Aminechakr/146971bc8c0fc4741c5b7b8426188c42 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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