|
| 1 | +package native |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "math/big" |
| 6 | + "regexp" |
| 7 | + "strconv" |
| 8 | + "strings" |
| 9 | + |
| 10 | + "github.com/ethereum/go-ethereum/common" |
| 11 | + "github.com/ethereum/go-ethereum/common/hexutil" |
| 12 | + "github.com/ethereum/go-ethereum/core/tracing" |
| 13 | + "github.com/ethereum/go-ethereum/core/types" |
| 14 | + "github.com/ethereum/go-ethereum/core/vm" |
| 15 | + "github.com/ethereum/go-ethereum/eth/tracers" |
| 16 | + "github.com/holiman/uint256" |
| 17 | +) |
| 18 | + |
| 19 | +func init() { |
| 20 | + tracers.DefaultDirectory.Register("bundlerCollectorTracer", newBundlerCollector, false) |
| 21 | +} |
| 22 | + |
| 23 | +type partialStack = []*uint256.Int |
| 24 | + |
| 25 | +type contractSizeVal struct { |
| 26 | + ContractSize int `json:"contractSize"` |
| 27 | + Opcode string `json:"opcode"` |
| 28 | +} |
| 29 | + |
| 30 | +type access struct { |
| 31 | + Reads map[string]string `json:"reads"` |
| 32 | + Writes map[string]uint64 `json:"writes"` |
| 33 | +} |
| 34 | + |
| 35 | +type entryPointCall struct { |
| 36 | + TopLevelMethodSig hexutil.Bytes `json:"topLevelMethodSig"` |
| 37 | + TopLevelTargetAddress common.Address `json:"topLevelTargetAddress"` |
| 38 | + Access map[common.Address]*access `json:"access"` |
| 39 | + Opcodes map[string]uint64 `json:"opcodes"` |
| 40 | + ExtCodeAccessInfo map[common.Address]string `json:"extCodeAccessInfo"` |
| 41 | + ContractSize map[common.Address]*contractSizeVal `json:"contractSize"` |
| 42 | + OOG bool `json:"oog"` |
| 43 | +} |
| 44 | + |
| 45 | +type callsItem struct { |
| 46 | + // Common |
| 47 | + Type string `json:"type"` |
| 48 | + |
| 49 | + // Enter info |
| 50 | + From common.Address `json:"from"` |
| 51 | + To common.Address `json:"to"` |
| 52 | + Method hexutil.Bytes `json:"method"` |
| 53 | + Value *hexutil.Big `json:"value"` |
| 54 | + Gas uint64 `json:"gas"` |
| 55 | + |
| 56 | + // Exit info |
| 57 | + GasUsed uint64 `json:"gasUsed"` |
| 58 | + Data hexutil.Bytes `json:"data"` |
| 59 | +} |
| 60 | + |
| 61 | +type logsItem struct { |
| 62 | + Data hexutil.Bytes `json:"data"` |
| 63 | + Topic []hexutil.Bytes `json:"topic"` |
| 64 | +} |
| 65 | + |
| 66 | +type lastThreeOpCodesItem struct { |
| 67 | + Opcode string |
| 68 | + StackTop3 partialStack |
| 69 | +} |
| 70 | + |
| 71 | +type bundlerCollectorResults struct { |
| 72 | + CallsFromEntryPoint []*entryPointCall `json:"callsFromEntryPoint"` |
| 73 | + Keccak []hexutil.Bytes `json:"keccak"` |
| 74 | + Logs []*logsItem `json:"logs"` |
| 75 | + Calls []*callsItem `json:"calls"` |
| 76 | +} |
| 77 | + |
| 78 | +type bundlerCollector struct { |
| 79 | + vm *tracing.VMContext |
| 80 | + |
| 81 | + CallsFromEntryPoint []*entryPointCall |
| 82 | + CurrentLevel *entryPointCall |
| 83 | + Keccak []hexutil.Bytes |
| 84 | + Calls []*callsItem |
| 85 | + Logs []*logsItem |
| 86 | + lastOp string |
| 87 | + lastThreeOpCodes []*lastThreeOpCodesItem |
| 88 | + allowedOpcodeRegex *regexp.Regexp |
| 89 | + stopCollectingTopic string |
| 90 | + stopCollecting bool |
| 91 | +} |
| 92 | + |
| 93 | +func newBundlerCollector(ctx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) { |
| 94 | + rgx, err := regexp.Compile( |
| 95 | + `^(DUP\d+|PUSH\d+|SWAP\d+|POP|ADD|SUB|MUL|DIV|EQ|LTE?|S?GTE?|SLT|SH[LR]|AND|OR|NOT|ISZERO)$`, |
| 96 | + ) |
| 97 | + if err != nil { |
| 98 | + return nil, err |
| 99 | + } |
| 100 | + // event sent after all validations are done: keccak("BeforeExecution()") |
| 101 | + stopCollectingTopic := "0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972" |
| 102 | + |
| 103 | + t := &bundlerCollector{ |
| 104 | + CallsFromEntryPoint: []*entryPointCall{}, |
| 105 | + CurrentLevel: nil, |
| 106 | + Keccak: []hexutil.Bytes{}, |
| 107 | + Calls: []*callsItem{}, |
| 108 | + Logs: []*logsItem{}, |
| 109 | + lastOp: "", |
| 110 | + lastThreeOpCodes: []*lastThreeOpCodesItem{}, |
| 111 | + allowedOpcodeRegex: rgx, |
| 112 | + stopCollectingTopic: stopCollectingTopic, |
| 113 | + stopCollecting: false, |
| 114 | + } |
| 115 | + |
| 116 | + return &tracers.Tracer{ |
| 117 | + Hooks: &tracing.Hooks{ |
| 118 | + OnTxStart: t.OnTxStart, |
| 119 | + OnEnter: t.OnEnter, |
| 120 | + OnExit: t.OnExit, |
| 121 | + OnOpcode: t.OnOpcode, |
| 122 | + }, |
| 123 | + GetResult: t.GetResult, |
| 124 | + }, nil |
| 125 | +} |
| 126 | + |
| 127 | +func (b *bundlerCollector) isEXTorCALL(opcode string) bool { |
| 128 | + return strings.HasPrefix(opcode, "EXT") || |
| 129 | + opcode == "CALL" || |
| 130 | + opcode == "CALLCODE" || |
| 131 | + opcode == "DELEGATECALL" || |
| 132 | + opcode == "STATICCALL" |
| 133 | +} |
| 134 | + |
| 135 | +// not using 'isPrecompiled' to only allow the ones defined by the ERC-4337 as stateless precompiles |
| 136 | +// [OP-062] |
| 137 | +func (b *bundlerCollector) isAllowedPrecompile(addr common.Address) bool { |
| 138 | + addrInt := addr.Big() |
| 139 | + return addrInt.Cmp(big.NewInt(0)) == 1 && addrInt.Cmp(big.NewInt(10)) == -1 |
| 140 | +} |
| 141 | + |
| 142 | +func (b *bundlerCollector) incrementCount(m map[string]uint64, k string) { |
| 143 | + if _, ok := m[k]; !ok { |
| 144 | + m[k] = 0 |
| 145 | + } |
| 146 | + m[k]++ |
| 147 | +} |
| 148 | + |
| 149 | +func (b *bundlerCollector) OnTxStart( |
| 150 | + vm *tracing.VMContext, |
| 151 | + tx *types.Transaction, |
| 152 | + from common.Address, |
| 153 | +) { |
| 154 | + b.vm = vm |
| 155 | +} |
| 156 | + |
| 157 | +func (b *bundlerCollector) GetResult() (json.RawMessage, error) { |
| 158 | + bcr := bundlerCollectorResults{ |
| 159 | + CallsFromEntryPoint: b.CallsFromEntryPoint, |
| 160 | + Keccak: b.Keccak, |
| 161 | + Logs: b.Logs, |
| 162 | + Calls: b.Calls, |
| 163 | + } |
| 164 | + |
| 165 | + r, err := json.Marshal(bcr) |
| 166 | + if err != nil { |
| 167 | + return nil, err |
| 168 | + } |
| 169 | + return r, nil |
| 170 | +} |
| 171 | + |
| 172 | +func (b *bundlerCollector) OnEnter( |
| 173 | + depth int, |
| 174 | + typ byte, |
| 175 | + from common.Address, |
| 176 | + to common.Address, |
| 177 | + input []byte, |
| 178 | + gas uint64, |
| 179 | + value *big.Int, |
| 180 | +) { |
| 181 | + if b.stopCollecting { |
| 182 | + return |
| 183 | + } |
| 184 | + |
| 185 | + op := vm.OpCode(typ) |
| 186 | + method := []byte{} |
| 187 | + if len(input) >= 4 { |
| 188 | + method = append(method, input[:4]...) |
| 189 | + } |
| 190 | + b.Calls = append(b.Calls, &callsItem{ |
| 191 | + Type: op.String(), |
| 192 | + From: from, |
| 193 | + To: to, |
| 194 | + Method: method, |
| 195 | + Gas: gas, |
| 196 | + Value: (*hexutil.Big)(value), |
| 197 | + }) |
| 198 | +} |
| 199 | + |
| 200 | +func (b *bundlerCollector) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { |
| 201 | + if b.stopCollecting { |
| 202 | + return |
| 203 | + } |
| 204 | + |
| 205 | + rt := "RETURN" |
| 206 | + if err != nil { |
| 207 | + rt = "REVERT" |
| 208 | + } |
| 209 | + b.Calls = append(b.Calls, &callsItem{ |
| 210 | + Type: rt, |
| 211 | + GasUsed: gasUsed, |
| 212 | + Data: output, |
| 213 | + }) |
| 214 | +} |
| 215 | + |
| 216 | +func (b *bundlerCollector) OnOpcode( |
| 217 | + pc uint64, |
| 218 | + op byte, |
| 219 | + gas, cost uint64, |
| 220 | + scope tracing.OpContext, |
| 221 | + rData []byte, |
| 222 | + depth int, |
| 223 | + err error, |
| 224 | +) { |
| 225 | + if b.stopCollecting { |
| 226 | + return |
| 227 | + } |
| 228 | + opcode := vm.OpCode(op).String() |
| 229 | + |
| 230 | + stackSize := len(scope.StackData()) |
| 231 | + stackLast := stackSize - 1 |
| 232 | + stackTop3 := partialStack{} |
| 233 | + for i := 0; i < 3 && i < stackSize; i++ { |
| 234 | + stackTop3 = append(stackTop3, scope.StackData()[stackLast-i].Clone()) |
| 235 | + } |
| 236 | + b.lastThreeOpCodes = append(b.lastThreeOpCodes, &lastThreeOpCodesItem{ |
| 237 | + Opcode: opcode, |
| 238 | + StackTop3: stackTop3, |
| 239 | + }) |
| 240 | + if len(b.lastThreeOpCodes) > 3 { |
| 241 | + b.lastThreeOpCodes = b.lastThreeOpCodes[1:] |
| 242 | + } |
| 243 | + |
| 244 | + if gas < cost || (opcode == "SSTORE" && gas < 2300) { |
| 245 | + b.CurrentLevel.OOG = true |
| 246 | + } |
| 247 | + |
| 248 | + if opcode == "REVERT" || opcode == "RETURN" { |
| 249 | + // exit() is not called on top-level return/revert, so we reconstruct it from opcode |
| 250 | + if depth == 1 { |
| 251 | + ofs := scope.StackData()[stackLast].ToBig().Int64() |
| 252 | + len := scope.StackData()[stackLast-1].ToBig().Int64() |
| 253 | + data := make([]byte, len) |
| 254 | + copy(data, scope.MemoryData()[ofs:ofs+len]) |
| 255 | + b.Calls = append(b.Calls, &callsItem{ |
| 256 | + Type: opcode, |
| 257 | + GasUsed: 0, |
| 258 | + Data: data, |
| 259 | + }) |
| 260 | + } |
| 261 | + // NOTE: flushing all history after RETURN |
| 262 | + b.lastThreeOpCodes = []*lastThreeOpCodesItem{} |
| 263 | + } |
| 264 | + |
| 265 | + if depth == 1 { |
| 266 | + if opcode == "CALL" || opcode == "STATICCALL" { |
| 267 | + addr := common.HexToAddress(scope.StackData()[stackLast-1].Hex()) |
| 268 | + ofs := scope.StackData()[stackLast-3].ToBig().Int64() |
| 269 | + sig := make([]byte, 4) |
| 270 | + copy(sig, scope.MemoryData()[ofs:ofs+4]) |
| 271 | + |
| 272 | + b.CurrentLevel = &entryPointCall{ |
| 273 | + TopLevelMethodSig: sig, |
| 274 | + TopLevelTargetAddress: addr, |
| 275 | + Access: map[common.Address]*access{}, |
| 276 | + Opcodes: map[string]uint64{}, |
| 277 | + ExtCodeAccessInfo: map[common.Address]string{}, |
| 278 | + ContractSize: map[common.Address]*contractSizeVal{}, |
| 279 | + OOG: false, |
| 280 | + } |
| 281 | + b.CallsFromEntryPoint = append(b.CallsFromEntryPoint, b.CurrentLevel) |
| 282 | + } else if opcode == "LOG1" && scope.StackData()[stackLast-2].Hex() == b.stopCollectingTopic { |
| 283 | + b.stopCollecting = true |
| 284 | + } |
| 285 | + b.lastOp = "" |
| 286 | + return |
| 287 | + } |
| 288 | + |
| 289 | + var lastOpInfo *lastThreeOpCodesItem |
| 290 | + if len(b.lastThreeOpCodes) >= 2 { |
| 291 | + lastOpInfo = b.lastThreeOpCodes[len(b.lastThreeOpCodes)-2] |
| 292 | + } |
| 293 | + // store all addresses touched by EXTCODE* opcodes |
| 294 | + if lastOpInfo != nil && strings.HasPrefix(lastOpInfo.Opcode, "EXT") { |
| 295 | + addr := common.HexToAddress(lastOpInfo.StackTop3[0].Hex()) |
| 296 | + ops := []string{} |
| 297 | + for _, item := range b.lastThreeOpCodes { |
| 298 | + ops = append(ops, item.Opcode) |
| 299 | + } |
| 300 | + last3OpcodeStr := strings.Join(ops, ",") |
| 301 | + |
| 302 | + // only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case |
| 303 | + // [OP-051] |
| 304 | + if !strings.Contains(last3OpcodeStr, ",EXTCODESIZE,ISZERO") { |
| 305 | + b.CurrentLevel.ExtCodeAccessInfo[addr] = opcode |
| 306 | + } |
| 307 | + } |
| 308 | + |
| 309 | + // [OP-041] |
| 310 | + if b.isEXTorCALL(opcode) { |
| 311 | + n := 0 |
| 312 | + if !strings.HasPrefix(opcode, "EXT") { |
| 313 | + n = 1 |
| 314 | + } |
| 315 | + addr := common.BytesToAddress(scope.StackData()[stackLast-n].Bytes()) |
| 316 | + |
| 317 | + if _, ok := b.CurrentLevel.ContractSize[addr]; !ok && !b.isAllowedPrecompile(addr) { |
| 318 | + b.CurrentLevel.ContractSize[addr] = &contractSizeVal{ |
| 319 | + ContractSize: len(b.vm.StateDB.GetCode(addr)), |
| 320 | + Opcode: opcode, |
| 321 | + } |
| 322 | + } |
| 323 | + } |
| 324 | + |
| 325 | + // [OP-012] |
| 326 | + if b.lastOp == "GAS" && !strings.Contains(opcode, "CALL") { |
| 327 | + b.incrementCount(b.CurrentLevel.Opcodes, "GAS") |
| 328 | + } |
| 329 | + // ignore "unimportant" opcodes |
| 330 | + if opcode != "GAS" && !b.allowedOpcodeRegex.MatchString(opcode) { |
| 331 | + b.incrementCount(b.CurrentLevel.Opcodes, opcode) |
| 332 | + } |
| 333 | + b.lastOp = opcode |
| 334 | + |
| 335 | + if opcode == "SLOAD" || opcode == "SSTORE" { |
| 336 | + slot := common.BytesToHash(scope.StackData()[stackLast].Bytes()) |
| 337 | + slotHex := slot.Hex() |
| 338 | + addr := scope.Address() |
| 339 | + if _, ok := b.CurrentLevel.Access[addr]; !ok { |
| 340 | + b.CurrentLevel.Access[addr] = &access{ |
| 341 | + Reads: map[string]string{}, |
| 342 | + Writes: map[string]uint64{}, |
| 343 | + } |
| 344 | + } |
| 345 | + access := *b.CurrentLevel.Access[addr] |
| 346 | + |
| 347 | + if opcode == "SLOAD" { |
| 348 | + // read slot values before this UserOp was created |
| 349 | + // (so saving it if it was written before the first read) |
| 350 | + _, rOk := access.Reads[slotHex] |
| 351 | + _, wOk := access.Writes[slotHex] |
| 352 | + if !rOk && !wOk { |
| 353 | + access.Reads[slotHex] = string(b.vm.StateDB.GetState(addr, slot).Hex()) |
| 354 | + } |
| 355 | + } else { |
| 356 | + b.incrementCount(access.Writes, slotHex) |
| 357 | + } |
| 358 | + } |
| 359 | + |
| 360 | + if opcode == "KECCAK256" { |
| 361 | + // collect keccak on 64-byte blocks |
| 362 | + ofs := scope.StackData()[stackLast].ToBig().Int64() |
| 363 | + len := scope.StackData()[stackLast-1].ToBig().Int64() |
| 364 | + // currently, solidity uses only 2-word (6-byte) for a key. this might change..still, no need to |
| 365 | + // return too much |
| 366 | + if len > 20 && len < 512 { |
| 367 | + data := make([]byte, len) |
| 368 | + copy(data, scope.MemoryData()[ofs:ofs+len]) |
| 369 | + b.Keccak = append(b.Keccak, data) |
| 370 | + } |
| 371 | + } else if strings.HasPrefix(opcode, "LOG") { |
| 372 | + count, _ := strconv.Atoi(opcode[3:]) |
| 373 | + ofs := scope.StackData()[stackLast].ToBig().Int64() |
| 374 | + len := scope.StackData()[stackLast-1].ToBig().Int64() |
| 375 | + topics := []hexutil.Bytes{} |
| 376 | + for i := 0; i < count; i++ { |
| 377 | + topics = append(topics, scope.StackData()[stackLast-2+i].Bytes()) |
| 378 | + } |
| 379 | + |
| 380 | + data := make([]byte, len) |
| 381 | + copy(data, scope.MemoryData()[ofs:ofs+len]) |
| 382 | + b.Logs = append(b.Logs, &logsItem{ |
| 383 | + Data: data, |
| 384 | + Topic: topics, |
| 385 | + }) |
| 386 | + } |
| 387 | +} |
0 commit comments