diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0c63be3f5c..6d050e5738 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v5 with: - go-version: "1.22" + go-version: "1.23.2" id: go - name: Check out code into the Go module directory @@ -44,16 +44,16 @@ jobs: - name: install golang uses: actions/setup-go@v5 with: - go-version: "1.22" + go-version: "1.23.2" - name: install rust-toolchain uses: actions-rs/toolchain@v1.0.7 with: - toolchain: stable + toolchain: 1.80.0 - name: install wasm-pack run: | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + curl https://raw.githubusercontent.com/rustwasm/wasm-pack/refs/heads/master/docs/_installer/init.sh -sSf | env VERSION=v0.13.0 sh - name: install schema run: | diff --git a/packages/evm/jsonrpc/evmchain.go b/packages/evm/jsonrpc/evmchain.go index 7f235058af..55790c49ce 100644 --- a/packages/evm/jsonrpc/evmchain.go +++ b/packages/evm/jsonrpc/evmchain.go @@ -4,16 +4,20 @@ package jsonrpc import ( + "encoding/json" "errors" "fmt" "math" "math/big" "path" + "slices" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "github.com/labstack/gommon/log" "github.com/samber/lo" @@ -662,43 +666,41 @@ func (e *EVMChain) iscRequestsInBlock(evmBlockNumber uint64) (*blocklog.BlockInf return blocklog.GetRequestsInBlock(blocklogStatePartition, iscBlockIndex) } -func (e *EVMChain) Trace(config *tracers.TraceConfig, txIndex *uint64, txHash common.Hash, blockNumber uint64, blockHash common.Hash) (any, error) { +func (e *EVMChain) isFakeTransaction(tx *types.Transaction) bool { + sender, err := evmutil.GetSender(tx) + + // the error will fire when the transaction is invalid. This is most of the time a fake evm tx we use for internal calls, therefore it's fine to assume both. + if slices.Equal(sender.Bytes(), common.Address{}.Bytes()) || err != nil { + return true + } + + return false +} + +// Trace allows the tracing of EVM transactions and considers "fake" evm transactions that are emitted when ISC internal requests are being made. (Transfer of funds from L1->L2EVM for example) +func (e *EVMChain) trace(config *tracers.TraceConfig, blockInfo *blocklog.BlockInfo, requestsInBlock []isc.Request, evmTxs types.Transactions, txIndex uint64, txHash common.Hash, blockHash common.Hash) (json.RawMessage, error) { tracerType := "callTracer" if config.Tracer != nil { tracerType = *config.Tracer } - iscBlock, iscRequestsInBlock, err := e.iscRequestsInBlock(blockNumber) - if err != nil { - return nil, err - } - - var blockTxs types.Transactions - var txi int - if txIndex != nil { - txi = int(*txIndex) - } else { - blockTxs, err = e.txsByBlockNumber(new(big.Int).SetUint64(blockNumber)) - if err != nil { - return nil, err - } - } + blockNumber := uint64(blockInfo.BlockIndex()) tracer, err := newTracer(tracerType, &tracers.Context{ BlockHash: blockHash, BlockNumber: new(big.Int).SetUint64(blockNumber), - TxIndex: txi, + TxIndex: int(txIndex), TxHash: txHash, - }, config.TracerConfig, blockTxs) + }, config.TracerConfig) if err != nil { return nil, err } err = e.backend.EVMTrace( - iscBlock.PreviousAliasOutput, - iscBlock.Timestamp, - iscRequestsInBlock, - txIndex, + blockInfo.PreviousAliasOutput, + blockInfo.Timestamp, + requestsInBlock, + &txIndex, &blockNumber, tracer, ) @@ -706,7 +708,64 @@ func (e *EVMChain) Trace(config *tracers.TraceConfig, txIndex *uint64, txHash co return nil, err } - return tracer.GetResult() + result, err := tracer.GetResult() + if err != nil { + if !errors.Is(err, ErrIncorrectTopLevelCalls) { + return nil, err + } + + tx, ok := lo.Find(evmTxs, func(tx *types.Transaction) bool { return slices.Equal(txHash.Bytes(), tx.Hash().Bytes()) }) + if !ok { + return nil, fmt.Errorf("can not find transaction: %v", txHash.String()) + } + + if e.isFakeTransaction(tx) { + return json.Marshal(RPCMarshalTransactionTraceForFakeTX(tx, tx.GasPrice())) + } + } + + return result, nil +} + +func (e *EVMChain) traceTransaction(config *tracers.TraceConfig, txIndex uint64, txHash common.Hash, blockNumber uint64, blockHash common.Hash) (any, error) { + iscBlock, iscRequestsInBlock, err := e.iscRequestsInBlock(blockNumber) + if err != nil { + return nil, err + } + + blockTxs, err := e.txsByBlockNumber(new(big.Int).SetUint64(blockNumber)) + if err != nil { + return nil, err + } + + return e.trace(config, iscBlock, iscRequestsInBlock, blockTxs, txIndex, txHash, blockHash) +} + +func (e *EVMChain) traceBlock(config *tracers.TraceConfig, block *types.Block) (any, error) { + iscBlock, iscRequestsInBlock, err := e.iscRequestsInBlock(block.NumberU64()) + if err != nil { + return nil, err + } + + blockTxs, err := e.txsByBlockNumber(new(big.Int).SetUint64(block.NumberU64())) + if err != nil { + return nil, err + } + + results := make([]TxTraceResult, 0) + for i, tx := range blockTxs { + result, err := e.trace(config, iscBlock, iscRequestsInBlock, blockTxs, uint64(i), tx.Hash(), block.Hash()) + + // Transactions which failed tracing will be omitted, so the rest of the block can be returned + if err == nil { + results = append(results, TxTraceResult{ + TxHash: tx.Hash(), + Result: result, + }) + } + } + + return results, nil } func (e *EVMChain) TraceTransaction(txHash common.Hash, config *tracers.TraceConfig) (any, error) { @@ -717,10 +776,10 @@ func (e *EVMChain) TraceTransaction(txHash common.Hash, config *tracers.TraceCon return nil, err } if blockNumber == 0 { - return nil, errors.New("tx not found") + return nil, errors.New("transaction not found") } - return e.Trace(config, &txIndex, txHash, blockNumber, blockHash) + return e.traceTransaction(config, txIndex, txHash, blockNumber, blockHash) } func (e *EVMChain) TraceBlockByHash(blockHash common.Hash, config *tracers.TraceConfig) (any, error) { @@ -728,10 +787,10 @@ func (e *EVMChain) TraceBlockByHash(blockHash common.Hash, config *tracers.Trace block := e.BlockByHash(blockHash) if block == nil { - return nil, errors.New("block not found") + return nil, fmt.Errorf("block not found: %s", blockHash.String()) } - return e.Trace(config, nil, common.Hash{}, block.Number().Uint64(), blockHash) + return e.traceBlock(config, block) } func (e *EVMChain) TraceBlockByNumber(blockNumber uint64, config *tracers.TraceConfig) (any, error) { @@ -739,22 +798,56 @@ func (e *EVMChain) TraceBlockByNumber(blockNumber uint64, config *tracers.TraceC block, err := e.BlockByNumber(big.NewInt(int64(blockNumber))) if err != nil { - return nil, fmt.Errorf("block not found: %w", err) + return nil, fmt.Errorf("block not found: %d", blockNumber) } - return e.Trace(config, nil, common.Hash{}, blockNumber, block.Hash()) + return e.traceBlock(config, block) } -func (e *EVMChain) GetBlockReceipts(blockNumber rpc.BlockNumber) ([]*types.Receipt, error) { - e.log.Debugf("GetBlockReceipts(blockNumber=%v)", blockNumber) - bn := parseBlockNumber(blockNumber) - chainState, err := e.iscStateFromEVMBlockNumber(bn) +func (e *EVMChain) getBlockByNumberOrHash(blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) { + if h, ok := blockNrOrHash.Hash(); ok { + return e.BlockByHash(h), nil + } else if n, ok := blockNrOrHash.Number(); ok { + switch n { + case rpc.LatestBlockNumber: + return e.BlockByNumber(nil) + default: + if n < 0 { + return nil, fmt.Errorf("%v is unsupported", blockNrOrHash.String()) + } + + return e.BlockByNumber(big.NewInt(n.Int64())) + } + } + + return nil, fmt.Errorf("block not found: %v", blockNrOrHash.String()) +} + +func (e *EVMChain) GetRawBlock(blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { + block, err := e.getBlockByNumberOrHash(blockNrOrHash) if err != nil { return nil, err } + return rlp.EncodeToBytes(block) +} + +func (e *EVMChain) GetBlockReceipts(blockNrOrHash rpc.BlockNumberOrHash) ([]*types.Receipt, []*types.Transaction, error) { + e.log.Debugf("GetBlockReceipts(blockNumber=%v)", blockNrOrHash.String()) + + block, err := e.getBlockByNumberOrHash(blockNrOrHash) + if err != nil { + return nil, nil, err + } + + chainState, err := e.iscStateFromEVMBlockNumber(block.Number()) + if err != nil { + return nil, nil, err + } + db := blockchainDB(chainState) - return db.GetReceiptsByBlockNumber(bn.Uint64()), nil + + return db.GetReceiptsByBlockNumber(block.NumberU64()), db.GetTransactionsByBlockNumber(block.NumberU64()), nil } var maxUint32 = big.NewInt(math.MaxUint32) diff --git a/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go b/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go index 335f7d8120..afa7190ec7 100644 --- a/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go +++ b/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go @@ -6,6 +6,7 @@ package jsonrpctest import ( "context" "encoding/json" + "fmt" "math/big" "slices" "strings" @@ -589,7 +590,7 @@ func TestRPCTraceTx(t *testing.T) { require.Equal(t, "0x7b", trace1.Value.String()) expectedInput, err := contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(1)) require.NoError(t, err) - require.Equal(t, expectedInput, trace1.Input) + require.Equal(t, expectedInput, []byte(trace1.Input)) require.Empty(t, trace1.Error) require.Empty(t, trace1.RevertReason) require.Equal(t, "0x0", trace1.Gas.String()) @@ -607,6 +608,8 @@ func TestRPCTraceTx(t *testing.T) { require.Contains(t, trace2.GasUsed.String(), "0x") } +// Transfer calls produce "fake" Transactions to simulate EVM behavior. +// They are not real in the sense of being persisted to the blockchain, therefore requires additional checks. func TestRPCTraceEvmDeposit(t *testing.T) { env := newSoloTestEnv(t) wallet, _ := env.solo.NewKeyPairWithFunds() @@ -638,10 +641,14 @@ func TestRPCTraceEvmDeposit(t *testing.T) { ) require.NoError(t, err) - var trace1 []jsonrpc.CallFrame + var trace1 jsonrpc.SendTxArgs err = json.Unmarshal(res1, &trace1) require.NoError(t, err) - require.Len(t, trace1, 0) + + fmt.Print(hexutil.EncodeUint64(isc.NewAssetsBaseTokens(1000).BaseTokens)) + + require.Equal(t, evmAddr.String(), trace1.To.String()) + require.Equal(t, hexutil.EncodeUint64(isc.NewAssetsBaseTokens(1000).BaseTokens*1e12), trace1.Value.String()) } func TestRPCTraceBlock(t *testing.T) { @@ -712,20 +719,24 @@ func TestRPCTraceBlock(t *testing.T) { require.Len(t, traceBlock, 2) - trace1 := traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { + var trace1 jsonrpc.CallFrame + err = json.Unmarshal(traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { return v.TxHash == tx1.Hash() - })].Result + })].Result, &trace1) + require.NoError(t, err) - trace2 := traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { + var trace2 jsonrpc.CallFrame + err = json.Unmarshal(traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { return v.TxHash == tx2.Hash() - })].Result + })].Result, &trace2) + require.NoError(t, err) require.Equal(t, creatorAddress, trace1.From) require.Equal(t, contractAddress, *trace1.To) require.Equal(t, "0x7b", trace1.Value.String()) expectedInput, err := contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(2)) require.NoError(t, err) - require.Equal(t, expectedInput, trace1.Input) + require.Equal(t, expectedInput, []byte(trace1.Input)) require.Empty(t, trace1.Error) require.Empty(t, trace1.RevertReason) require.Equal(t, "0x0", trace1.Gas.String()) @@ -747,7 +758,7 @@ func TestRPCTraceBlock(t *testing.T) { require.Equal(t, "0x141", trace2.Value.String()) expectedInput, err = contractABI.Pack("sendTo", common.Address{0x2}, big.NewInt(3)) require.NoError(t, err) - require.Equal(t, expectedInput, trace2.Input) + require.Equal(t, expectedInput, []byte(trace2.Input)) require.Empty(t, trace2.Error) require.Empty(t, trace2.RevertReason) require.Equal(t, "0x0", trace2.Gas.String()) @@ -765,6 +776,91 @@ func TestRPCTraceBlock(t *testing.T) { require.Contains(t, innerCall2.GasUsed.String(), "0x") } +func TestRPCTraceBlockSingleCall(t *testing.T) { + env := newSoloTestEnv(t) + creator, creatorAddress := env.soloChain.NewEthereumAccountWithL2Funds() + contractABI, err := abi.JSON(strings.NewReader(evmtest.ISCTestContractABI)) + require.NoError(t, err) + _, _, contractAddress := env.DeployEVMContract(creator, contractABI, evmtest.ISCTestContractBytecode) + + // make it so that 2 requests are included in the same block + tx1 := types.MustSignNewTx(creator, types.NewEIP155Signer(big.NewInt(int64(env.ChainID))), + &types.LegacyTx{ + Nonce: env.NonceAt(creatorAddress), + To: &contractAddress, + Value: big.NewInt(123), + Gas: 100000, + GasPrice: big.NewInt(10000000000), + Data: lo.Must(contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(2))), + }) + + req1 := lo.Must(isc.NewEVMOffLedgerTxRequest(env.soloChain.ChainID, tx1)) + env.soloChain.WaitForRequestsMark() + env.soloChain.Env.AddRequestsToMempool(env.soloChain, []isc.Request{req1}) + require.True(t, env.soloChain.WaitForRequestsThrough(1, 180*time.Second)) + + bi := env.soloChain.GetLatestBlockInfo() + require.EqualValues(t, 1, bi.NumSuccessfulRequests) + + var res1 json.RawMessage + // we have to use the raw client, because the normal client does not support debug methods + err = env.RawClient.CallContext( + context.Background(), + &res1, + "debug_traceBlockByNumber", + hexutil.Uint64(env.BlockNumber()).String(), + tracers.TraceConfig{TracerConfig: []byte(`{"tracer": "callTracer"}`)}, + ) + require.NoError(t, err) + + var res2 json.RawMessage + // we have to use the raw client, because the normal client does not support debug methods + err = env.RawClient.CallContext( + context.Background(), + &res2, + "debug_traceBlockByHash", + env.BlockByNumber(big.NewInt(int64(env.BlockNumber()))).Hash(), + tracers.TraceConfig{TracerConfig: []byte(`{"tracer": "callTracer"}`)}, + ) + require.NoError(t, err) + + require.Equal(t, res1, res2, "debug_traceBlockByNumber and debug_traceBlockByHash should produce equal results") + + traceBlock := make([]jsonrpc.TxTraceResult, 0) + err = json.Unmarshal(res1, &traceBlock) + require.NoError(t, err) + + require.Len(t, traceBlock, 1) + + var trace1 jsonrpc.CallFrame + err = json.Unmarshal(traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { + return v.TxHash == tx1.Hash() + })].Result, &trace1) + require.NoError(t, err) + + require.Equal(t, creatorAddress, trace1.From) + require.Equal(t, contractAddress, *trace1.To) + require.Equal(t, "0x7b", trace1.Value.String()) + expectedInput, err := contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(2)) + require.NoError(t, err) + require.Equal(t, expectedInput, []byte(trace1.Input)) + require.Empty(t, trace1.Error) + require.Empty(t, trace1.RevertReason) + require.Equal(t, "0x0", trace1.Gas.String()) + require.Equal(t, "0x0", trace1.GasUsed.String()) + + require.Len(t, trace1.Calls, 1) + innerCall1 := trace1.Calls[0] + require.Equal(t, contractAddress, innerCall1.From) + require.Equal(t, common.Address{0x1}, *innerCall1.To) + require.Equal(t, "0x2", innerCall1.Value.String()) + require.Empty(t, innerCall1.Input) + require.Empty(t, innerCall1.Error) + require.Empty(t, innerCall1.RevertReason) + require.Contains(t, innerCall1.Gas.String(), "0x") + require.Contains(t, innerCall1.GasUsed.String(), "0x") +} + func TestRPCBlockReceipt(t *testing.T) { env := newSoloTestEnv(t) creator, creatorAddress := env.soloChain.NewEthereumAccountWithL2Funds() @@ -802,28 +898,75 @@ func TestRPCBlockReceipt(t *testing.T) { bi := env.soloChain.GetLatestBlockInfo() require.EqualValues(t, 2, bi.NumSuccessfulRequests) - var resceipts []*types.Receipt + var receipts []*types.Receipt err = env.RawClient.CallContext( context.Background(), - &resceipts, + &receipts, "eth_getBlockReceipts", - env.BlockNumber()) + hexutil.EncodeUint64(env.BlockNumber())) require.NoError(t, err) - require.Len(t, resceipts, 2) + require.Len(t, receipts, 2) - r1 := resceipts[slices.IndexFunc(resceipts, func(v *types.Receipt) bool { + r1 := receipts[slices.IndexFunc(receipts, func(v *types.Receipt) bool { return v.TxHash == tx1.Hash() })] - r2 := resceipts[slices.IndexFunc(resceipts, func(v *types.Receipt) bool { + r2 := receipts[slices.IndexFunc(receipts, func(v *types.Receipt) bool { return v.TxHash == tx2.Hash() })] require.Equal(t, uint64(1), r1.Status) require.Equal(t, big.NewInt(4), r1.BlockNumber) require.Equal(t, uint64(1), r2.Status) + require.Equal(t, big.NewInt(4), r2.BlockNumber) + + // Test the same block with its hash. + block := env.BlockByNumber(new(big.Int).SetUint64(env.BlockNumber())) + err = env.RawClient.CallContext( + context.Background(), + &receipts, + "eth_getBlockReceipts", + block.Hash().String()) + require.NoError(t, err) + + require.Len(t, receipts, 2) + + r1 = receipts[slices.IndexFunc(receipts, func(v *types.Receipt) bool { + return v.TxHash == tx1.Hash() + })] + + r2 = receipts[slices.IndexFunc(receipts, func(v *types.Receipt) bool { + return v.TxHash == tx2.Hash() + })] + + require.Equal(t, uint64(1), r1.Status) require.Equal(t, big.NewInt(4), r1.BlockNumber) + require.Equal(t, uint64(1), r2.Status) + require.Equal(t, big.NewInt(4), r2.BlockNumber) + + // Test "latest" block + err = env.RawClient.CallContext( + context.Background(), + &receipts, + "eth_getBlockReceipts", + "latest") + require.NoError(t, err) + + require.Len(t, receipts, 2) + + r1 = receipts[slices.IndexFunc(receipts, func(v *types.Receipt) bool { + return v.TxHash == tx1.Hash() + })] + + r2 = receipts[slices.IndexFunc(receipts, func(v *types.Receipt) bool { + return v.TxHash == tx2.Hash() + })] + + require.Equal(t, uint64(1), r1.Status) + require.Equal(t, big.NewInt(4), r1.BlockNumber) + require.Equal(t, uint64(1), r2.Status) + require.Equal(t, big.NewInt(4), r2.BlockNumber) } func BenchmarkRPCEstimateGas(b *testing.B) { diff --git a/packages/evm/jsonrpc/service.go b/packages/evm/jsonrpc/service.go index 1174e2d04c..ade2fc8810 100644 --- a/packages/evm/jsonrpc/service.go +++ b/packages/evm/jsonrpc/service.go @@ -451,14 +451,35 @@ func (e *EthService) Logs(ctx context.Context, q *RPCFilterQuery) (*rpc.Subscrip return rpcSub, nil } -func (e *EthService) GetBlockReceipts(blockNumber int64) ([]*types.Receipt, error) { - return withMetrics(e.metrics, "eth_getBlockReceipts", func() ([]*types.Receipt, error) { - receipts, err := e.evmChain.GetBlockReceipts(rpc.BlockNumber(blockNumber)) +func (e *EthService) GetBlockReceipts(blockNumber rpc.BlockNumberOrHash) ([]map[string]interface{}, error) { + return withMetrics(e.metrics, "eth_getBlockReceipts", func() ([]map[string]interface{}, error) { + receipts, txs, err := e.evmChain.GetBlockReceipts(blockNumber) if err != nil { - return nil, e.resolveError(err) + return []map[string]interface{}{}, e.resolveError(err) + } + + if len(receipts) != len(txs) { + return nil, fmt.Errorf("receipts length mismatch: %d vs %d", len(receipts), len(txs)) } - return receipts, nil + result := make([]map[string]interface{}, len(receipts)) + for i, receipt := range receipts { + // This is pretty ugly, maybe we should shift to uint64 for internals too. + feePolicy, err := e.evmChain.backend.FeePolicy(uint32(receipt.BlockNumber.Uint64())) + if err != nil { + return nil, err + } + + effectiveGasPrice := txs[i].GasPrice() + if effectiveGasPrice.Sign() == 0 && !feePolicy.GasPerToken.IsEmpty() { + // tx sent before gasPrice was mandatory + effectiveGasPrice = feePolicy.DefaultGasPriceFullDecimals(parameters.L1().BaseToken.Decimals) + } + + result[i] = RPCMarshalReceipt(receipt, txs[i], effectiveGasPrice) + } + + return result, nil }) } @@ -566,6 +587,12 @@ func (d *DebugService) TraceBlockByHash(blockHash common.Hash, config *tracers.T }) } +func (d *DebugService) GetRawBlock(blockNrOrHash rpc.BlockNumberOrHash) (interface{}, error) { + return withMetrics(d.metrics, "debug_traceBlockByHash", func() (interface{}, error) { + return d.evmChain.GetRawBlock(blockNrOrHash) + }) +} + type EVMService struct { evmChain *EVMChain } diff --git a/packages/evm/jsonrpc/tracer.go b/packages/evm/jsonrpc/tracer.go index f284911d4a..8f0a419c15 100644 --- a/packages/evm/jsonrpc/tracer.go +++ b/packages/evm/jsonrpc/tracer.go @@ -7,7 +7,7 @@ import ( "github.com/ethereum/go-ethereum/eth/tracers" ) -type tracerFactory func(*tracers.Context, json.RawMessage, any) (*tracers.Tracer, error) +type tracerFactory func(*tracers.Context, json.RawMessage) (*tracers.Tracer, error) var allTracers = map[string]tracerFactory{} @@ -15,10 +15,10 @@ func registerTracer(tracerType string, fn tracerFactory) { allTracers[tracerType] = fn } -func newTracer(tracerType string, ctx *tracers.Context, cfg json.RawMessage, initValue any) (*tracers.Tracer, error) { +func newTracer(tracerType string, ctx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) { fn := allTracers[tracerType] if fn == nil { return nil, fmt.Errorf("unsupported tracer type: %s", tracerType) } - return fn(ctx, cfg, initValue) + return fn(ctx, cfg) } diff --git a/packages/evm/jsonrpc/tracer_call.go b/packages/evm/jsonrpc/tracer_call.go index 3330435434..5e45c86b63 100644 --- a/packages/evm/jsonrpc/tracer_call.go +++ b/packages/evm/jsonrpc/tracer_call.go @@ -3,7 +3,6 @@ package jsonrpc import ( "encoding/json" "errors" - "fmt" "math/big" "strings" "sync/atomic" @@ -42,7 +41,7 @@ func NewOpCodeJSON(code vm.OpCode) OpCodeJSON { } func (o OpCodeJSON) MarshalJSON() ([]byte, error) { - return json.Marshal(strings.ToLower(o.String())) + return json.Marshal(strings.ToUpper(o.String())) } func (o *OpCodeJSON) UnmarshalJSON(data []byte) error { @@ -60,8 +59,8 @@ type CallFrame struct { Gas hexutil.Uint64 `json:"gas"` GasUsed hexutil.Uint64 `json:"gasUsed"` To *common.Address `json:"to,omitempty" rlp:"optional"` - Input []byte `json:"input" rlp:"optional"` - Output []byte `json:"output,omitempty" rlp:"optional"` + Input hexutil.Bytes `json:"input" rlp:"optional"` + Output hexutil.Bytes `json:"output,omitempty" rlp:"optional"` Error string `json:"error,omitempty" rlp:"optional"` RevertReason string `json:"revertReason,omitempty"` Calls []CallFrame `json:"calls,omitempty" rlp:"optional"` @@ -109,9 +108,9 @@ func (f *CallFrame) processOutput(output []byte, err error, reverted bool) { } type TxTraceResult struct { - TxHash common.Hash `json:"txHash"` // transaction hash - Result CallFrame `json:"result,omitempty"` // Trace results produced by the tracer - Error string `json:"error,omitempty"` // Trace failure produced by the tracer + TxHash common.Hash `json:"txHash"` // transaction hash + Result json.RawMessage `json:"result,omitempty"` // Trace results produced by the tracer + Error string `json:"error,omitempty"` // Trace failure produced by the tracer } type callTracer struct { @@ -119,9 +118,8 @@ type callTracer struct { config callTracerConfig gasLimit uint64 depth int - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption - blockTxs types.Transactions // for block tracing we need this to get ordered tx hashes + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption } type callTracerConfig struct { @@ -131,17 +129,8 @@ type callTracerConfig struct { // newCallTracer returns a native go tracer which tracks // call frames of a tx, and implements vm.EVMLogger. -func newCallTracer(ctx *tracers.Context, cfg json.RawMessage, initValue any) (*tracers.Tracer, error) { - var txs types.Transactions - if initValue != nil { - var ok bool - txs, ok = initValue.(types.Transactions) - if !ok { - return nil, fmt.Errorf("invalid init value type for tracer: %T", initValue) - } - } - - t, err := newCallTracerObject(ctx, cfg, txs) +func newCallTracer(ctx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) { + t, err := newCallTracerObject(ctx, cfg) if err != nil { return nil, err } @@ -158,7 +147,7 @@ func newCallTracer(ctx *tracers.Context, cfg json.RawMessage, initValue any) (*t }, nil } -func newCallTracerObject(_ *tracers.Context, cfg json.RawMessage, blockTxs types.Transactions) (*callTracer, error) { +func newCallTracerObject(_ *tracers.Context, cfg json.RawMessage) (*callTracer, error) { var config callTracerConfig if cfg != nil { if err := json.Unmarshal(cfg, &config); err != nil { @@ -167,7 +156,7 @@ func newCallTracerObject(_ *tracers.Context, cfg json.RawMessage, blockTxs types } // First callframe contains tx context info // and is populated on start and end. - return &callTracer{callstack: make([]CallFrame, 0, 1), config: config, blockTxs: blockTxs}, nil + return &callTracer{callstack: make([]CallFrame, 0, 1), config: config}, nil } // OnEnter is called when EVM enters a new scope (via call, create or selfdestruct). @@ -269,32 +258,21 @@ func (t *callTracer) OnLog(log *types.Log) { t.callstack[len(t.callstack)-1].Logs = append(t.callstack[len(t.callstack)-1].Logs, l) } +var ErrIncorrectTopLevelCalls = errors.New("incorrect number of top-level calls") + // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). func (t *callTracer) GetResult() (json.RawMessage, error) { - if len(t.callstack) == 1 { - res, err := json.Marshal(t.callstack[0]) - if err != nil { - return nil, err - } - return res, t.reason - } - - // otherwise return all call frames - results := make([]TxTraceResult, 0, len(t.callstack)) - for i, cs := range t.callstack { - results = append(results, TxTraceResult{ - TxHash: t.blockTxs[i].Hash(), - Result: cs, - }) + if len(t.callstack) != 1 { + return nil, ErrIncorrectTopLevelCalls } - resJSON, err := json.Marshal(results) + res, err := json.Marshal(t.callstack[0]) if err != nil { return nil, err } - return resJSON, t.reason + return res, t.reason } // Stop terminates execution of the tracer at the first opportune moment. diff --git a/packages/evm/jsonrpc/types.go b/packages/evm/jsonrpc/types.go index 5434daf2bf..c8cba20f48 100644 --- a/packages/evm/jsonrpc/types.go +++ b/packages/evm/jsonrpc/types.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "math/big" + "slices" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" @@ -158,6 +159,20 @@ func parseBlockNumber(bn rpc.BlockNumber) *big.Int { return big.NewInt(n) } +const FakeTxOpcode = "CALL" + +func RPCMarshalTransactionTraceForFakeTX(tx *types.Transaction, effectiveGasPrice *big.Int) map[string]interface{} { + return map[string]interface{}{ + "from": evmutil.MustGetSenderIfTxSigned(tx), + "gas": hexutil.Uint64(tx.Gas()), + "gasUsed": hexutil.Uint64(tx.Gas()), + "to": tx.To(), + "type": FakeTxOpcode, + "input": "0x", + "value": hexutil.Big(*tx.Value()), + } +} + func RPCMarshalReceipt(r *types.Receipt, tx *types.Transaction, effectiveGasPrice *big.Int) map[string]interface{} { // fix for an already fixed bug where some old failed receipts contain non-empty logs if r.Status != types.ReceiptStatusSuccessful { @@ -165,7 +180,7 @@ func RPCMarshalReceipt(r *types.Receipt, tx *types.Transaction, effectiveGasPric r.Bloom = types.CreateBloom(types.Receipts{r}) } - return map[string]interface{}{ + result := map[string]interface{}{ "transactionHash": r.TxHash, "transactionIndex": hexutil.Uint64(r.TransactionIndex), "blockHash": r.BlockHash, @@ -175,12 +190,20 @@ func RPCMarshalReceipt(r *types.Receipt, tx *types.Transaction, effectiveGasPric "cumulativeGasUsed": hexutil.Uint64(r.CumulativeGasUsed), "gasUsed": hexutil.Uint64(r.GasUsed), "effectiveGasPrice": hexutil.EncodeBig(effectiveGasPrice), - "contractAddress": r.ContractAddress, "logs": rpcMarshalLogs(r), "logsBloom": r.Bloom, "status": hexutil.Uint64(r.Status), "type": hexutil.Uint64(types.LegacyTxType), } + + // Eth compatibility. Return "null" instead of "0x00000000000000000000000..." + if slices.Equal(r.ContractAddress.Bytes(), common.Address{}.Bytes()) { + result["contractAddress"] = nil + } else { + result["contractAddress"] = r.ContractAddress + } + + return result } func rpcMarshalLogs(r *types.Receipt) []interface{} { @@ -195,6 +218,7 @@ func rpcMarshalLogs(r *types.Receipt) []interface{} { "address": log.Address, "data": hexutil.Bytes(log.Data), "topics": log.Topics, + "removed": log.Removed, } } return ret