-
Notifications
You must be signed in to change notification settings - Fork 228
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
base: master
Are you sure you want to change the base?
Conversation
.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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is temporary, no?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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]: { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
9271c17
to
ae02267
Compare
b7d163f
to
2055b72
Compare
82261b7
to
9df986b
Compare
23017c9
to
c5eebfa
Compare
51b8e19
to
56482d1
Compare
// 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'])]) |
There was a problem hiding this comment.
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.
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) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- need to deploy it, at least once
- it loses typechecking. I return an array of structs.
packages/utils/contracts/Imports.sol
Outdated
|
||
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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
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 + ' ') | ||
} | ||
} | ||
|
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, what??
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh?..
a84afcc
to
c85351a
Compare
using different compiler settings. also, sanity-check addresses
4d022d8
to
1e3b92e
Compare
@@ -1,5 +1,4 @@ | |||
{ | |||
"version": "0.6.0", | |||
"npmClient": "yarn", | |||
"useWorkspaces": true |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
using handleOps (not EntryPointSimulations) for UserOp validation.