Skip to content

Commit

Permalink
Changin usedOpcodes types
Browse files Browse the repository at this point in the history
  • Loading branch information
shahafn committed Dec 19, 2024
1 parent 0c899b2 commit 56a1598
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 139 deletions.
166 changes: 34 additions & 132 deletions packages/validation-manager/src/TracerResultParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@
// where xxx is OP/STO/COD/EP/SREP/EREP/UREP/ALT, and ### is a number
// the validation rules are defined in erc-aa-validation.md
import Debug from 'debug'
import { BigNumber, BigNumberish } from 'ethers'
import { hexZeroPad, Interface, keccak256 } from 'ethers/lib/utils'
import { BigNumber } from 'ethers'
import { hexZeroPad, keccak256 } from 'ethers/lib/utils'
import { inspect } from 'util'

import { BundlerTracerResult, MethodInfo, TopLevelCallInfo } from './BundlerCollectorTracer'
import { AccessInfo, BundlerTracerResult, MethodInfo, TopLevelCallInfo } from './BundlerCollectorTracer'
import {
IEntryPoint__factory,
IPaymaster__factory,
OperationBase,
RpcError,
SenderCreator__factory,
StakeInfo,
StorageMap,
ValidationErrors,
Expand All @@ -25,102 +22,6 @@ import { ValidationResult } from './IValidationManager'

const debug = Debug('aa.handler.opcodes')

interface CallEntry {
to: string
from: string
type: string // call opcode
method: string // parsed method, or signash if unparsed
revert?: any // parsed output from REVERT
return?: any // parsed method output.
value?: BigNumberish
}

const abi = Object.values([
...SenderCreator__factory.abi,
...IEntryPoint__factory.abi,
...IPaymaster__factory.abi
].reduce((set, entry) => {
const key = `${entry.name}(${entry.inputs.map(i => i.type).join(',')})`
// console.log('key=', key, keccak256(Buffer.from(key)).slice(0,10))
return {
...set,
[key]: entry
}
}, {})) as any

/**
* parse all call operation in the trace.
* notes:
* - entries are ordered by the return (so nested call appears before its outer call
* - last entry is top-level return from "simulateValidation". it as ret and rettype, but no type or address
* @param tracerResults
*/
function parseCallStack (
tracerResults: BundlerTracerResult
): CallEntry[] {
const xfaces = new Interface(abi)

function callCatch<T, T1> (x: () => T, def: T1): T | T1 {
try {
return x()
} catch {
return def
}
}

const out: CallEntry[] = []
const stack: any[] = []
tracerResults.calls
.filter(x => !x.type.startsWith('depth'))
.forEach(c => {
if (c.type.match(/REVERT|RETURN/) != null) {
const top = stack.splice(-1)[0] ?? {
type: 'top',
method: 'validateUserOp'
}
const returnData: string = (c as any).data
if (top.type.match(/CREATE/) != null) {
out.push({
to: top.to,
from: top.from,
type: top.type,
method: '',
return: `len=${returnData.length}`
})
} else {
const method = callCatch(() => xfaces.getFunction(top.method), top.method)
if (c.type === 'REVERT') {
const parsedError = callCatch(() => xfaces.parseError(returnData), returnData)
out.push({
to: top.to,
from: top.from,
type: top.type,
method: method.name,
value: top.value,
revert: parsedError
})
} else {
const ret = callCatch(() => xfaces.decodeFunctionResult(method, returnData), returnData)
out.push({
to: top.to,
from: top.from,
type: top.type,
value: top.value,
method: method.name ?? method,
return: ret
})
}
}
} else {
stack.push(c)
}
})

// TODO: verify that stack is empty at the end.

return out
}

/**
* slots associated with each entity.
* keccak( A || ...) is associated with "A"
Expand Down Expand Up @@ -367,43 +268,15 @@ function processEntityCall (entityCall: TopLevelCallInfo, entityAddress: string,
const readWrite = isWrite ? 'write to' : 'read from'
const transientStr = isTransient ? 'transient ' : ''
requireCond(false,
`${entityTitle} has forbidden ${readWrite} ${transientStr}${nameAddr(addr, entityTitle)} slot ${slot}`,
`${entityTitle} has forbidden ${readWrite} ${transientStr}${nameAddr(addr, stakeInfoEntities)} slot ${slot}`,
ValidationErrors.OpcodeValidation, { [entityTitle]: entStakes?.addr })
}
})

// if addr is current account/paymaster/factory, then return that title
// otherwise, return addr as-is
function nameAddr (addr: string, currentEntity: string): string {
const [title] = Object.entries(stakeInfoEntities).find(([title, info]) =>
info?.addr.toLowerCase() === addr.toLowerCase()) ?? []

return title ?? addr
}

requireCondAndStake(requireStakeSlot != null, entStakes,
`unstaked ${entityTitle} accessed ${nameAddr(addr, entityTitle)} slot ${requireStakeSlot}`)
`unstaked ${entityTitle} accessed ${nameAddr(addr, stakeInfoEntities)} slot ${requireStakeSlot}`, entityTitle, access)
})

// check if the given entity is staked
function isStaked (entStake?: StakeInfo): boolean {
return entStake != null && BigNumber.from(1).lte(entStake.stake) && BigNumber.from(1).lte(entStake.unstakeDelaySec)
}

// helper method: if condition is true, then entity must be staked.
function requireCondAndStake (cond: boolean, entStake: StakeInfo | undefined, failureMessage: string): void {
if (!cond) {
return
}
if (entStake == null) {
throw new Error(`internal: ${entityTitle} not in userOp, but has storage accesses in ${JSON.stringify(access)}`)
}
requireCond(isStaked(entStake),
failureMessage, ValidationErrors.OpcodeValidation, { [entityTitle]: entStakes?.addr })

// TODO: check real minimum stake values
}

// the only contract we allow to access before its deployment is the "sender" itself, which gets created.
let illegalZeroCodeAccess: any
for (const addr of Object.keys(entityCall.contractSize)) {
Expand Down Expand Up @@ -432,13 +305,42 @@ function processEntityCall (entityCall: TopLevelCallInfo, entityAddress: string,
`${entityTitle} accesses EntryPoint contract address ${entryPointAddress} with opcode ${illegalEntryPointCodeAccess}`,
ValidationErrors.OpcodeValidation)

// Recursively handling all subcalls to check validation rules
if (entityCall.calls != null) {
entityCall.calls.forEach((call: any) => {
processEntityCall(call, entityAddress, entityTitle, entStakes, entitySlots, userOp, stakeInfoEntities, entryPointAddress, tracerResults)
})
}
}

// helper method: if condition is true, then entity must be staked.
function requireCondAndStake (cond: boolean, entStake: StakeInfo | undefined, failureMessage: string, entityTitle: string, access: { [address: string]: AccessInfo }): void {
if (!cond) {
return
}
if (entStake == null) {
throw new Error(`internal: ${entityTitle} not in userOp, but has storage accesses in ${JSON.stringify(access)}`)
}
requireCond(isStaked(entStake),
failureMessage, ValidationErrors.OpcodeValidation, { [entityTitle]: entStake?.addr })

// TODO: check real minimum stake values
}

// check if the given entity is staked
function isStaked (entStake?: StakeInfo): boolean {
return entStake != null && BigNumber.from(1).lte(entStake.stake) && BigNumber.from(1).lte(entStake.unstakeDelaySec)
}

// if addr is current account/paymaster/factory, then return that title
// otherwise, return addr as-is
function nameAddr (addr: string, stakeInfoEntities: {[addr: string]: StakeInfo}): string {
const [title] = Object.entries(stakeInfoEntities).find(([title, info]) =>
info?.addr.toLowerCase() === addr.toLowerCase()) ?? []

return title ?? addr
}

// return true if the given slot is associated with the given address, given the known keccak operations:
// @param slot the SLOAD/SSTORE slot address we're testing
// @param addr - the address we try to check for association with
Expand Down
13 changes: 6 additions & 7 deletions packages/validation-manager/src/ValidationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ export class ValidationManager implements IValidationManager {
}
}, this.providerForTracer)


// const lastResult = tracerResult.calls.slice(-1)[0]
// const data = (lastResult as ExitInfo).data
const data = tracerResult.output
Expand Down Expand Up @@ -221,9 +220,10 @@ export class ValidationManager implements IValidationManager {
[res, tracerResult] = await this._geth_traceCall_SimulateValidation(userOp).catch(e => {
throw e
})
// console.log('wtf validation res', res)
// console.log('validation res', res)
// todo fix
this.convertTracerResult(tracerResult, userOp)
// console.log('tracer res')
// console.dir(tracerResult, { depth: null })
let contractAddresses: string[]
[contractAddresses, storageMap] = tracerResultParser(userOp, tracerResult, res, this.entryPoint.address)
Expand Down Expand Up @@ -351,7 +351,7 @@ export class ValidationManager implements IValidationManager {
call.opcodes = {}
if (call.usedOpcodes != null) {
Object.keys(call.usedOpcodes).forEach((opcode: string) => {
call.opcodes[this.getOpcodeName(parseInt(opcode))] = 1
call.opcodes[this.getOpcodeName(parseInt(opcode))] = call.usedOpcodes[opcode]
})
}

Expand Down Expand Up @@ -388,7 +388,7 @@ export class ValidationManager implements IValidationManager {
if (call.input.length <= 2) {
call.method = '0x'
} else {
const abi = Object.values([
const mergedAbi = Object.values([
...SenderCreator__factory.abi,
...IEntryPoint__factory.abi,
...IPaymaster__factory.abi
Expand All @@ -400,7 +400,7 @@ export class ValidationManager implements IValidationManager {
[key]: entry
}
}, {})) as any
const xfaces = new Interface(abi)
const AbiInterfaces = new Interface(mergedAbi)

function callCatch<T, T1> (x: () => T, def: T1): T | T1 {
try {
Expand All @@ -410,7 +410,7 @@ export class ValidationManager implements IValidationManager {
}
}
const methodSig = call.input.slice(0, 10)
const method = callCatch(() => xfaces.getFunction(methodSig), methodSig)
const method = callCatch(() => AbiInterfaces.getFunction(methodSig), methodSig)
call.method = method.name
}
}
Expand All @@ -419,7 +419,6 @@ export class ValidationManager implements IValidationManager {
// requires a change of this address.
// TODO check why the filter fails test_ban_user_op_access_other_ops_sender_in_bundle
tracerResult.callsFromEntryPoint = tracerResult.calls // .filter((call: { from: string }) => call.from.toLowerCase() === this.entryPoint.address.toLowerCase() || call.from.toLowerCase() === SENDER_CREATOR)
// console.log('wtf calls NOT from EntryPoint', tracerResult.calls.filter((call: { from: string }) => !(call.from.toLowerCase() === this.entryPoint.address.toLowerCase() || call.from.toLowerCase() === SENDER_CREATOR)))

return tracerResult
}
Expand Down

0 comments on commit 56a1598

Please sign in to comment.