Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AA-453: Implement initial EIP-7702 UserOperation test #120

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
95 changes: 90 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,100 @@ 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: "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: "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: rm -rf node_modules/ yarn.lock && yarn install --ignore-engines && yarn preprocess && rm -rf ./node_modules/@ethereumjs/common/node_modules/@ethereumjs/util
- 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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from solcx import install_solc
from web3 import Web3
from web3.middleware import geth_poa_middleware
from .types import UserOperation, RPCRequest, CommandLineArgs
from .types import RPCRequest, CommandLineArgs
from .user_operation_erc4337 import UserOperation
from .utils import (
assert_ok,
deploy_and_deposit,
Expand Down
82 changes: 82 additions & 0 deletions tests/eip7702/test_eip7702_tuple_userop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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}
)

# bug workaround
send_tx_waste_nonce(w3)

# 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)

print(auth_tuple)

userop.sender = ADDRESS
userop.authorizationList = [auth_tuple]

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)

# force waiting for a receipt and a new block
send_tx_waste_nonce(w3)

# it is weird but seems like it does not refresh immediately, how could it be?
sender_code = w3.eth.get_code(ADDRESS)

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,
)
state_after = eoa_with_authorization.functions.state().call()
assert state_after == 1111111
# w3.eth.send_transaction(
# {"from": w3.eth.accounts[0], "to": "0x2ceB5e5999417babAcdBA0C3DFC501989A53888D", "value": 10**18}
# )


# TODO: remove this after 0 nonce bug in ethereumjs/tx library is fixed
def send_tx_waste_nonce(w3):
signed = w3.eth.account.sign_transaction(
{
"to": "0x0000000000000000000000000000000000000000",
"nonce": w3.eth.get_transaction_count(ADDRESS),
"gasPrice": 1000000,
"gas": 21000,
},
PRIVATE_KEY,
)
res = w3.eth.send_raw_transaction(signed.raw_transaction)
rec = w3.eth.wait_for_transaction_receipt(res)
print(res, rec)
72 changes: 72 additions & 0 deletions tests/transaction_eip_7702.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 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())
print(rlp_encode.hex())
print(rlp_encode_hash.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)
52 changes: 0 additions & 52 deletions tests/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
import json
import jsonrpcclient
import requests
from eth_typing import (
HexStr,
)
from eth_utils import to_checksum_address


@dataclass()
Expand Down Expand Up @@ -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
Expand Down
Loading