AA-247: Implement tracer (#11)

* WIP: bring in old 4337 Bundler Collector Tracer code (compiles)

* Fix runtime errors (WIP)

* Adding missing fields, using OnEnter hook

* Fix using [0:20] bytes of "PaymasterData" instead of the new "Paymaster" field

* Fix banned opcode checks for Factory frame

* Add 'nil' check

---------

Co-authored-by: shahafn <shahaflol@gmail.com>
This commit is contained in:
Alex Forshtat 2024-07-17 23:10:15 +02:00 committed by GitHub
parent 2312c4fdde
commit 71c013f9d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 289 additions and 24 deletions

View file

@ -145,8 +145,8 @@ func BuyGasRip7560Transaction(st *types.Rip7560AccountAbstractionTx, state vm.St
chargeFrom := st.Sender chargeFrom := st.Sender
if st.Paymaster != nil { if st.Paymaster != nil && st.Paymaster.Cmp(common.Address{}) != 0 {
chargeFrom = st.Paymaster chargeFrom = *st.Paymaster
} }
if have, want := state.GetBalance(*chargeFrom), balanceCheck; have.Cmp(want) < 0 { if have, want := state.GetBalance(*chargeFrom), balanceCheck; have.Cmp(want) < 0 {
@ -183,6 +183,11 @@ func ApplyRip7560ValidationPhases(chainConfig *params.ChainConfig, bc ChainConte
GasPrice: tx.GasFeeCap(), GasPrice: tx.GasFeeCap(),
} }
evm := vm.NewEVM(blockContext, txContext, statedb, chainConfig, cfg) evm := vm.NewEVM(blockContext, txContext, statedb, chainConfig, cfg)
if evm.Config.Tracer != nil && evm.Config.Tracer.OnTxStart != nil {
evm.Config.Tracer.OnTxStart(evm.GetVMContext(), tx, common.Address{})
}
/*** Deployer Frame ***/ /*** Deployer Frame ***/
deployerMsg := prepareDeployerMessage(tx, chainConfig) deployerMsg := prepareDeployerMessage(tx, chainConfig)
var deploymentUsedGas uint64 var deploymentUsedGas uint64
@ -202,7 +207,6 @@ func ApplyRip7560ValidationPhases(chainConfig *params.ChainConfig, bc ChainConte
err = errors.New("sender not deployed") err = errors.New("sender not deployed")
} }
if err != nil { if err != nil {
// TODO: bubble up the inner error message to the user, if possible
return nil, fmt.Errorf("account deployment failed: %v", err) return nil, fmt.Errorf("account deployment failed: %v", err)
} }
statedb.IntermediateRoot(true) statedb.IntermediateRoot(true)
@ -348,7 +352,7 @@ func ApplyRip7560ExecutionPhase(config *params.ChainConfig, vpr *ValidationPhase
func prepareDeployerMessage(baseTx *types.Transaction, config *params.ChainConfig) *Message { func prepareDeployerMessage(baseTx *types.Transaction, config *params.ChainConfig) *Message {
tx := baseTx.Rip7560TransactionData() tx := baseTx.Rip7560TransactionData()
if tx.Deployer == nil { if tx.Deployer == nil || tx.Deployer.Cmp(common.Address{}) == 0 {
return nil return nil
} }
return &Message{ return &Message{
@ -395,7 +399,7 @@ func prepareAccountValidationMessage(baseTx *types.Transaction, chainConfig *par
func preparePaymasterValidationMessage(baseTx *types.Transaction, config *params.ChainConfig, signingHash common.Hash) (*Message, error) { func preparePaymasterValidationMessage(baseTx *types.Transaction, config *params.ChainConfig, signingHash common.Hash) (*Message, error) {
tx := baseTx.Rip7560TransactionData() tx := baseTx.Rip7560TransactionData()
if tx.Paymaster == nil { if tx.Paymaster == nil || tx.Paymaster.Cmp(common.Address{}) == 0 {
return nil, nil return nil, nil
} }
jsondata := `[ jsondata := `[

View file

@ -2,18 +2,54 @@ package native
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/eth/tracers"
"github.com/holiman/uint256"
"math/big"
"regexp"
"strings"
) )
func init() { func init() {
tracers.DefaultDirectory.Register("rip7560Validation", newRip7560Tracer, false) tracers.DefaultDirectory.Register("rip7560Validation", newRip7560Tracer, false)
} }
/******* taken from ERC-4337 bundler collector tracer *******/
type partialStack = []*uint256.Int
type lastThreeOpCodesItem struct {
Opcode string
StackTop3 partialStack
}
type contractSizeVal struct {
ContractSize int `json:"contractSize"`
Opcode string `json:"opcode"`
}
type access struct {
Reads map[string]string `json:"reads"`
Writes map[string]uint64 `json:"writes"`
}
// note - this means an individual 'frame' in 7560 (validate, execute, postOp)
type entryPointCall struct {
//TopLevelMethodSig hexutil.Bytes `json:"topLevelMethodSig"`
TopLevelTargetAddress common.Address `json:"topLevelTargetAddress"`
Access map[common.Address]*access `json:"access"`
Opcodes map[string]uint64 `json:"opcodes"`
ExtCodeAccessInfo map[common.Address]string `json:"extCodeAccessInfo"`
ContractSize map[common.Address]*contractSizeVal `json:"contractSize"`
OOG bool `json:"oog"`
}
/******* *******/
const ValidationFramesMaxCount = 3 const ValidationFramesMaxCount = 3
func newRip7560Tracer(ctx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) { func newRip7560Tracer(ctx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) {
@ -23,14 +59,31 @@ func newRip7560Tracer(ctx *tracers.Context, cfg json.RawMessage) (*tracers.Trace
return nil, err return nil, err
} }
} }
allowedOpcodeRegex, err := regexp.Compile(
`^(DUP\d+|PUSH\d+|SWAP\d+|POP|ADD|SUB|MUL|DIV|EQ|LTE?|S?GTE?|SLT|SH[LR]|AND|OR|NOT|ISZERO)$`,
)
if err != nil {
return nil, err
}
// TODO FIX mock fields
t := &rip7560ValidationTracer{ t := &rip7560ValidationTracer{
TraceResults: make([]stateMap, ValidationFramesMaxCount), TraceResults: make([]stateMap, ValidationFramesMaxCount),
UsedOpcodes: make([]map[string]bool, ValidationFramesMaxCount), UsedOpcodes: make([]map[string]bool, ValidationFramesMaxCount),
Created: make([]map[common.Address]bool, ValidationFramesMaxCount), Created: make([]map[common.Address]bool, ValidationFramesMaxCount),
Deleted: make([]map[common.Address]bool, ValidationFramesMaxCount), //Deleted: make([]map[common.Address]bool, ValidationFramesMaxCount),
allowedOpcodeRegex: allowedOpcodeRegex,
lastThreeOpCodes: make([]*lastThreeOpCodesItem, 0),
CurrentLevel: nil,
lastOp: "",
Calls: make([]*callsItem, 0),
Keccak: make([]hexutil.Bytes, 0),
Logs: make([]*logsItem, 0),
} }
return &tracers.Tracer{ return &tracers.Tracer{
Hooks: &tracing.Hooks{ Hooks: &tracing.Hooks{
OnEnter: t.OnEnter,
OnTxStart: t.OnTxStart, OnTxStart: t.OnTxStart,
OnTxEnd: t.OnTxEnd, OnTxEnd: t.OnTxEnd,
OnOpcode: t.OnOpcode, OnOpcode: t.OnOpcode,
@ -40,38 +93,246 @@ func newRip7560Tracer(ctx *tracers.Context, cfg json.RawMessage) (*tracers.Trace
}, nil }, nil
} }
type callsItem struct {
// Common
Type string `json:"type"`
// Enter info
From common.Address `json:"from"`
To common.Address `json:"to"`
Method hexutil.Bytes `json:"method"`
Value *hexutil.Big `json:"value"`
Gas uint64 `json:"gas"`
// Exit info
GasUsed uint64 `json:"gasUsed"`
Data hexutil.Bytes `json:"data"`
}
type logsItem struct {
Data hexutil.Bytes `json:"data"`
Topic []hexutil.Bytes `json:"topic"`
}
// Array fields contain of all access details of all validation frames // Array fields contain of all access details of all validation frames
type rip7560ValidationTracer struct { type rip7560ValidationTracer struct {
//rip7560TxData *types.Rip7560AccountAbstractionTx
env *tracing.VMContext env *tracing.VMContext
TraceResults []stateMap `json:"traceResults"` TraceResults []stateMap `json:"traceResults"`
UsedOpcodes []map[string]bool `json:"usedOpcodes"` UsedOpcodes []map[string]bool `json:"usedOpcodes"`
Created []map[common.Address]bool `json:"created"` Created []map[common.Address]bool `json:"created"`
Deleted []map[common.Address]bool `json:"deleted"` //Deleted []map[common.Address]bool `json:"deleted"`
lastThreeOpCodes []*lastThreeOpCodesItem
allowedOpcodeRegex *regexp.Regexp `json:"allowedOpcodeRegex,omitempty"`
CurrentLevel *entryPointCall
lastOp string
CallsFromEntryPoint []*entryPointCall `json:"callsFromEntryPoint,omitempty"`
Keccak []hexutil.Bytes `json:"keccak"`
Calls []*callsItem `json:"calls"`
Logs []*logsItem `json:"logs"`
// todo // todo
//interrupt atomic.Bool // Atomic flag to signal execution interruption //interrupt atomic.Bool // Atomic flag to signal execution interruption
//reason error // Textual reason for the interruption //reason error // Textual reason for the interruption
} }
func (t *rip7560ValidationTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { func (b *rip7560ValidationTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
if depth == 0 {
} b.createNewTopLevelFrame(to)
func (t *rip7560ValidationTracer) OnTxEnd(receipt *types.Receipt, err error) {
}
func (t *rip7560ValidationTracer) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) {
opcodeName := vm.OpCode(opcode).String()
fmt.Printf("%s %d %d\n", opcodeName, cost, depth)
if t.UsedOpcodes[0] == nil {
t.UsedOpcodes[0] = make(map[string]bool)
} }
t.UsedOpcodes[0][opcodeName] = true
} }
func (t *rip7560ValidationTracer) GetResult() (json.RawMessage, error) { func (b *rip7560ValidationTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) {
jsonResult, err := json.MarshalIndent(*t, "", " ") b.env = env
//b.rip7560TxData = tx.Rip7560TransactionData()
}
func (b *rip7560ValidationTracer) createNewTopLevelFrame(addr common.Address) {
b.CurrentLevel = &entryPointCall{
TopLevelTargetAddress: addr,
Access: map[common.Address]*access{},
Opcodes: map[string]uint64{},
ExtCodeAccessInfo: map[common.Address]string{},
ContractSize: map[common.Address]*contractSizeVal{},
OOG: false,
}
b.CallsFromEntryPoint = append(b.CallsFromEntryPoint, b.CurrentLevel)
b.lastOp = ""
return
}
func (b *rip7560ValidationTracer) OnTxEnd(receipt *types.Receipt, err error) {
}
func (b *rip7560ValidationTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) {
opcode := vm.OpCode(op).String()
stackSize := len(scope.StackData())
stackTop3 := partialStack{}
for i := 0; i < 3 && i < stackSize; i++ {
stackTop3 = append(stackTop3, StackBack(scope.StackData(), i))
}
b.lastThreeOpCodes = append(b.lastThreeOpCodes, &lastThreeOpCodesItem{
Opcode: opcode,
StackTop3: stackTop3,
})
if len(b.lastThreeOpCodes) > 3 {
b.lastThreeOpCodes = b.lastThreeOpCodes[1:]
}
if gas < cost || (opcode == "SSTORE" && gas < 2300) {
b.CurrentLevel.OOG = true
}
if opcode == "REVERT" || opcode == "RETURN" {
// exit() is not called on top-level return/revert, so we reconstruct it from opcode
if depth == 1 {
// TODO: uncomment and fix with StackBack
//ofs := scope.Stack.Back(0).ToBig().Int64()
//len := scope.Stack.Back(1).ToBig().Int64()
//data := scope.Memory.GetCopy(ofs, len)
//b.Calls = append(b.Calls, &callsItem{
// Type: opcode,
// GasUsed: 0,
// Data: data,
//})
}
// NOTE: flushing all history after RETURN
b.lastThreeOpCodes = []*lastThreeOpCodesItem{}
}
// not pasting the new "entryPointCall" detection here - not necessary for 7560
var lastOpInfo *lastThreeOpCodesItem
if len(b.lastThreeOpCodes) >= 2 {
lastOpInfo = b.lastThreeOpCodes[len(b.lastThreeOpCodes)-2]
}
// store all addresses touched by EXTCODE* opcodes
if lastOpInfo != nil && strings.HasPrefix(lastOpInfo.Opcode, "EXT") {
addr := common.HexToAddress(lastOpInfo.StackTop3[0].Hex())
ops := []string{}
for _, item := range b.lastThreeOpCodes {
ops = append(ops, item.Opcode)
}
last3OpcodeStr := strings.Join(ops, ",")
// only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case
// [OP-051]
if !strings.Contains(last3OpcodeStr, ",EXTCODESIZE,ISZERO") {
b.CurrentLevel.ExtCodeAccessInfo[addr] = opcode
}
}
// [OP-041]
if b.isEXTorCALL(opcode) {
n := 0
if !strings.HasPrefix(opcode, "EXT") {
n = 1
}
addr := common.BytesToAddress(StackBack(scope.StackData(), n).Bytes())
if _, ok := b.CurrentLevel.ContractSize[addr]; !ok && !b.isAllowedPrecompile(addr) {
b.CurrentLevel.ContractSize[addr] = &contractSizeVal{
ContractSize: len(b.env.StateDB.GetCode(addr)),
Opcode: opcode,
}
}
}
// [OP-012]
if b.lastOp == "GAS" && !strings.Contains(opcode, "CALL") {
b.incrementCount(b.CurrentLevel.Opcodes, "GAS")
}
// ignore "unimportant" opcodes
if opcode != "GAS" && !b.allowedOpcodeRegex.MatchString(opcode) {
b.incrementCount(b.CurrentLevel.Opcodes, opcode)
}
b.lastOp = opcode
if opcode == "SLOAD" || opcode == "SSTORE" {
slot := common.BytesToHash(StackBack(scope.StackData(), 0).Bytes())
slotHex := slot.Hex()
addr := scope.Address()
if _, ok := b.CurrentLevel.Access[addr]; !ok {
b.CurrentLevel.Access[addr] = &access{
Reads: map[string]string{},
Writes: map[string]uint64{},
}
}
access := *b.CurrentLevel.Access[addr]
if opcode == "SLOAD" {
// read slot values before this UserOp was created
// (so saving it if it was written before the first read)
_, rOk := access.Reads[slotHex]
_, wOk := access.Writes[slotHex]
if !rOk && !wOk {
access.Reads[slotHex] = b.env.StateDB.GetState(addr, slot).Hex()
}
} else {
b.incrementCount(access.Writes, slotHex)
}
}
if opcode == "KECCAK256" {
// TODO: uncomment and fix with StackBack
// collect keccak on 64-byte blocks
// ofs := scope.Stack.Back(0).ToBig().Int64()
// len := scope.Stack.Back(1).ToBig().Int64()
// // currently, solidity uses only 2-word (6-byte) for a key. this might change..still, no need to
// // return too much
// if len > 20 && len < 512 {
// b.Keccak = append(b.Keccak, scope.Memory.GetCopy(ofs, len))
// }
//} else if strings.HasPrefix(opcode, "LOG") {
// count, _ := strconv.Atoi(opcode[3:])
// ofs := scope.Stack.Back(0).ToBig().Int64()
// len := scope.Stack.Back(1).ToBig().Int64()
// topics := []hexutil.Bytes{}
// for i := 0; i < count; i++ {
// topics = append(topics, scope.Stack.Back(2+i).Bytes())
// }
//
// b.Logs = append(b.Logs, &logsItem{
// Data: scope.Memory.GetCopy(ofs, len),
// Topic: topics,
// })
}
}
// StackBack returns the n-th item in stack
func StackBack(stackData []uint256.Int, n int) *uint256.Int {
return &stackData[len(stackData)-n-1]
}
func (b *rip7560ValidationTracer) isEXTorCALL(opcode string) bool {
return strings.HasPrefix(opcode, "EXT") ||
opcode == "CALL" ||
opcode == "CALLCODE" ||
opcode == "DELEGATECALL" ||
opcode == "STATICCALL"
}
// not using 'isPrecompiled' to only allow the ones defined by the ERC-7562 as stateless precompiles
// [OP-062]
func (b *rip7560ValidationTracer) isAllowedPrecompile(addr common.Address) bool {
addrInt := addr.Big()
return addrInt.Cmp(big.NewInt(0)) == 1 && addrInt.Cmp(big.NewInt(10)) == -1
}
func (b *rip7560ValidationTracer) incrementCount(m map[string]uint64, k string) {
if _, ok := m[k]; !ok {
m[k] = 0
}
m[k]++
}
func (b *rip7560ValidationTracer) GetResult() (json.RawMessage, error) {
jsonResult, err := json.MarshalIndent(*b, "", " ")
return jsonResult, err return jsonResult, err
} }
func (t *rip7560ValidationTracer) Stop(err error) { func (b *rip7560ValidationTracer) Stop(err error) {
} }