diff --git a/.circleci/config.yml b/.circleci/config.yml index f240fe2b..96940359 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -180,15 +180,99 @@ jobs: paths: - ./bundler/node_modules +# TODO: extract all the shared boilerplate stuff, pdm and bundler compilation + test-erc4337-with-eip7702-bundler: + # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. + # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor + docker: + - image: shahafn/go-python-node +# - image: ethpandaops/geth:prague-devnet-3-effcd38 +# command: "\ +# --miner.gaslimit 12000000 \ +# --http \ +# --http.api personal,eth,net,web3,debug \ +# --allow-insecure-unlock \ +# --rpc.allow-unprotected-txs \ +# --http.vhosts '*,localhost,host.docker.internal' \ +# --http.corsdomain '*' \ +# --http.addr '0.0.0.0' \ +# --dev \ +# --rpc.txfeecap 0 \ +# --nodiscover --maxpeers 0 --mine \ +# --verbosity 2" + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + - checkout + - run: + name: "clone bundler" + command: ./scripts/clone-helper master https://github.com/eth-infinitism/bundler.git + - run: + name: "yarn install for bundler (TMP: delete nested 'util' to use our pre-built)" + working_directory: "./bundler" + command: yarn install --ignore-engines && rm -rf ./node_modules/@ethereumjs/common/node_modules/@ethereumjs/util && yarn preprocess + - run: + name: "curl pdm" + command: "curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -" + - run: + name: "update-deps" + command: "pdm run update-deps" + - run: + name: "pdm install" + command: "pdm install" + - run: + name: "clone go-ethereum" + command: ./scripts/clone-helper RIP-7560-revision-2 https://github.com/eth-infinitism/go-ethereum.git + - run: + name: "build go-ethereum" + working_directory: "./go-ethereum" + command: make geth + - run: + name: "run go-ethereum" + working_directory: "./go-ethereum" + command: "\ + ./build/bin/geth \ + --dev \ + --dev.gaslimit \ + 30000000 \ + --http \ + --http.api \ + 'eth,net,web3,personal,debug' \ + --http.port \ + 8545 \ + --rpc.allow-unprotected-txs \ + " + background: true + - run: + name: "deploy entry point" + working_directory: "./bundler" + command: yarn hardhat-deploy --network localhost + - run: + name: "run bundler" + working_directory: "./bundler" + command: yarn bundler + background: true + - run: + name: "await bundler" + working_directory: "./bundler" + shell: /bin/sh + command: | + wget --post-data="{\"method\": \"eth_supportedEntryPoints\"}" --retry-connrefused --waitretry=2 --timeout=60 --tries=30 http://localhost:3000/rpc + - run: + name: "pytest" + command: "pdm run test-eip7702" # Invoke jobs via workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows workflows: - test-bundler-erc4337-workflow: - jobs: - - test-erc4337-bundler - test-bundler-rip7560-workflow: +# test-bundler-erc4337-workflow: +# jobs: +# - test-erc4337-bundler +# test-bundler-rip7560-workflow: +# jobs: +# - test-rip7560-bundler + test-erc4337-with-eip7702-bundler: jobs: - - test-rip7560-bundler + - test-erc4337-with-eip7702-bundler diff --git a/pyproject.toml b/pyproject.toml index 0d0bad66..aaadb387 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ update-deps = {shell = "git submodule update --init --recursive && cd @account-a update-deps-remote = {shell = "git submodule update --init --recursive --remote && cd @account-abstraction && yarn && yarn compile && cd ../spec && yarn && yarn build && cd ../@rip7560 && yarn && yarn compile-hardhat"} test = "pytest --tb=short -rA -W ignore::DeprecationWarning --url http://localhost:3000/rpc --entry-point 0x0000000071727De22E5E9d8BAf0edAc6f37da032 --ethereum-node http://127.0.0.1:8545/ tests/single" test-rip7560 = "pytest --tb=short -rA -W ignore::DeprecationWarning --url http://localhost:3000/rpc --nonce-manager 0x63f63e798f5F6A934Acf0a3FD1C01f3Fac851fF0 --stake-manager 0x570Aa568b6cf62ff08c6C3a3b3DB1a0438E871Fb --ethereum-node http://127.0.0.1:8545/ tests/rip7560" +test-eip7702 = "pytest --tb=short -rA -W ignore::DeprecationWarning --url http://localhost:3000/rpc --entry-point 0x0000000071727De22E5E9d8BAf0edAc6f37da032 --nonce-manager 0x63f63e798f5F6A934Acf0a3FD1C01f3Fac851fF0 --stake-manager 0x570Aa568b6cf62ff08c6C3a3b3DB1a0438E871Fb --ethereum-node http://127.0.0.1:8545/ tests/eip7702" p2ptest = "pytest --tb=short -rA -W ignore::DeprecationWarning --url http://localhost:3000/rpc --entry-point 0x0000000071727De22E5E9d8BAf0edAc6f37da032 --ethereum-node http://127.0.0.1:8545/ tests/p2p" lint = "pylint tests" format = "black tests" diff --git a/tests/conftest.py b/tests/conftest.py index 41c2cd04..aa6a1952 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,9 @@ from web3 import Web3 from web3.middleware import SignAndSendRawMiddlewareBuilder, ExtraDataToPOAMiddleware from .types import UserOperation, RPCRequest, CommandLineArgs + +from .user_operation_erc4337 import UserOperation + from .utils import ( assert_ok, deploy_and_deposit, diff --git a/tests/eip7702/test_eip7702_tuple_userop.py b/tests/eip7702/test_eip7702_tuple_userop.py new file mode 100644 index 00000000..89493a07 --- /dev/null +++ b/tests/eip7702/test_eip7702_tuple_userop.py @@ -0,0 +1,58 @@ +import time + +from tests.transaction_eip_7702 import TupleEIP7702 +from tests.types import CommandLineArgs +from tests.utils import deploy_contract, userop_hash, send_bundle_now + +ADDRESS = "0xbe862AD9AbFe6f22BCb087716c7D89a26051f74C" +PRIVATE_KEY = "e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109" +AUTHORIZED_ACCOUNT_PREFIX = "ef0100" + + +def test_send_eip_7702_tx(w3, userop, wallet_contract, helper_contract): + # fund the EOA address + w3.eth.send_transaction( + {"from": w3.eth.accounts[0], "to": ADDRESS, "value": 10 ** 18} + ) + + # implementation is only used as a "delegation target" + implementation = deploy_contract( + w3, "SimpleWallet", ctrparams=[CommandLineArgs.entrypoint] + ) + + # create an EIP-7702 authorization tuple + nonce = w3.eth.get_transaction_count(ADDRESS) + auth_tuple = TupleEIP7702( + chainId=hex(1337), address=implementation.address, nonce=hex(nonce) + ) + auth_tuple.sign(PRIVATE_KEY) + + userop.sender = ADDRESS + userop.authorizationList = [auth_tuple] + + # note that ADDRESS used here is hard-coded so the test will only pass once! + sender_code = w3.eth.get_code(ADDRESS) + assert len(sender_code) == 0 + + response = userop.send() + send_bundle_now() + + assert response.result == userop_hash(helper_contract, userop) + + sender_code = w3.eth.get_code(ADDRESS) + + # delegated EOA code is always 23 bytes long + assert len(sender_code) == 23 + expected_code = "".join( + [AUTHORIZED_ACCOUNT_PREFIX, implementation.address[2:].lower()] + ) + assert sender_code.hex() == expected_code + + eoa_with_authorization = w3.eth.contract( + abi=wallet_contract.abi, + address=ADDRESS, + ) + + # delegated EOA account can actually have a state + state_after = eoa_with_authorization.functions.state().call() + assert state_after == 1111111 diff --git a/tests/transaction_eip_7702.py b/tests/transaction_eip_7702.py new file mode 100644 index 00000000..3c58a72d --- /dev/null +++ b/tests/transaction_eip_7702.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass, asdict +from typing import Optional + +import rlp +from eth_keys import keys +from eth_typing import HexStr +from eth_utils import to_bytes +from web3 import Web3 + +from tests.rip7560.types import remove_nulls +from tests.types import RPCRequest + + +@dataclass +class TupleEIP7702: + # pylint: disable=invalid-name + chainId: HexStr + address: HexStr + nonce: HexStr + # pylint: disable=invalid-name + yParity: Optional[HexStr] = None + r: Optional[HexStr] = None + s: Optional[HexStr] = None + + def __post_init__(self): + if self.nonce == "0x0": + self.nonce = "0x" + + def sign(self, private_key: str): + pk = keys.PrivateKey(bytes.fromhex(private_key)) + rlp_encode = bytearray( + rlp.encode( + [ + to_bytes(hexstr=self.chainId), + to_bytes(hexstr=self.address), + to_bytes(hexstr=self.nonce), + ] + ) + ) + rlp_encode.insert(0, 5) + rlp_encode_hash = Web3.keccak(hexstr=rlp_encode.hex()) + signature = pk.sign_msg_hash(rlp_encode_hash) + self.yParity = hex(signature.v) + self.r = hex(signature.r) + self.s = hex(signature.s) + + +# pylint: disable=fixme +# TODO: Will we have any tests sending EIP-7702 transactions directly? +# If not, this class can be removed. +@dataclass +class TransactionEIP7702: + # pylint: disable=too-many-instance-attributes, invalid-name + to: HexStr = "0x0000000000000000000000000000000000000000" + data: HexStr = "0x00" + nonce: HexStr = hex(0) + gasLimit: HexStr = hex(1_000_000) # alias for callGasLimit + maxFeePerGas: HexStr = hex(4 * 10 ** 9) + maxPriorityFeePerGas: HexStr = hex(3 * 10 ** 9) + chainId: HexStr = hex(1337) + value: HexStr = hex(0) + # pylint: disable=fixme + accessList: list[HexStr] = () # todo: type is not correct, must always be empty! + authorizationList: list[TupleEIP7702] = () + + # pylint: disable=fixme + # todo: implement + def cleanup(self): + return self + + def send(self, url=None): + return RPCRequest( + method="eth_sendTransaction", params=[remove_nulls(asdict(self.cleanup()))] + ).send(url) diff --git a/tests/types.py b/tests/types.py index bd841e3c..2379a590 100644 --- a/tests/types.py +++ b/tests/types.py @@ -6,10 +6,6 @@ import json import jsonrpcclient import requests -from eth_typing import ( - HexStr, -) -from eth_utils import to_checksum_address @dataclass() @@ -43,54 +39,6 @@ def configure( cls.log_rpc = log_rpc -@dataclass -class UserOperation: - # pylint: disable=too-many-instance-attributes, invalid-name - sender: HexStr - nonce: HexStr = hex(0) - factory: HexStr = None - factoryData: HexStr = None - callData: HexStr = "0x" - callGasLimit: HexStr = hex(3 * 10**5) - verificationGasLimit: HexStr = hex(10**6) - preVerificationGas: HexStr = hex(3 * 10**5) - maxFeePerGas: HexStr = hex(4 * 10**9) - maxPriorityFeePerGas: HexStr = hex(3 * 10**9) - signature: HexStr = "0x" - paymaster: HexStr = None - paymasterData: HexStr = None - paymasterVerificationGasLimit: HexStr = None - paymasterPostOpGasLimit: HexStr = None - - def __post_init__(self): - self.sender = to_checksum_address(self.sender) - self.callData = self.callData.lower() - self.signature = self.signature.lower() - if self.paymaster is not None: - self.paymaster = to_checksum_address(self.paymaster) - if self.paymasterVerificationGasLimit is None: - self.paymasterVerificationGasLimit = hex(10**5) - if self.paymasterPostOpGasLimit is None: - self.paymasterPostOpGasLimit = hex(10**5) - if self.paymasterData is None: - self.paymasterData = "0x" - else: - self.paymasterData = self.paymasterData.lower() - if self.factory is not None: - self.factory = to_checksum_address(self.factory) - if self.factoryData is None: - self.factoryData = "0x" - else: - self.factoryData = self.factoryData.lower() - - def send(self, entrypoint=None, url=None): - if entrypoint is None: - entrypoint = CommandLineArgs.entrypoint - return RPCRequest( - method="eth_sendUserOperation", params=[asdict(self), entrypoint] - ).send(url) - - @dataclass class RPCRequest: method: str diff --git a/tests/user_operation_erc4337.py b/tests/user_operation_erc4337.py new file mode 100644 index 00000000..a96d664a --- /dev/null +++ b/tests/user_operation_erc4337.py @@ -0,0 +1,56 @@ +from dataclasses import asdict, dataclass + +from eth_typing import HexStr +from eth_utils import to_checksum_address + +from tests.transaction_eip_7702 import TupleEIP7702 +from tests.types import RPCRequest, CommandLineArgs + + +@dataclass +class UserOperation: + # pylint: disable=too-many-instance-attributes, invalid-name + sender: HexStr + nonce: HexStr = hex(0) + factory: HexStr = None + factoryData: HexStr = None + callData: HexStr = "0x" + callGasLimit: HexStr = hex(3 * 10**5) + verificationGasLimit: HexStr = hex(10**6) + preVerificationGas: HexStr = hex(3 * 10**5) + maxFeePerGas: HexStr = hex(4 * 10**9) + maxPriorityFeePerGas: HexStr = hex(3 * 10**9) + signature: HexStr = "0x" + paymaster: HexStr = None + paymasterData: HexStr = None + paymasterVerificationGasLimit: HexStr = None + paymasterPostOpGasLimit: HexStr = None + authorizationList: list[TupleEIP7702] = () + + def __post_init__(self): + self.sender = to_checksum_address(self.sender) + self.callData = self.callData.lower() + self.signature = self.signature.lower() + if self.paymaster is not None: + self.paymaster = to_checksum_address(self.paymaster) + if self.paymasterVerificationGasLimit is None: + self.paymasterVerificationGasLimit = hex(10**5) + if self.paymasterPostOpGasLimit is None: + self.paymasterPostOpGasLimit = hex(10**5) + if self.paymasterData is None: + self.paymasterData = "0x" + else: + self.paymasterData = self.paymasterData.lower() + if self.factory is not None: + self.factory = to_checksum_address(self.factory) + if self.factoryData is None: + self.factoryData = "0x" + else: + self.factoryData = self.factoryData.lower() + + def send(self, entrypoint=None, url=None): + if entrypoint is None: + entrypoint = CommandLineArgs.entrypoint + return RPCRequest( + method="eth_sendUserOperation", params=[asdict(self), entrypoint] + ).send(url) diff --git a/tests/utils.py b/tests/utils.py index df6a024b..6c6f7228 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,13 +2,14 @@ import time from functools import cache -from eth_utils import to_checksum_address from eth_abi import decode from eth_abi.packed import encode_packed +from eth_utils import to_checksum_address from solcx import compile_source from .rip7560.types import TransactionRIP7560 -from .types import RPCRequest, UserOperation, CommandLineArgs +from .types import RPCRequest, CommandLineArgs +from .user_operation_erc4337 import UserOperation @cache