diff --git a/core/state_transition.go b/core/state_transition.go index df8c679a7a..2ddd735005 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -457,14 +457,16 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { ) // Check clauses 4-5, subtract intrinsic gas if everything is correct - gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules) - if err != nil { - return nil, err - } - if st.gasRemaining < gas { - return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas) + if !st.evm.Config.IgnoreGas { + gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules) + if err != nil { + return nil, err + } + if st.gasRemaining < gas { + return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas) + } + st.gasRemaining -= gas } - st.gasRemaining -= gas // Check clause 6 value, overflow := uint256.FromBig(msg.Value) @@ -476,7 +478,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { } // Check whether the init code size has been exceeded. - if rules.IsDurango && contractCreation && len(msg.Data) > params.MaxInitCodeSize { + if !st.evm.Config.IgnoreCodeSizeLimit && rules.IsDurango && contractCreation && len(msg.Data) > params.MaxInitCodeSize { return nil, fmt.Errorf("%w: code size %v limit %v", vmerrs.ErrMaxInitCodeSizeExceeded, len(msg.Data), params.MaxInitCodeSize) } diff --git a/core/vm/evm.go b/core/vm/evm.go index 0eb7ec12df..cf0742f56b 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -514,6 +514,16 @@ func (c *codeAndHash) Hash() common.Hash { func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { // Depth check execution. Fail if we're trying to execute above the // limit. + if evm.Config.CreateAddressOverride != nil { + address = *evm.Config.CreateAddressOverride + } + if evm.Config.CreationCodeOverrides != nil { + if code, ok := evm.Config.CreationCodeOverrides[address]; ok { + codeAndHash.code = code + codeAndHash.hash = common.Hash{} + _ = codeAndHash.Hash() + } + } if evm.depth > int(params.CallCreateDepth) { return nil, common.Address{}, gas, vmerrs.ErrDepth } @@ -538,10 +548,12 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if evm.chainRules.IsApricotPhase2 { evm.StateDB.AddAddressToAccessList(address) } - // Ensure there's no existing contract already at the designated address - contractHash := evm.StateDB.GetCodeHash(address) - if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) { - return nil, common.Address{}, 0, vmerrs.ErrContractAddressCollision + if evm.Config.CreateAddressOverride == nil { + // Ensure there's no existing contract already at the designated address + contractHash := evm.StateDB.GetCodeHash(address) + if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) { + return nil, common.Address{}, 0, vmerrs.ErrContractAddressCollision + } } // Create a new account on the state snapshot := evm.StateDB.Snapshot() @@ -567,7 +579,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, ret, err := evm.interpreter.Run(contract, nil, false) // Check whether the max code size has been exceeded, assign err if the case. - if err == nil && evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize { + if err == nil && !evm.Config.IgnoreCodeSizeLimit && evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize { err = vmerrs.ErrMaxCodeSizeExceeded } @@ -582,6 +594,9 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // by the error checking condition below. if err == nil { createDataGas := uint64(len(ret)) * params.CreateDataGas + if evm.Config.IgnoreGas { + createDataGas = 0 + } if contract.UseGas(createDataGas) { evm.StateDB.SetCode(address, ret) } else { diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 72047c260d..8904e7d93e 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -588,7 +588,7 @@ func opCreate(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b input = scope.Memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) gas = scope.Contract.Gas ) - if interpreter.evm.chainRules.IsEIP150 { + if !interpreter.evm.Config.IgnoreGas && interpreter.evm.chainRules.IsEIP150 { gas -= gas / 64 } // reuse size int for stackvalue @@ -631,7 +631,9 @@ func opCreate2(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([] gas = scope.Contract.Gas ) // Apply EIP150 - gas -= gas / 64 + if !interpreter.evm.Config.IgnoreGas { + gas -= gas / 64 + } scope.Contract.UseGas(gas) // reuse size int for stackvalue stackvalue := size diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 5d495f0af6..f64a6e13e0 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -29,6 +29,7 @@ package vm import ( "github.com/ava-labs/coreth/vmerrs" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -40,6 +41,11 @@ type Config struct { NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages ExtraEips []int // Additional EIPS that are to be enabled + + CreationCodeOverrides map[common.Address]hexutil.Bytes + CreateAddressOverride *common.Address + IgnoreGas bool + IgnoreCodeSizeLimit bool } // ScopeContext contains the things that are per-call, such as stack and memory, @@ -106,6 +112,22 @@ func NewEVMInterpreter(evm *EVM) *EVMInterpreter { } } evm.Config.ExtraEips = extraEips + if evm.Config.IgnoreGas { + table = copyJumpTable(table) + for i, op := range table { + opCode := OpCode(i) + // retain call costs to prevent call stack from going too deep + // some contracts use a loop to burn gas + // if all codes in the loop have zero cost, it will run forever + if opCode == CALL || opCode == STATICCALL || opCode == CALLCODE || opCode == DELEGATECALL || opCode == GAS { + continue + } + op.constantGas = 0 + op.dynamicGas = func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) { + return 0, nil + } + } + } return &EVMInterpreter{evm: evm, table: table} } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 605a2495fe..9059e483f9 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -184,13 +184,18 @@ type TraceConfig struct { // Config specific to given tracer. Note struct logger // config are historically embedded in main object. TracerConfig json.RawMessage + + StateOverrides *ethapi.StateOverride + IgnoreGas *bool + IgnoreCodeSizeLimit *bool + CreationCodeOverrides map[common.Address]hexutil.Bytes + CreateAddressOverride *common.Address } // TraceCallConfig is the config for traceCall API. It holds one more // field to override the state for tracing. type TraceCallConfig struct { TraceConfig - StateOverrides *ethapi.StateOverride BlockOverrides *ethapi.BlockOverrides TxIndex *hexutil.Uint } @@ -906,6 +911,12 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config * TxIndex: int(index), TxHash: hash, } + + if config != nil { + if err := config.StateOverrides.Apply(statedb); err != nil { + return nil, err + } + } return api.traceTx(ctx, msg, txctx, vmctx, statedb, config) } @@ -1008,7 +1019,21 @@ func (api *baseAPI) traceTx(ctx context.Context, message *core.Message, txctx *C return nil, err } } - vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Tracer: tracer, NoBaseFee: true}) + vmConfig := vm.Config{ + Tracer: tracer, + NoBaseFee: true, + } + if config != nil { + vmConfig.CreateAddressOverride = config.CreateAddressOverride + vmConfig.CreationCodeOverrides = config.CreationCodeOverrides + if config.IgnoreCodeSizeLimit != nil { + vmConfig.IgnoreCodeSizeLimit = *config.IgnoreCodeSizeLimit + } + if config.IgnoreGas != nil { + vmConfig.IgnoreGas = *config.IgnoreGas + } + } + vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vmConfig) // Define a meaningful timeout of a single transaction trace if config.Timeout != nil { @@ -1116,3 +1141,116 @@ func overrideConfig(original *params.ChainConfig, override *params.ChainConfig) return copy, canon } + +type Bundle struct { + Transactions []*ethapi.TransactionArgs `json:"transactions"` + BlockOverride ethapi.BlockOverrides `json:"blockOverride"` +} + +type StateContext struct { + BlockNumber *rpc.BlockNumberOrHash `json:"blockNumber"` + TransactionIndex int `json:"transactionIndex"` +} + +type FailedTrace struct { + Failed string `json:"failed,omitempty"` +} + +func (api *API) TraceCallMany(ctx context.Context, bundles []*Bundle, simulateContext *StateContext, config *TraceCallConfig) (interface{}, error) { + if len(bundles) == 0 { + return nil, errors.New("empty bundles") + } + var result []interface{} + for _, bundle := range bundles { + r, err := api.traceBundle(ctx, bundle, simulateContext, config) + if err != nil { + if r != nil { + // return partial results + r = append(r, &FailedTrace{Failed: err.Error()}) + result = append(result, r) + return result, nil + } + return nil, err + } + result = append(result, r) + } + return result, nil +} + +func (api *API) traceBundle(ctx context.Context, bundle *Bundle, simulateContext *StateContext, config *TraceCallConfig) ([]interface{}, error) { + var result []interface{} + // Try to retrieve the specified block + var ( + err error + block *types.Block + ) + if hash, ok := simulateContext.BlockNumber.Hash(); ok { + block, err = api.blockByHash(ctx, hash) + } else if number, ok := simulateContext.BlockNumber.Number(); ok { + if number == rpc.PendingBlockNumber { + // We don't have access to the miner here. For tracing 'future' transactions, + // it can be done with block- and state-overrides instead, which offers + // more flexibility and stability than trying to trace on 'pending', since + // the contents of 'pending' is unstable and probably not a true representation + // of what the next actual block is likely to contain. + return nil, errors.New("tracing on top of pending is not supported") + } + block, err = api.blockByNumber(ctx, number) + } else { + return nil, errors.New("invalid arguments; neither block nor hash specified") + } + if err != nil { + return nil, err + } + // try to recompute the state + reexec := defaultTraceReexec + if config != nil && config.Reexec != nil { + reexec = *config.Reexec + } + is158 := api.backend.ChainConfig().IsEIP158(block.Number()) + + if err != nil { + return nil, err + } + _, vmctx, statedb, release, err := api.backend.StateAtTransaction(ctx, block, simulateContext.TransactionIndex, reexec) + if err != nil { + return nil, err + } + defer release() + + // Apply the customization rules if required. + if config != nil { + if err := config.StateOverrides.Apply(statedb); err != nil { + return nil, err + } + config.BlockOverrides.Apply(&vmctx) + } + // Execute the trace + for idx, args := range bundle.Transactions { + if args.Gas == nil { + gasCap := api.backend.RPCGasCap() + args.Gas = (*hexutil.Uint64)(&gasCap) + } + msg, err := args.ToMessage(api.backend.RPCGasCap(), block.BaseFee()) + if err != nil { + return result, err + } + + var traceConfig *TraceConfig + if config != nil { + traceConfig = &config.TraceConfig + } + txctx := &Context{ + BlockHash: block.Hash(), + BlockNumber: block.Number(), + TxIndex: simulateContext.TransactionIndex + idx, + } + r, err := api.traceTx(ctx, msg, txctx, vmctx, statedb, traceConfig) + if err != nil { + return result, err + } + result = append(result, r) + statedb.Finalise(is158) + } + return result, nil +} diff --git a/eth/tracers/internal/tracetest/util.go b/eth/tracers/internal/tracetest/util.go index 3cecbf84ab..d36a40cae9 100644 --- a/eth/tracers/internal/tracetest/util.go +++ b/eth/tracers/internal/tracetest/util.go @@ -16,6 +16,7 @@ import ( // Force-load native and js packages, to trigger registration _ "github.com/ava-labs/coreth/eth/tracers/js" _ "github.com/ava-labs/coreth/eth/tracers/native" + _ "github.com/ava-labs/coreth/eth/tracers/sentio" ) // To generate a new callTracer test, copy paste the makeTest method below into diff --git a/eth/tracers/logger/gen_structlog.go b/eth/tracers/logger/gen_structlog.go index c41d569a96..d753824435 100644 --- a/eth/tracers/logger/gen_structlog.go +++ b/eth/tracers/logger/gen_structlog.go @@ -22,6 +22,7 @@ func (s StructLog) MarshalJSON() ([]byte, error) { Gas math.HexOrDecimal64 `json:"gas"` GasCost math.HexOrDecimal64 `json:"gasCost"` Memory hexutil.Bytes `json:"memory,omitempty"` + Meq *int `json:"meq,omitempty"` MemorySize int `json:"memSize"` Stack []hexutil.U256 `json:"stack"` ReturnData hexutil.Bytes `json:"returnData,omitempty"` @@ -38,6 +39,7 @@ func (s StructLog) MarshalJSON() ([]byte, error) { enc.Gas = math.HexOrDecimal64(s.Gas) enc.GasCost = math.HexOrDecimal64(s.GasCost) enc.Memory = s.Memory + enc.Meq = s.Meq enc.MemorySize = s.MemorySize if s.Stack != nil { enc.Stack = make([]hexutil.U256, len(s.Stack)) @@ -63,6 +65,7 @@ func (s *StructLog) UnmarshalJSON(input []byte) error { Gas *math.HexOrDecimal64 `json:"gas"` GasCost *math.HexOrDecimal64 `json:"gasCost"` Memory *hexutil.Bytes `json:"memory,omitempty"` + Meq *int `json:"meq,omitempty"` MemorySize *int `json:"memSize"` Stack []hexutil.U256 `json:"stack"` ReturnData *hexutil.Bytes `json:"returnData,omitempty"` @@ -90,6 +93,9 @@ func (s *StructLog) UnmarshalJSON(input []byte) error { if dec.Memory != nil { s.Memory = *dec.Memory } + if dec.Meq != nil { + s.Meq = dec.Meq + } if dec.MemorySize != nil { s.MemorySize = *dec.MemorySize } diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go index d94ffd3cf7..fd8ac4f973 100644 --- a/eth/tracers/logger/logger.go +++ b/eth/tracers/logger/logger.go @@ -17,6 +17,7 @@ package logger import ( + "bytes" "encoding/hex" "encoding/json" "fmt" @@ -49,12 +50,13 @@ func (s Storage) Copy() Storage { // Config are the configuration options for structured logger the EVM type Config struct { - EnableMemory bool // enable memory capture - DisableStack bool // disable stack capture - DisableStorage bool // disable storage capture - EnableReturnData bool // enable return data capture - Debug bool // print output during capture end - Limit int // maximum length of output, but zero means unlimited + EnableMemory bool // enable memory capture + DisableStack bool // disable stack capture + DisableStorage bool // disable storage capture + EnableReturnData bool // enable return data capture + Debug bool // print output during capture end + Limit int // maximum length of output, but zero means unlimited + MemoryCompressionWindow int // Chain overrides, can be used to execute a trace using future fork rules Overrides *params.ChainConfig `json:"overrides,omitempty"` } @@ -69,6 +71,7 @@ type StructLog struct { Gas uint64 `json:"gas"` GasCost uint64 `json:"gasCost"` Memory []byte `json:"memory,omitempty"` + Meq *int `json:"meq,omitempty"` MemorySize int `json:"memSize"` Stack []uint256.Int `json:"stack"` ReturnData []byte `json:"returnData,omitempty"` @@ -83,6 +86,7 @@ type structLogMarshaling struct { Gas math.HexOrDecimal64 GasCost math.HexOrDecimal64 Memory hexutil.Bytes + Meq *int `json:"meq,omitempty"` ReturnData hexutil.Bytes Stack []hexutil.U256 OpName string `json:"opName"` // adds call to OpName() in MarshalJSON @@ -118,6 +122,10 @@ type StructLogger struct { gasLimit uint64 usedGas uint64 + prevMem [][]byte + prevMemWindow int + prevMemIdx int + interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption } @@ -129,6 +137,9 @@ func NewStructLogger(cfg *Config) *StructLogger { } if cfg != nil { logger.cfg = *cfg + logger.prevMemWindow = cfg.MemoryCompressionWindow + logger.prevMemIdx = 0 + logger.prevMem = make([][]byte, cfg.MemoryCompressionWindow) } return logger } @@ -164,9 +175,36 @@ func (l *StructLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, s contract := scope.Contract // Copy a snapshot of the current memory state to a new buffer var mem []byte + var meq *int if l.cfg.EnableMemory { mem = make([]byte, len(memory.Data())) copy(mem, memory.Data()) + + foundEq := false + if l.prevMemWindow > 0 { + i := l.prevMemIdx + for dist := 1; dist <= l.prevMemWindow; dist++ { + if i--; i < 0 { + i = l.prevMemWindow - 1 + } + if len(l.prevMem[i]) == len(mem) && bytes.Equal(l.prevMem[i], mem) { + foundEq = true + meq = new(int) + *meq = dist + mem = nil + break + } + } + if l.prevMemIdx++; l.prevMemIdx == l.prevMemWindow { + l.prevMemIdx = 0 + } + if foundEq { + l.prevMem[l.prevMemIdx] = l.prevMem[i] + } else { + l.prevMem[l.prevMemIdx] = make([]byte, len(mem)) + copy(l.prevMem[l.prevMemIdx], mem) + } + } } // Copy a snapshot of the current stack state to a new buffer var stck []uint256.Int @@ -210,7 +248,7 @@ func (l *StructLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, s copy(rdata, rData) } // create a new snapshot of the EVM. - log := StructLog{pc, op, gas, cost, mem, memory.Len(), stck, rdata, storage, depth, l.env.StateDB.GetRefund(), err} + log := StructLog{pc, op, gas, cost, mem, meq, memory.Len(), stck, rdata, storage, depth, l.env.StateDB.GetRefund(), err} l.logs = append(l.logs, log) } @@ -422,6 +460,7 @@ type StructLogRes struct { Stack *[]string `json:"stack,omitempty"` ReturnData string `json:"returnData,omitempty"` Memory *[]string `json:"memory,omitempty"` + Meq *int `json:"meq,omitempty"` Storage *map[string]string `json:"storage,omitempty"` RefundCounter uint64 `json:"refund,omitempty"` } @@ -438,6 +477,7 @@ func formatLogs(logs []StructLog) []StructLogRes { Depth: trace.Depth, Error: trace.ErrorString(), RefundCounter: trace.RefundCounter, + Meq: trace.Meq, } if trace.Stack != nil { stack := make([]string, len(trace.Stack)) diff --git a/eth/tracers/sentio/gen_account_json.go b/eth/tracers/sentio/gen_account_json.go new file mode 100644 index 0000000000..e5498234a6 --- /dev/null +++ b/eth/tracers/sentio/gen_account_json.go @@ -0,0 +1,74 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package sentio + +import ( + "encoding/json" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var _ = (*accountMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (a account) MarshalJSON() ([]byte, error) { + type account struct { + Balance *hexutil.Big `json:"balance,omitempty"` + Code hexutil.Bytes `json:"code,omitempty"` + Nonce uint64 `json:"nonce,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` + CodeAddress *common.Address `json:"codeAddress,omitempty"` + CodeAddressBySlot map[common.Hash]*common.Address `json:"codeAddressBySlot,omitempty"` + MappingKeys map[string]string `json:"mappingKeys,omitempty"` + } + var enc account + enc.Balance = (*hexutil.Big)(a.Balance) + enc.Code = a.Code + enc.Nonce = a.Nonce + enc.Storage = a.Storage + enc.CodeAddress = a.CodeAddress + enc.CodeAddressBySlot = a.CodeAddressBySlot + enc.MappingKeys = a.MappingKeys + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (a *account) UnmarshalJSON(input []byte) error { + type account struct { + Balance *hexutil.Big `json:"balance,omitempty"` + Code *hexutil.Bytes `json:"code,omitempty"` + Nonce *uint64 `json:"nonce,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` + CodeAddress *common.Address `json:"codeAddress,omitempty"` + CodeAddressBySlot map[common.Hash]*common.Address `json:"codeAddressBySlot,omitempty"` + MappingKeys map[string]string `json:"mappingKeys,omitempty"` + } + var dec account + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Balance != nil { + a.Balance = (*big.Int)(dec.Balance) + } + if dec.Code != nil { + a.Code = *dec.Code + } + if dec.Nonce != nil { + a.Nonce = *dec.Nonce + } + if dec.Storage != nil { + a.Storage = dec.Storage + } + if dec.CodeAddress != nil { + a.CodeAddress = dec.CodeAddress + } + if dec.CodeAddressBySlot != nil { + a.CodeAddressBySlot = dec.CodeAddressBySlot + } + if dec.MappingKeys != nil { + a.MappingKeys = dec.MappingKeys + } + return nil +} diff --git a/eth/tracers/sentio/prestate.go b/eth/tracers/sentio/prestate.go new file mode 100644 index 0000000000..9d159e0fd8 --- /dev/null +++ b/eth/tracers/sentio/prestate.go @@ -0,0 +1,323 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package sentio + +import ( + "bytes" + "encoding/json" + "math/big" + "sync/atomic" + + "github.com/ava-labs/coreth/core/vm" + "github.com/ava-labs/coreth/eth/tracers" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" +) + +//go:generate go run github.com/fjl/gencodec -type account -field-override accountMarshaling -out gen_account_json.go + +func init() { + tracers.DefaultDirectory.Register("sentioPrestateTracer", newSentioPrestateTracer, false) +} + +type state = map[common.Address]*account + +type account struct { + Balance *big.Int `json:"balance,omitempty"` + Code []byte `json:"code,omitempty"` + Nonce uint64 `json:"nonce,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` + CodeAddress *common.Address `json:"codeAddress,omitempty"` + CodeAddressBySlot map[common.Hash]*common.Address `json:"codeAddressBySlot,omitempty"` + MappingKeys map[string]string `json:"mappingKeys,omitempty"` +} + +func (a *account) exists() bool { + return a.Nonce > 0 || len(a.Code) > 0 || len(a.Storage) > 0 || (a.Balance != nil && a.Balance.Sign() != 0) +} + +type accountMarshaling struct { + Balance *hexutil.Big + Code hexutil.Bytes +} + +type sentioPrestateTracer struct { + env *vm.EVM + pre state + post state + create bool + to common.Address + gasLimit uint64 // Amount of gas bought for the whole tx + config prestateTracerConfig + interrupt uint32 // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption + created map[common.Address]bool + deleted map[common.Address]bool +} + +func (t *sentioPrestateTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + //TODO implement me +} + +func (t *sentioPrestateTracer) CaptureExit(output []byte, usedGas uint64, err error) { + //TODO implement me +} + +func (t *sentioPrestateTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { + //TODO implement me +} + +type prestateTracerConfig struct { + DiffMode bool `json:"diffMode"` // If true, this tracer will return state modifications +} + +func newSentioPrestateTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { + var config prestateTracerConfig + if cfg != nil { + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + } + return &sentioPrestateTracer{ + pre: state{}, + post: state{}, + config: config, + created: make(map[common.Address]bool), + deleted: make(map[common.Address]bool), + }, nil +} + +// CaptureStart implements the EVMLogger interface to initialize the tracing operation. +func (t *sentioPrestateTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + t.create = create + t.to = to + + t.lookupAccount(from) + t.lookupAccount(to) + t.lookupAccount(env.Context.Coinbase) + + // The recipient balance includes the value transferred. + toBal := new(big.Int).Sub(t.pre[to].Balance, value) + t.pre[to].Balance = toBal + + // The sender balance is after reducing: value and gasLimit. + // We need to re-add them to get the pre-tx balance. + fromBal := new(big.Int).Set(t.pre[from].Balance) + gasPrice := env.TxContext.GasPrice + consumedGas := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(t.gasLimit)) + fromBal.Add(fromBal, new(big.Int).Add(value, consumedGas)) + t.pre[from].Balance = fromBal + t.pre[from].Nonce-- + + if create && t.config.DiffMode { + t.created[to] = true + } +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *sentioPrestateTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { + if t.config.DiffMode { + return + } + + if t.create { + // Keep existing account prior to contract creation at that address + if s := t.pre[t.to]; s != nil && !s.exists() { + // Exclude newly created contract. + delete(t.pre, t.to) + } + } +} + +// CaptureState implements the EVMLogger interface to trace a single step of VM execution. +func (t *sentioPrestateTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + stack := scope.Stack + stackData := stack.Data() + stackLen := len(stackData) + caller := scope.Contract.Address() + switch { + case stackLen >= 2 && op == vm.KECCAK256: + size := stackData[stackLen-2] + if size.Uint64() == 64 { + offset := stackData[stackLen-1] + rawkey := scope.Memory.GetCopy(int64(offset.Uint64()), 64) + + // only cares 64 bytes for mapping key + hashOfKey := crypto.Keccak256(rawkey) + t.pre[caller].MappingKeys[common.Bytes2Hex(rawkey)] = "0x" + common.Bytes2Hex(hashOfKey) + + baseSlot := rawkey[32:] + t.pre[caller].CodeAddressBySlot[common.BytesToHash(baseSlot)] = scope.Contract.CodeAddr + t.pre[caller].CodeAddressBySlot[common.BytesToHash(hashOfKey)] = scope.Contract.CodeAddr + } + case stackLen >= 1 && (op == vm.SLOAD || op == vm.SSTORE): + slot := common.Hash(stackData[stackLen-1].Bytes32()) + t.pre[caller].CodeAddress = scope.Contract.CodeAddr + t.pre[caller].CodeAddressBySlot[slot] = scope.Contract.CodeAddr + t.lookupStorage(caller, slot) + case stackLen >= 1 && (op == vm.EXTCODECOPY || op == vm.EXTCODEHASH || op == vm.EXTCODESIZE || op == vm.BALANCE || op == vm.SELFDESTRUCT): + addr := common.Address(stackData[stackLen-1].Bytes20()) + t.lookupAccount(addr) + if op == vm.SELFDESTRUCT { + t.deleted[caller] = true + } + case stackLen >= 5 && (op == vm.DELEGATECALL || op == vm.CALL || op == vm.STATICCALL || op == vm.CALLCODE): + addr := common.Address(stackData[stackLen-2].Bytes20()) + t.lookupAccount(addr) + case op == vm.CREATE: + nonce := t.env.StateDB.GetNonce(caller) + addr := crypto.CreateAddress(caller, nonce) + t.lookupAccount(addr) + t.created[addr] = true + case stackLen >= 4 && op == vm.CREATE2: + offset := stackData[stackLen-2] + size := stackData[stackLen-3] + init := scope.Memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) + inithash := crypto.Keccak256(init) + salt := stackData[stackLen-4] + addr := crypto.CreateAddress2(caller, salt.Bytes32(), inithash) + t.lookupAccount(addr) + t.created[addr] = true + } +} + +func (t *sentioPrestateTracer) CaptureTxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +func (t *sentioPrestateTracer) CaptureTxEnd(restGas uint64) { + if !t.config.DiffMode { + return + } + + for addr, state := range t.pre { + // The deleted account's state is pruned from `post` but kept in `pre` + if _, ok := t.deleted[addr]; ok { + continue + } + modified := false + postAccount := &account{Storage: make(map[common.Hash]common.Hash)} + newBalance := t.env.StateDB.GetBalance(addr) + newNonce := t.env.StateDB.GetNonce(addr) + newCode := t.env.StateDB.GetCode(addr) + postAccount.CodeAddress = state.CodeAddress + postAccount.MappingKeys = t.pre[addr].MappingKeys + + if newBalance.Cmp(t.pre[addr].Balance) != 0 { + modified = true + postAccount.Balance = newBalance + } + if newNonce != t.pre[addr].Nonce { + modified = true + postAccount.Nonce = newNonce + } + if !bytes.Equal(newCode, t.pre[addr].Code) { + modified = true + postAccount.Code = newCode + } + + for key, val := range state.Storage { + // don't include the empty slot + if val == (common.Hash{}) { + delete(t.pre[addr].Storage, key) + } + + newVal := t.env.StateDB.GetState(addr, key) + if val == newVal { + // Omit unchanged slots + delete(t.pre[addr].Storage, key) + } else { + modified = true + if newVal != (common.Hash{}) { + postAccount.Storage[key] = newVal + } + } + } + + if modified { + t.post[addr] = postAccount + } else { + // if state is not modified, then no need to include into the pre state + delete(t.pre, addr) + } + } + // the new created contracts' prestate were empty, so delete them + for a := range t.created { + // the created contract maybe exists in statedb before the creating tx + if s := t.pre[a]; s != nil && !s.exists() { + delete(t.pre, a) + } + } +} + +// GetResult returns the json-encoded nested list of call traces, and any +// error arising from the encoding or forceful termination (via `Stop`). +func (t *sentioPrestateTracer) GetResult() (json.RawMessage, error) { + var res []byte + var err error + if t.config.DiffMode { + res, err = json.Marshal(struct { + Post state `json:"post"` + Pre state `json:"pre"` + }{t.post, t.pre}) + } else { + res, err = json.Marshal(struct { + Pre state `json:"pre"` + }{t.pre}) + } + if err != nil { + return nil, err + } + return json.RawMessage(res), t.reason +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *sentioPrestateTracer) Stop(err error) { + t.reason = err + atomic.StoreUint32(&t.interrupt, 1) +} + +// lookupAccount fetches details of an account and adds it to the prestate +// if it doesn't exist there. +func (t *sentioPrestateTracer) lookupAccount(addr common.Address) { + if _, ok := t.pre[addr]; ok { + return + } + + t.pre[addr] = &account{ + Balance: t.env.StateDB.GetBalance(addr), + Nonce: t.env.StateDB.GetNonce(addr), + Code: t.env.StateDB.GetCode(addr), + Storage: make(map[common.Hash]common.Hash), + MappingKeys: make(map[string]string), + CodeAddressBySlot: make(map[common.Hash]*common.Address), + } +} + +// lookupStorage fetches the requested storage slot and adds +// it to the prestate of the given contract. It assumes `lookupAccount` +// has been performed on the contract before. +func (t *sentioPrestateTracer) lookupStorage(addr common.Address, key common.Hash) { + if _, ok := t.pre[addr].Storage[key]; ok { + return + } + t.pre[addr].Storage[key] = t.env.StateDB.GetState(addr, key) +} + +func (*sentioPrestateTracer) CapturePreimage(pc uint64, hash common.Hash, preimage []byte) {} diff --git a/eth/tracers/sentio/tracer.go b/eth/tracers/sentio/tracer.go new file mode 100644 index 0000000000..2303ddf2b3 --- /dev/null +++ b/eth/tracers/sentio/tracer.go @@ -0,0 +1,678 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package sentio + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "sync/atomic" + + "github.com/ava-labs/coreth/accounts/abi" + "github.com/ava-labs/coreth/core/vm" + "github.com/ava-labs/coreth/eth/tracers" + "github.com/ava-labs/coreth/vmerrs" + corestate "github.com/ava-labs/coreth/core/state" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/log" + "github.com/holiman/uint256" +) + +type functionInfo struct { + address string + Name string `json:"name"` + SignatureHash string `json:"signatureHash"` + + Pc uint64 `json:"pc"` + InputSize int `json:"inputSize"` + InputMemory bool `json:"inputMemory"` + OutputSize int `json:"outputSize"` + OutputMemory bool `json:"outputMemory"` +} + +type sentioTracerConfig struct { + Functions map[string][]functionInfo `json:"functions"` + Calls map[string][]uint64 `json:"calls"` + Debug bool `json:"debug"` + WithInternalCalls bool `json:"withInternalCalls"` +} + +func init() { + tracers.DefaultDirectory.Register("sentioTracer", NewSentioTracer, false) +} + +type Trace struct { + // only in debug mode + Name string `json:"name,omitempty"` + + Type string `json:"type"` + Pc uint64 `json:"pc"` + // Global index of the trace + StartIndex int `json:"startIndex"` + EndIndex int `json:"endIndex"` + + // Gas remaining before the OP + Gas math.HexOrDecimal64 `json:"gas"` + // Gas for the entire call + GasUsed math.HexOrDecimal64 `json:"gasUsed"` + + From *common.Address `json:"from,omitempty"` + // Used by call + To *common.Address `json:"to,omitempty"` + // Input + Input string `json:"input,omitempty"` // TODO better struct it and make it bytes + // Ether transfered + Value *hexutil.Big `json:"value,omitempty"` + // Return for calls + Output hexutil.Bytes `json:"output,omitempty"` + Error string `json:"error,omitempty"` + Revertal string `json:"revertReason,omitempty"` + + // Used by jump + InputStack []uint256.Int `json:"inputStack,omitempty"` + InputMemory *[]string `json:"inputMemory,omitempty"` + OutputStack []uint256.Int `json:"outputStack,omitempty"` + OutputMemory *[]string `json:"outputMemory,omitempty"` + FunctionPc uint64 `json:"functionPc,omitempty"` + + // Used by log + Address *common.Address `json:"address,omitempty"` + CodeAddress *common.Address `json:"codeAddress,omitempty"` + Data hexutil.Bytes `json:"data,omitempty"` + + Topics []common.Hash `json:"topics,omitempty"` + + // Only used by root + Traces []Trace `json:"traces,omitempty"` + + // Use for internal call stack organization + // The jump to go into the function + //enterPc uint64 + exitPc uint64 + + // the function get called + function *functionInfo +} + +type Receipt struct { + Nonce uint64 `json:"nonce"` + TxHash *common.Hash `json:"transactionHash,omitempty"` + BlockNumber *hexutil.Big `json:"blockNumber,omitempty"` + BlockHash *common.Hash `json:"blockHash,omitempty"` + TransactionIndex uint `json:"transactionIndex"` +} + +type sentioTracer struct { + config sentioTracerConfig + env *vm.EVM + activePrecompiles []common.Address // Updated on CaptureStart based on given rules + + functionMap map[string]map[uint64]functionInfo + callMap map[string]map[uint64]bool + receipt Receipt + + previousJump *Trace + index int + entryPc map[uint64]bool + + callstack []Trace + gasLimit uint64 + + interrupt uint32 // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +func (t *sentioTracer) CaptureTxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +func (t *sentioTracer) CaptureTxEnd(restGas uint64) { + if len(t.callstack) == 0 { + return + } + t.callstack[0].EndIndex = t.index + t.callstack[0].GasUsed = math.HexOrDecimal64(t.gasLimit - restGas) + if t.callstack[0].StartIndex == -1 { + // It's possible that we can't correctly locate the PC that match the entry function (check why), in this case we need to 0 for the user + t.callstack[0].StartIndex = 0 + } +} + +func (t *sentioTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + t.receipt.BlockNumber = (*hexutil.Big)(env.Context.BlockNumber) + // TODO this current will block the tracer + + // TODO bockHash & txHash + t.receipt.Nonce = env.StateDB.GetNonce(from) - 1 + if ibs, ok := env.StateDB.(*corestate.StateDB); ok { + t.receipt.TransactionIndex = uint(ibs.TxIndex()) + } + + rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Time) + t.activePrecompiles = vm.ActivePrecompiles(rules) + + root := Trace{ + StartIndex: -1, + Type: vm.CALL.String(), + From: &from, + To: &to, + Gas: math.HexOrDecimal64(gas), + Input: hexutil.Bytes(input).String(), + } + if value != nil { + root.Value = (*hexutil.Big)(value) + } + if create { + root.Type = vm.CREATE.String() + } + + if !create && !t.isPrecompiled(to) && len(input) >= 4 { + m, ok := t.functionMap[to.String()] + if ok { + sigHash := "0x" + common.Bytes2Hex(input[0:4]) + for pc, fn := range m { + if fn.SignatureHash == sigHash { + t.entryPc[pc] = true + } + } + log.Info(fmt.Sprintf("entry pc match %s (%d times) ", sigHash, len(t.entryPc))) + } + } + t.callstack = append(t.callstack, root) +} + +func (t *sentioTracer) CaptureEnd(output []byte, usedGas uint64, err error) { + t.callstack[0].EndIndex = t.index + t.callstack[0].GasUsed = math.HexOrDecimal64(usedGas) + t.callstack[0].Output = common.CopyBytes(output) + + stackSize := len(t.callstack) + t.popStack(1, output, uint64(t.callstack[stackSize-1].Gas)-usedGas, err) + + t.callstack[0].processError(output, err) +} + +func (t *sentioTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + // Skip if tracing was interrupted + if atomic.LoadUint32(&t.interrupt) > 0 { + return + } + + if typ == vm.CALL || typ == vm.CALLCODE { + // After enter, make the assumped transfer as function call + topElementTraces := t.callstack[len(t.callstack)-1].Traces + call := topElementTraces[len(topElementTraces)-1] + topElementTraces = topElementTraces[:len(topElementTraces)-1] + t.callstack[len(t.callstack)-1].Traces = topElementTraces + t.callstack = append(t.callstack, call) + } + + size := len(t.callstack) + + t.callstack[size-1].From = &from + t.callstack[size-1].To = &to + t.callstack[size-1].Input = hexutil.Bytes(input).String() + t.callstack[size-1].Gas = math.HexOrDecimal64(gas) + + if value != nil { + t.callstack[size-1].Value = (*hexutil.Big)(value) + } +} + +func (t *sentioTracer) CaptureExit(output []byte, usedGas uint64, err error) { + size := len(t.callstack) + if size <= 1 { + return + } + + //log.Info(fmt.Sprintf("CaptureExit pop frame %s", t.callstack[size-1].Type)) + + stackSize := len(t.callstack) + for i := stackSize - 1; i >= 0; i-- { + if t.callstack[i].function != nil { + continue + } + + if stackSize-i > 1 { + log.Info(fmt.Sprintf("tail call optimization [external] size %d", stackSize-i)) + } + + call := &t.callstack[i] + //call.EndIndex = t.index + //call.GasUsed = math.HexOrDecimal64(usedGas) + call.processError(output, err) + + t.popStack(i, output, uint64(call.Gas)-usedGas, err) + return + } + + log.Error(fmt.Sprintf("failed to pop stack")) +} + +func (t *sentioTracer) popStack(to int, output []byte, currentGas uint64, err error) { // , scope *vm.ScopeContext + stackSize := len(t.callstack) + for j := stackSize - 1; j >= to; j-- { + t.callstack[j].Output = common.CopyBytes(output) + t.callstack[j].EndIndex = t.index + t.callstack[j].GasUsed = math.HexOrDecimal64(uint64(t.callstack[j].Gas) - currentGas) + + // TODO consider pass scopeContext so that popStack also record this + //if t.callstack[j].function != nil { + // t.callstack[j].OutputStack = copyStack(scope.Stack, t.callstack[j].function.OutputSize) + // if t.callstack[j].function.OutputMemory { + // t.callstack[j].OutputMemory = formatMemory(scope.Memory) + // } + //} + //if err != nil { + // t.callstack[j].Error = err.Error() + //} + t.callstack[j-1].Traces = append(t.callstack[j-1].Traces, t.callstack[j]) + } + + t.callstack = t.callstack[:to] +} + +func (t *sentioTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + // Skip if tracing was interrupted + if atomic.LoadUint32(&t.interrupt) > 0 { + return + } + t.index++ + + if t.callstack[0].StartIndex == -1 && t.entryPc[pc] { + //fillback the index and PC for root + t.callstack[0].Pc = pc + t.callstack[0].StartIndex = t.index - 1 + t.previousJump = nil + return + } + + var mergeBase = func(trace Trace) Trace { + trace.Pc = pc + trace.Type = op.String() + trace.Gas = math.HexOrDecimal64(gas) + trace.StartIndex = t.index - 1 + trace.EndIndex = t.index + + // Assume it's single instruction, adjust it for jump and call + trace.GasUsed = math.HexOrDecimal64(cost) + if err != nil { + // set error for instruction + trace.Error = err.Error() + } + return trace + } + + switch op { + case vm.CALL, vm.CALLCODE: + call := mergeBase(Trace{}) + call.Gas = math.HexOrDecimal64(scope.Stack.Back(0).Uint64()) + from := scope.Contract.Address() + call.From = &from + call.CodeAddress = scope.Contract.CodeAddr + to := common.BigToAddress(scope.Stack.Back(1).ToBig()) + call.To = &to + call.Value = (*hexutil.Big)(scope.Stack.Back(2).ToBig()) + + v := call.Value.ToInt() + if v.BitLen() != 0 && !t.env.Context.CanTransfer(t.env.StateDB, from, v) { + if call.Error == "" { + call.Error = "insufficient funds for transfer" + } + } + + // Treat this call as pure transfer until it enters the CaptureEnter + t.callstack[len(t.callstack)-1].Traces = append(t.callstack[len(t.callstack)-1].Traces, call) + case vm.CREATE, vm.CREATE2, vm.DELEGATECALL, vm.STATICCALL, vm.SELFDESTRUCT: + // more info to be add at CaptureEnter + call := mergeBase(Trace{}) + t.callstack = append(t.callstack, call) + case vm.LOG0, vm.LOG1, vm.LOG2, vm.LOG3, vm.LOG4: + topicCount := int(op - vm.LOG0) + logOffset := scope.Stack.Back(0) + logSize := scope.Stack.Back(1) + data := copyMemory(scope.Memory, logOffset, logSize) + var topics []common.Hash + //stackLen := scope.Stack.Len() + for i := 0; i < topicCount; i++ { + topics = append(topics, scope.Stack.Back(2+i).Bytes32()) + } + addr := scope.Contract.Address() + l := mergeBase(Trace{ + Address: &addr, + CodeAddress: scope.Contract.CodeAddr, + Data: data, + Topics: topics, + }) + t.callstack[len(t.callstack)-1].Traces = append(t.callstack[len(t.callstack)-1].Traces, l) + case vm.JUMP: + if !t.config.WithInternalCalls { + break + } + from := scope.Contract.CodeAddr + codeAddress := scope.Contract.CodeAddr + + jump := mergeBase(Trace{ + From: from, + CodeAddress: codeAddress, + //InputStack: append([]uint256.Int(nil), scope.Stack.Data...), // TODO only need partial + }) + if t.previousJump != nil { + log.Error("Unexpected previous jump", t.previousJump) + } + if err == nil { + t.previousJump = &jump + } else { + log.Error("error in jump", "err", err) + // error happend, attach to current frame + t.callstack[len(t.callstack)-1].Traces = append(t.callstack[len(t.callstack)-1].Traces, jump) + } + case vm.JUMPDEST: + if !t.config.WithInternalCalls { + break + } + from := scope.Contract.CodeAddr + fromStr := from.String() + + if t.previousJump != nil { // vm.JumpDest and match with a previous jump (otherwise it's a jumpi) + defer func() { + t.previousJump = nil + }() + // Check if this is return + // TODO pontentially maintain a map for fast filtering + //log.Info("fromStr" + fromStr + ", callstack size" + fmt.Sprint(len(t.callStack))) + stackSize := len(t.callstack) + + // Part 1: try process the trace as function call exit + for i := stackSize - 1; i >= 0; i-- { + // process internal call within the same contract + // no function info means another external call + functionInfo := t.callstack[i].function + if functionInfo == nil { + break + } + + if functionInfo.address != fromStr { + break + } + + // find a match + if t.callstack[i].exitPc == pc { + // find a match, pop the stack, copy memory if needed + + if stackSize-i > 1 { + log.Info(fmt.Sprintf("tail call optimization size %d", stackSize-i)) + } + + // TODO maybe don't need return all + for j := stackSize - 1; j >= i; j-- { + call := &t.callstack[j] + functionJ := call.function + call.EndIndex = t.index - 1 // EndIndex should before the jumpdest + call.GasUsed = math.HexOrDecimal64(uint64(t.callstack[j].Gas) - gas) + if functionJ.OutputSize > len(scope.Stack.Data()) { + log.Error(fmt.Sprintf("stack size not enough (%d vs %d) for function %s %s. pc: %d", + len(scope.Stack.Data()), functionJ.OutputSize, functionJ.address, functionJ.Name, pc)) + if err == nil { + log.Error("stack size not enough has error", "err", err) + } + } else { + call.OutputStack = copyStack(scope.Stack, t.callstack[j].function.OutputSize) + } + if call.function.OutputMemory { + call.OutputMemory = formatMemory(scope.Memory) + } + //if err != nil { + // call.Error = err.Error() + //} + t.callstack[j-1].Traces = append(t.callstack[j-1].Traces, *call) + } + t.callstack = t.callstack[:i] + return + } + } + + // Part 2: try process the trace as function call entry + funcInfo := t.getFunctionInfo(fromStr, pc) + //log.Info("function info" + fmt.Sprint(funcInfo)) + + if funcInfo != nil { + // filter those jump are not call site + if !t.isCall(t.previousJump.From.String(), t.previousJump.Pc) { + return + } + + if funcInfo.InputSize >= len(scope.Stack.Data()) { + // TODO this check should not needed after frist check + log.Error("Unexpected stack size for function:" + fmt.Sprint(funcInfo) + ", stack" + fmt.Sprint(scope.Stack.Data)) + log.Error("previous jump" + fmt.Sprint(*t.previousJump)) + return + } + + // confirmed that we are in an internal call + //t.internalCallStack = append(t.internalCallStack, internalCallStack{ + // enterPc: t.previousJump.Pc, + // exitPc: scope.Stack.Back(funcInfo.InputSize).Uint64(), + // function: funcInfo, + //}) + //jump.enterPc = t.previousJump.Pc + t.previousJump.exitPc = scope.Stack.Back(funcInfo.InputSize).Uint64() + t.previousJump.function = funcInfo + t.previousJump.FunctionPc = pc + t.previousJump.InputStack = copyStack(scope.Stack, funcInfo.InputSize) + if t.config.Debug { + t.previousJump.Name = funcInfo.Name + } + if funcInfo.InputMemory { + t.previousJump.InputMemory = formatMemory(scope.Memory) + } + t.callstack = append(t.callstack, *t.previousJump) + //t.callstack = append(t.callstack, callStack{ + } + } + case vm.REVERT: + if !t.config.WithInternalCalls { + break + } + logOffset := scope.Stack.Back(0) + logSize := scope.Stack.Back(1) + output := scope.Memory.GetPtr(int64(logOffset.Uint64()), int64(logSize.Uint64())) + //data := copyMemory(logOffset, logSize) + + trace := mergeBase(Trace{ + Output: output, + Error: "execution reverted", + }) + if unpacked, err := abi.UnpackRevert(output); err == nil { + trace.Revertal = unpacked + } + t.callstack[len(t.callstack)-1].Traces = append(t.callstack[len(t.callstack)-1].Traces, trace) + default: + if !t.config.WithInternalCalls { + break + } + if err != nil { + // Error happen, attach the error OP if not already processed + t.callstack[len(t.callstack)-1].Traces = append(t.callstack[len(t.callstack)-1].Traces, mergeBase(Trace{})) + } + } +} +func (t *sentioTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { +} + +// CapturePreimage records a SHA3 preimage discovered during execution. +func (t *sentioTracer) CapturePreimage(pc uint64, hash common.Hash, preimage []byte) {} + +func (t *sentioTracer) GetResult() (json.RawMessage, error) { + type RootTrace struct { + Trace + TracerConfig *sentioTracerConfig `json:"tracerConfig,omitempty"` + Receipt Receipt `json:"receipt"` + } + root := RootTrace{ + Trace: t.callstack[0], + Receipt: t.receipt, + } + if t.config.Debug { + root.TracerConfig = &t.config + } + + if len(t.callstack) != 1 { + log.Error("callstack length is not 1, is " + fmt.Sprint(len(t.callstack))) + } + + res, err := json.Marshal(root) + if err != nil { + return nil, err + } + return res, t.reason +} + +func (t *sentioTracer) Stop(err error) { + t.reason = err + atomic.StoreUint32(&t.interrupt, 1) +} + +func NewSentioTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { + //if name != "sentioTracer" { + // return nil, errors.New("no tracer found") + //} + + var config sentioTracerConfig + functionMap := map[string]map[uint64]functionInfo{} + callMap := map[string]map[uint64]bool{} + + if cfg != nil { + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + + for address, functions := range config.Functions { + checkSumAddress := common.HexToAddress(address).String() + functionMap[checkSumAddress] = make(map[uint64]functionInfo) + + for _, function := range functions { + function.address = checkSumAddress + functionMap[checkSumAddress][function.Pc] = function + } + } + + for address, calls := range config.Calls { + checkSumAddress := common.HexToAddress(address).String() + callMap[checkSumAddress] = make(map[uint64]bool) + + for _, call := range calls { + callMap[checkSumAddress][call] = true + } + } + + log.Info(fmt.Sprintf("create sentioTracer config with %d functions, %d calls", len(functionMap), len(callMap))) + } + + return &sentioTracer{ + config: config, + functionMap: functionMap, + callMap: callMap, + entryPc: map[uint64]bool{}, + }, nil +} + +func (t *sentioTracer) isPrecompiled(addr common.Address) bool { + for _, p := range t.activePrecompiles { + if p == addr { + return true + } + } + return false +} + +func (t *sentioTracer) getFunctionInfo(address string, pc uint64) *functionInfo { + m, ok := t.functionMap[address] + if !ok || m == nil { + return nil + } + info, ok := m[pc] + if ok { + return &info + } + + return nil +} + +func (t *sentioTracer) isCall(address string, pc uint64) bool { + m, ok := t.callMap[address] + if !ok || m == nil { + return false + } + info, ok := m[pc] + if ok { + return info + } + return false +} + +// Only used in non detail mode +func (f *Trace) processError(output []byte, err error) { + //output = common.CopyBytes(output) + if err == nil { + //f.Output = output + return + } + f.Error = err.Error() + if f.Type == vm.CREATE.String() || f.Type == vm.CREATE2.String() { + f.To = &common.Address{} + } + if !errors.Is(err, vmerrs.ErrExecutionReverted) || len(output) == 0 { + return + } + //f.Output = output + if len(output) < 4 { + return + } + if unpacked, err := abi.UnpackRevert(output); err == nil { + f.Revertal = unpacked + } +} + +func copyMemory(m *vm.Memory, offset *uint256.Int, size *uint256.Int) hexutil.Bytes { + // it's important to get copy + return m.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) +} + +func formatMemory(m *vm.Memory) *[]string { + res := make([]string, 0, (m.Len()+31)/32) + for i := 0; i+32 <= m.Len(); i += 32 { + res = append(res, fmt.Sprintf("%x", m.GetPtr(int64(i), 32))) + } + return &res +} + +func copyStack(s *vm.Stack, copySize int) []uint256.Int { + if copySize == 0 { + return nil + } + stackSize := len(s.Data()) + res := make([]uint256.Int, stackSize) + for i := stackSize - copySize; i < stackSize; i++ { + res[i] = s.Data()[i] + } + return res +} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 50606b0af7..1ab6cf1ab9 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -59,6 +59,7 @@ import ( // inside of cmd/geth. _ "github.com/ava-labs/coreth/eth/tracers/js" _ "github.com/ava-labs/coreth/eth/tracers/native" + _ "github.com/ava-labs/coreth/eth/tracers/sentio" "github.com/ava-labs/coreth/precompile/precompileconfig" // Force-load precompiles to trigger registration diff --git a/rpc/client.go b/rpc/client.go index 6c11365560..6871de012b 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -378,6 +378,7 @@ func (c *Client) CallContext(ctx context.Context, result interface{}, method str resp := batchresp[0] switch { case resp.Error != nil: + _ = json.Unmarshal(resp.Result, result) return resp.Error case len(resp.Result) == 0: return ErrNoResult