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-440 validate using handleOps #243

Open
wants to merge 27 commits into
base: master
Choose a base branch
from

Conversation

drortirosh
Copy link
Contributor

using handleOps (not EntryPointSimulations) for UserOp validation.

.gitmodules Outdated
@@ -1,7 +1,7 @@
[submodule "submodules/account-abstraction"]
path = submodules/account-abstraction
url = https://github.com/eth-infinitism/account-abstraction.git
branch = releases/v0.7
branch = callValidateUserOp-catch-revert
Copy link
Contributor

Choose a reason for hiding this comment

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

This is temporary, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, though the new value will be "develop", until we complete releases/v0.8

@@ -29,9 +28,14 @@ describe('BundleServer', function () {
let entryPoint: IEntryPoint
let server: BundlerServer
before(async () => {
const provider = ethers.provider
// const provider = ethers.provider
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

try {
entryPoint = await deployEntryPoint(provider)
} catch (e) {
throw new Error('Failed to deploy entry point - no geth?\n' + e)
Copy link
Contributor

Choose a reason for hiding this comment

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

Geth or just a node?

// standard eth_call to simulateValidation
async _callSimulateValidation (userOp: UserOperation): Promise<ValidationResult> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove the comment above and rename

[this.entryPoint.address]: {
code: EntryPointSimulationsJson.deployedBytecode
},
// [this.entryPoint.address]: {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

@@ -194,9 +252,10 @@ export class ValidationManager implements IValidationManager {
// return [data as any, tracerResult]
// }
try {
const [decodedSimulations] = entryPointSimulations.decodeFunctionResult('simulateValidation', data)
const validationResult = this.parseValidationResult(userOp, decodedSimulations)
// const [decodedSimulations] = entryPointSimulations.decodeFunctionResult('simulateValidation', data)
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

@@ -110,27 +148,40 @@ export class ValidationManager implements IValidationManager {
}
}

allOnes = '0x'.padEnd(66, 'f')

// a "flag" UserOperation that triggers "AA94" revert error (not even FailedOp)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain the rationale? That we inject a reverting userOp to avoid simulating the execution, and explicitly mention that revert on the eolUserOp means success?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the handleOps validate the userop before calling it.
Thus FailedOp(index=1, "AA94") means it is an invalid UserOp
is it better to have a revert inside validateUserOp ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I need more context to understand what is going on here.

@drortirosh drortirosh force-pushed the AA-440-validate-without-simulateValidation branch from 9271c17 to ae02267 Compare February 3, 2025 15:29
@drortirosh drortirosh force-pushed the AA-440-validate-without-simulateValidation branch from b7d163f to 2055b72 Compare February 4, 2025 16:24
@drortirosh drortirosh force-pushed the AA-440-validate-without-simulateValidation branch from 82261b7 to 9df986b Compare February 4, 2025 17:44
@drortirosh drortirosh force-pushed the AA-440-validate-without-simulateValidation branch from 23017c9 to c5eebfa Compare February 4, 2025 18:43
@drortirosh drortirosh force-pushed the AA-440-validate-without-simulateValidation branch from 51b8e19 to 56482d1 Compare February 5, 2025 11:13
// this method attempts to be generic
const retAddr = await this.provider.call({
to: factory, data: factoryData
const getSenderAddressData = this.entryPointView.interface.encodeFunctionData('getSenderAddress', [hexConcat([factory, factoryData ?? '0x'])])
Copy link
Contributor

@forshtat forshtat Feb 13, 2025

Choose a reason for hiding this comment

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

Why don't we use to the factory contract object here? Manually encoding and decoding this data is not pretty.

Comment on lines +104 to +106
const privateKey = (this.owner as any).privateKey
const sig = ecsign(Buffer.from(arrayify(userOpHash)), Buffer.from(arrayify(privateKey)))
return toRpcSig(sig.v, sig.r, sig.s)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this change made and how is it related to this issue?


error StakesRet(StakeInfo[] stakes);

// helper: get stake info of multiple entities.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we use OpenZeppelin's Multicall contract here instead of reinventing the wheel?
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Multicall.sol

Copy link
Contributor Author

Choose a reason for hiding this comment

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

multicall has to be deployed, and only then can be called.
This contract never gets deployed (in fact, it can't, since it reverts in its constructor)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but also this is a bit of reinventing the wheel. Maybe we should just give in and deploy the Multicall the same way we deploy EntryPoint? On real networks with real bundlers, it will just be an address config parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, we could use multicall, but:

  1. need to deploy it, at least once
  2. it loses typechecking. I return an array of structs.


import "@account-abstraction/contracts/core/EntryPointSimulations.sol";
import "@account-abstraction/contracts/interfaces/IStakeManager.sol";
import "@account-abstraction/contracts/samples/SimpleAccountFactory.sol";
import "@account-abstraction/contracts/samples/TokenPaymaster.sol";
//import "@account-abstraction/contracts/samples/TokenPaymaster.sol";
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

Comment on lines 369 to 397
private mapAddrToName (mapAddrs: {[name: string]: string}, addr: string): string {
if (addr == null) {
return addr
}
for (const name of Object.keys(mapAddrs)) {
if (mapAddrs[name]?.toString().toLowerCase() === addr.toLowerCase()) {
return name
}
}
return addr
}

// recursively dump call tree, and storage accesses
dumpCallTree (call: ERC7562Call, mapAddrs = {}, indent = ''): void {
const map = (addr: string): string => this.mapAddrToName(mapAddrs, addr)
debug(`${indent} ${map(call.from)} => ${call.type} ${call.to} ${map(call.to)}.${this._tryDetectKnownMethod(call)}`)
for (const access of ['reads', 'writes']) {
const arr = (call.accessedSlots as any)[access]
if (arr != null) {
for (const [idx, val] of Object.entries(arr)) {
debug(`${indent} - ${access} ${idx}: ${val as string}`)
}
}
}
for (const innerCall of call.calls ?? []) {
this.dumpCallTree(innerCall, mapAddrs, indent + ' ')
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

This code is basically a utility function and should not be added to the ERC7562Parser class itself.

I suggest extracting these two functions, as well as tryDetectKnownMethod, into Utils.
Also, how is this related to AA-440 ?


const ret: ValidationResult = {
returnInfo: {
sigFailed: false, // can't fail here, since handleOps didn't revert.
Copy link
Contributor

Choose a reason for hiding this comment

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

since handleOps didn't revert

Is it for sure? I was under impression that the debug_traceCall will return correctly with the trace results even if everything was reverted in the end. I may be wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

simulateValidation returns the validation result, even on failure.
since we use handleOps, it doesn't continue after a signature error - it reverts...

const paymasterAddress = op.paymaster ?? AddressZero
const factoryAddress = op.factory ?? AddressZero
const addrs = [op.sender, paymasterAddress, factoryAddress, aggregator]
const retStakes = (await runContractScript(this.provider, new GetStakes__factory(), [this.entryPoint.address, addrs]))[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, what??

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 the usage of GetStake. will refactor.

paymasterInfo: fillEntity(userOp.paymaster, res.paymasterInfo),
factoryInfo: fillEntity(userOp.factory, res.factoryInfo),
aggregatorInfo: fillEntity(res.aggregatorInfo.aggregator, res.aggregatorInfo.stakeInfo)
if (op.paymaster !== null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use != instead of !== maybe, to catch undefined and false ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed (though linter will report it when we're done.

@@ -110,27 +148,40 @@ export class ValidationManager implements IValidationManager {
}
}

allOnes = '0x'.padEnd(66, 'f')

// a "flag" UserOperation that triggers "AA94" revert error (not even FailedOp)
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I need more context to understand what is going on here.

}

if (decodedError.startsWith('FailedOp(1,"AA94 gas values overflow')) {
// this is not an error.. it is a marker the UserOp-under-test passed successfully
Copy link
Contributor

Choose a reason for hiding this comment

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

Huh?..

@drortirosh drortirosh force-pushed the AA-440-validate-without-simulateValidation branch from a84afcc to c85351a Compare February 19, 2025 15:56
using different compiler settings.
also, sanity-check addresses
@drortirosh drortirosh force-pushed the AA-440-validate-without-simulateValidation branch from 4d022d8 to 1e3b92e Compare February 19, 2025 18:20
@@ -1,5 +1,4 @@
{
"version": "0.6.0",
"npmClient": "yarn",
"useWorkspaces": true
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we use lerna workspaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lerna/lerna#3695
lerna removed the useWorkspaces, and makes it the default.

await deployNonceManager(provider, wallet as any)
await deployStakeManager(provider, wallet as any)
const nonceManager = await deployNonceManager(provider, wallet as any)
if (nonceManager.address !== AA_NONCE_MANAGER) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use a util function or at lest do .toLowerCase() consistently when comparing addresses - the amount of time I wasted on these kinds of bugs is just sad.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok for now, but I think its better to make one-pass to return always toChecksum addresses, and then just compare - instead of little the code with huge amount of toLowerCase, which makes it far less readable..

@@ -166,6 +177,13 @@ export async function runBundler (argv: string[], overrideExit = true): Promise<
const {
entryPoint
} = await connectContracts(wallet, !config.rip7560)

if (entryPoint != null && entryPoint?.address !== config.entryPoint && [1337, 31337].includes(chainId)) {
console.log('NOTICE: overriding config entrypoint: ', { entryPoint: entryPoint.address })
Copy link
Contributor

Choose a reason for hiding this comment

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

Please console.warn it - seems like something you don't actually want most of the time.


error StakesRet(StakeInfo[] stakes);

// helper: get stake info of multiple entities.
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but also this is a bit of reinventing the wheel. Maybe we should just give in and deploy the Multicall the same way we deploy EntryPoint? On real networks with real bundlers, it will just be an address config parameter.

// generate validation result from trace: by decoding inner calls.
async generateValidationResult (op: UserOperation, tracerResult: ERC7562Call): Promise<ValidationResult> {
// const validationData = tracerResult.calls[0].output
const validateUserOpCallIndex = op.factory == null ? 0 : 1
Copy link
Contributor

Choose a reason for hiding this comment

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

I would like to recommend that you add another utility function that turns the tracerResult.calls array to a named object like:

{
  senderCall: {},
  paymasterCall: {},
  factoryCall: null
}

It is taxing for the brain to deal with the code like const validateUserOpCallIndex = op.factory == null ? 0 : 1; const validatePaymasterCallIndex = validateUserOpCallIndex + 1, what do you think?

async _callSimulateValidation (userOp: UserOperation): Promise<ValidationResult> {
// Promise<IEntryPointSimulations.ValidationResultStructOutput> {
const data = entryPointSimulations.encodeFunctionData('simulateValidation', [packUserOp(userOp)])
async _simulateHandleOps (userOp: UserOperation): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe don't call it "simulate" if there is still a contract called "simulation" but this contract is not used in this function?

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 part of the code that will get away when removing unsafe.

@@ -288,7 +340,23 @@ export class ValidationManager implements IValidationManager {
}
} else {
// NOTE: this mode doesn't do any opcode checking and no stake checking!
res = await this._callSimulateValidation(userOp)
// can't decode validationResult at all. we only have "pass-or-not" result
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need the "unsafe" mode? The entire reason we keep this bundler repo maintained is as a test stub for the validation rules, isn't that so?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unsafe was for internal testing. and actually, now with "handleOps" validation, unsafe can't be fully implemented (one of the reason to use simulateValidation was to expose inner fields that are only accessible via tracing).
I suggest removing it in a separte PR.


const debug = Debug('aa.dump')

export function base64Tohex (input: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we the ones who encoded the data in base64 in native 7562 tracer in the first place, or does it come from go-ethereum developers?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants