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

Supporting ERC4337 #15

Merged
merged 20 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/optimism
/account-abstraction
/__pycache__
/.idea
/venv
Expand All @@ -9,4 +10,5 @@
/op-geth
/opnode_discovery_db
/opnode_peerstore_db
.DS_Store
.DS_Store
/.env
219 changes: 219 additions & 0 deletions bundler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/usr/bin/env python3

import subprocess
import argparse
import os
import libroll as lib
import deps
from processes import PROCESS_MGR
from config import L2Config

####################################################################################################
# CONSTANTS

# Minimum Go version required by the Stackup bundler.
GO_VERSION = "1.19"
eerkaijun marked this conversation as resolved.
Show resolved Hide resolved

####################################################################################################
# ARGUMENT PARSING

# Store the parsed arguments here.
global args

parser = argparse.ArgumentParser(
description="Helps you spin up an op-stack rollup.")

subparsers = parser.add_subparsers(
title="commands",
dest="command",
metavar="<command>")

subparsers.add_parser(
"start",
help="start an ERC4337 bundler")

subparsers.add_parser(
"clean",
help="cleanup bundler processes")

parser.add_argument(
"--no-ansi-esc",
help="disable ANSI escape codes for terminal manipulation",
default=True,
dest="use_ansi_esc",
action="store_false")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be folded into roll.py?
Not really important rn.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an issue for this & link it here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an issue for this and link it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#38


####################################################################################################
# SETUP

def start():
config = L2Config()
setup_4337_contracts(config)
setup_stackup_bundler(config)
setup_paymaster(config)

# --------------------------------------------------------------------------------------------------

def setup_4337_contracts(config: L2Config):
github_url = "https://github.com/0xFableOrg/account-abstraction.git"
norswap marked this conversation as resolved.
Show resolved Hide resolved

if os.path.isfile("account-abstraction"):
raise Exception("Error: 'account-abstraction' exists as a file and not a directory.")
elif not os.path.exists("account-abstraction"):
lib.clone_repo(github_url, "clone the account-abstraction repository")

# If contracts have not been previously deployed
if not os.path.exists("account-abstraction/deployments/opstack"):
log_file = "logs/build_4337_contracts.log"
lib.run_roll_log(
"install account abstraction dependencies",
command=deps.cmd_with_node("yarn install"),
cwd="account-abstraction",
log_file=log_file
)
# set private key for deployment
if config.deployer_key is None:
priv_key = input("Enter private key that you would like to deploy contracts with: ")
config.deployer_key = priv_key
else:
priv_key = config.deployer_key
lib.run("set private key", f"echo PRIVATE_KEY={priv_key} > account-abstraction/.env")
eerkaijun marked this conversation as resolved.
Show resolved Hide resolved
# set rpc url for deployment
lib.run("set rpc url", f"echo RPC_URL={config.l2_engine_rpc} >> account-abstraction/.env")
log_file = "logs/deploy_4337_contracts.log"
lib.run_roll_log(
"deploy contracts",
command=deps.cmd_with_node("yarn deploy --network opstack"),
cwd="account-abstraction",
log_file=log_file
)
print("Account abstraction contracts successfully deployed.")
else:
print("Account abstraction contracts already deployed.")

def setup_stackup_bundler(config: L2Config):
github_url = "github.com/stackup-wallet/stackup-bundler"
version = "latest"

lib.run("installing stackup bundler", f"go install {github_url}@{version}")
print("Installation successful")

# set environment variables for bundler
lib.run(
"set full node RPC",
f"echo ERC4337_BUNDLER_ETH_CLIENT_URL={config.l2_engine_rpc} > .env"
)
if config.bundler_key is None:
priv_key = input("Enter private key for bundler: ")
config.bundler_key = priv_key
else:
priv_key = config.bundler_key
lib.run("set private key", f"echo ERC4337_BUNDLER_PRIVATE_KEY={priv_key} >> .env")

# make sure that GOPATH is set in PATH
current_path = os.environ.get("PATH", "")
gopath = subprocess.check_output(["go", "env", "GOPATH"]).decode().strip()
# append the bin directory of GOPATH to PATH
new_path = f"{gopath}/bin:{current_path}"
os.environ["PATH"] = new_path
eerkaijun marked this conversation as resolved.
Show resolved Hide resolved

# start bundler as a persistent process
print("Starting bundler...")
log_file_path = "logs/stackup_bundler.log"
log_file = open(log_file_path, "w")
PROCESS_MGR.start(
"start bundler",
"stackup-bundler start --mode private",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, what does --mode private do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Means it's a private mempool for the bundler (i.e. only our own bundler will be able to see the transactions sent to this RPC endpoints as they are not broadcasted)

forward="fd",
stdout=log_file,
stderr=subprocess.STDOUT
)
print("Bundler is running!")

def setup_paymaster(config: L2Config):
# install paymaster dependencies
lib.run_roll_log(
"install paymaster dependencies",
command=deps.cmd_with_node("pnpm install"),
cwd="paymaster",
log_file="logs/install_paymaster_dependencies.log"
)

# set environment variables for paymaster (deterministic deployments can be hardcoded)
lib.run(
"set node RPC",
f"echo RPC_URL={config.l2_engine_rpc} > paymaster/.env"
)
lib.run("set paymaster RPC", "echo PAYMASTER_RPC_URL=http://localhost:3000 >> paymaster/.env")
entrypointAddress = lib.read_json_file(
"account-abstraction/deployments/opstack/EntryPoint.json"
)["address"]
lib.run(
"set entrypoint",
f"echo ENTRYPOINT_ADDRESS={entrypointAddress} >> paymaster/.env"
)
simpleAccountFactoryAddress = lib.read_json_file(
"account-abstraction/deployments/opstack/SimpleAccountFactory.json"
)["address"]
lib.run(
"set factory",
f"echo SIMPLE_ACCOUNT_FACTORY_ADDRESS={simpleAccountFactoryAddress} >> paymaster/.env"
)
norswap marked this conversation as resolved.
Show resolved Hide resolved
paymaster_address = subprocess.check_output(
["grep", '==VerifyingPaymaster addr=', "logs/deploy_4337_contracts.log"]
).decode().strip().split(' ')[-1]
lib.run("set paymaster", f"echo PAYMASTER_ADDRESS={paymaster_address} >> paymaster/.env")
# set private key for paymaster
if config.deployer_key is None:
priv_key = input("Enter private key for paymaster signer: ")
config.deployer_key = priv_key
else:
priv_key = config.deployer_key
lib.run("set private key", f"echo PRIVATE_KEY={priv_key} >> paymaster/.env")

# start paymaster signer service
print("Starting paymaster signer service...")
log_file_path = "logs/paymaster_signer.log"
log_file = open(log_file_path, "w")
PROCESS_MGR.start(
"start paymaster",
"pnpm run start",
cwd="paymaster",
forward="fd",
stdout=log_file,
stderr=subprocess.STDOUT
)
print("Paymaster service is running!")

# allow background processes to continue running
PROCESS_MGR.wait_all()
eerkaijun marked this conversation as resolved.
Show resolved Hide resolved

####################################################################################################
# CLEANUP

def clean():
lib.run("cleanup account abstraction directory", "rm -rf account-abstraction/deployments/opstack")
lib.run("cleanup environment variable", "rm .env")
print("Cleanup successful!")
eerkaijun marked this conversation as resolved.
Show resolved Hide resolved

####################################################################################################

if __name__ == "__main__":
args = parser.parse_args()
try:
if args.command is None:
parser.print_help()
exit()

deps.check_basic_prerequisites()
deps.check_go()
if args.command == "start":
start()
elif args.command == "clean":
clean()

print("Done.")
except Exception as e:
print(f"Aborted with error: {e}")

####################################################################################################
17 changes: 17 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,23 @@ def __init__(self):

# NOTE(norswap): The pprof server listens on port 6060 by default.

# ==========================================================================================
# Account Abstraction Configuration

# === Private Key ===

self.deployer_key = None
"""
Private key to use for deploying 4337 contracts and paymaster signature (None by default).
Will be used if set, otherwise will prompt users to enter private key.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it good to always make the deployer the same as the paymaster signature address? Haven't really thought about this in earnest.

I know people like to talk about "secure signers" as option for holding live keys (e.g. paymaster signer) and it might be a bit of a pain to use such a signer for deployment? Is that a good reason to separate them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good question, let's revisit it: #40


self.bundler_key = None
"""
Private key to use for submitting bundled transactions (None by default).
Will be used if set, otherwise will prompt users to enter private key.
"""

# ==============================================================================================
# Updating / Altering the Configuration

Expand Down
1 change: 1 addition & 0 deletions l1.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ def start_devnet_l1_node(paths: OPPaths):
f"--networkid={cfg.chain_id}",
"--syncmode=full", # doesn't matter, it's only us
"--gcmode=archive",
"--rpc.allow-unprotected-txs", # allow legacy transactions for deterministic deployment

# No peers: the blockchain is only this node
"--nodiscover",
Expand Down
9 changes: 9 additions & 0 deletions libroll.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,15 @@ def append_to_path(path: str):
"""
os.environ["PATH"] = f"{os.environ['PATH']}{os.pathsep}{os.path.abspath(path)}"

####################################################################################################

def clone_repo(url: str, descr: str):
"""
Clone a git repository
"""
run(descr, f"git clone {url}")
print(f"Succeeded: {descr}")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you extend the usage of this to other parts of the codebase where we clone? Or add an issue for it is equally good!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already done :)


####################################################################################################

Expand Down
3 changes: 3 additions & 0 deletions paymaster/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
.env
23 changes: 23 additions & 0 deletions paymaster/README.md
eerkaijun marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Paymaster

This is a barebone verifying paymaster server. It is meant to be used in conjuction with the `VerifyingPaymaster` [contract](https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/samples/VerifyingPaymaster.sol) by ETH Infinitism.

The flow for sponsoring a user's transaction as follows:
1. Users send a JSON RPC request to the paymaster server with the corresponding UserOp.
2. The paymaster server signs the hash of the UserOp if it decides to sponsor the transaction. The signature is encoded in the `paymasterAndData` field of the userOp.
3. The paymaster server sends the updated UserOp back to the user.
4. The user can now send the UserOp to the bundler and the transaction will be sponsored by the paymaster.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, there is no mempool involved!

This is good, below is just me spitballing on what how I thought this would be + how it could be:

Is there a way in which the paymaster could submit the transaction to the bundler directly? I guess the updated userop needs to be signed by the user right?

Before seeing this, my very naive flow assumption was:

  1. Users submit userOp to mempool (functionally equivalent, as its a RPC call, just that here we don't put it in the mempool)
  2. Batchers pick userOp, check if they can be sponsored (using the whole 4337 dance) and if so submit them.
  3. The sponsorship check is on-chain, the paymaster contract basically always sponsors non-reverting tx (is it even possible to check this in 4337???) to whitelisted contracts. Could also involve rate limiting (though update usage would have to live in the sponsored tx, NOT in the validation logic which can't write state).

I think the ideal flow for what we're doing would be to:

  1. Send to server that is both a paymaster & bundler. (Optionally drop to mempool, no need for now.)
  2. Sign userOp, then wrap in a tx that
    • Writes to the paymaster contract, authorizing userOp
    • Submits the userOp to the entrypoint contract (paymaster validation then checks the authorization we just wrote)

This would be an ideal one-request flow from the user's perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean users send the signed UserOp to paymaster, and the paymaster submit to the entrypoint contract directly?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly!


The encoding of `paymaster` as follows:
* First 20 bytes: paymaster smart contract address
* Next 64 bytes: 32 bytes each for `validUntil` and `validAfter`, these are `uint48` values for the time validity of the paymaster signature
* Final 65 bytes: the signature of the paymaster server on the hash of the UserOp


### Adding custom sponsor logic for paymaster

Developers could add custom logic to define what type of transactions to sponsor in the `src/rpcMethods.ts` file.

### Running tests

Once the bundler and paymaster server are running, run the tests with `pnpm run test`.
25 changes: 25 additions & 0 deletions paymaster/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "paymaster",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "tsc && node dist/src/server.js",
"test": "npx ts-node test/tests.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@ethersproject/bytes": "^5.7.0",
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.17",
"@types/node": "^20.5.7",
"axios": "^1.5.0",
"body-parser": "^1.20.2",
"dotenv": "^16.3.1",
"ethers": "^6.7.1",
"express": "^4.18.2",
"typescript": "^5.2.2"
}
}
Loading