diff --git a/core/state_processor.go b/core/state_processor.go index 2a8d027686..84d3e1ce13 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -248,6 +248,17 @@ func (p *StateProcessor) ProcessBlockNoValidator(cBlock *CalculatedBlock, stated // and uses the input parameters for its environment similar to ApplyTransaction. However, // this method takes an already created EVM instance as input. func ApplyTransactionWithEVM(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, balanceFee *big.Int, coinbaseOwner common.Address) (receipt *types.Receipt, gasUsed uint64, tokenFeeUsed bool, err error) { + // Initialize tracer at the beginning to ensure all transaction types + // (including non-EVM special transactions) are properly traced. + if evm.Config.Tracer != nil && evm.Config.Tracer.OnTxStart != nil { + evm.Config.Tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) + if evm.Config.Tracer.OnTxEnd != nil { + defer func() { + evm.Config.Tracer.OnTxEnd(receipt, err) + }() + } + } + to := tx.To() if to != nil { if *to == common.BlockSignersBinary && config.IsTIPSigning(blockNumber) { @@ -267,14 +278,6 @@ func ApplyTransactionWithEVM(msg *Message, config *params.ChainConfig, gp *GasPo return ApplyEmptyTransaction(config, statedb, blockNumber, blockHash, tx, usedGas) } - if evm.Config.Tracer != nil && evm.Config.Tracer.OnTxStart != nil { - evm.Config.Tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - if evm.Config.Tracer.OnTxEnd != nil { - defer func() { - evm.Config.Tracer.OnTxEnd(receipt, err) - }() - } - } // Create a new context to be used in the EVM environment txContext := NewEVMTxContext(msg) diff --git a/core/state_processor_test.go b/core/state_processor_test.go index d6eb7d6f3f..c2d31c8512 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -24,6 +24,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/consensus" "github.com/XinFinOrg/XDPoSChain/consensus/ethash" "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/core/tracing" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/core/vm" "github.com/XinFinOrg/XDPoSChain/crypto" @@ -289,3 +290,141 @@ func GenerateBadBlock(t *testing.T, parent *types.Block, engine consensus.Engine // Assemble and return the final block for sealing return types.NewBlock(header, &types.Body{Transactions: txs}, receipts, trie.NewStackTrie(nil)) } + +// TestApplyTransactionWithEVMTracer tests that tracer's OnTxStart and OnTxEnd +// are called for all transaction types, including non-EVM special transactions. +func TestApplyTransactionWithEVMTracer(t *testing.T) { + var ( + config = ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + Eip1559Block: big.NewInt(0), + Ethash: new(params.EthashConfig), + } + signer = types.LatestSigner(config) + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + ) + + tests := []struct { + name string + to *common.Address + expectOnTx bool // expect OnTxStart/OnTxEnd to be called + }{ + { + name: "BlockSignersBinary transaction", + to: &common.BlockSignersBinary, + expectOnTx: true, + }, + { + name: "XDCXAddrBinary transaction", + to: &common.XDCXAddrBinary, + expectOnTx: true, + }, + { + name: "Regular transaction", + to: func() *common.Address { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + return &addr + }(), + expectOnTx: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a test database and genesis + db := rawdb.NewMemoryDatabase() + gspec := &Genesis{ + Config: config, + Alloc: types.GenesisAlloc{ + testAddr: types.Account{ + Balance: big.NewInt(1000000000000000000), // 1 ether + Nonce: 0, + }, + }, + } + genesis := gspec.MustCommit(db) + blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + defer blockchain.Stop() + + // Create state database + statedb, err := blockchain.State() + if err != nil { + t.Fatalf("Failed to get state: %v", err) + } + + // Create a transaction with sufficient gas price to avoid base fee errors + tx := types.NewTransaction(0, *tt.to, big.NewInt(0), 100000, big.NewInt(20000000000), nil) + signedTx, err := types.SignTx(tx, signer, testKey) + if err != nil { + t.Fatalf("Failed to sign transaction: %v", err) + } + + // Create a mock tracer + onTxStartCalled := false + onTxEndCalled := false + mockTracer := &tracing.Hooks{ + OnTxStart: func(vmContext *tracing.VMContext, tx *types.Transaction, from common.Address) { + onTxStartCalled = true + if tx == nil { + t.Error("OnTxStart called with nil transaction") + } + if from != testAddr { + t.Errorf("OnTxStart called with wrong from address: got %v, want %v", from, testAddr) + } + }, + OnTxEnd: func(receipt *types.Receipt, err error) { + onTxEndCalled = true + }, + } + + // Create EVM with tracer + vmConfig := vm.Config{ + Tracer: mockTracer, + } + + msg, err := TransactionToMessage(signedTx, signer, nil, nil, nil) + if err != nil { + t.Fatalf("Failed to create message: %v", err) + } + + gasPool := new(GasPool).AddGas(1000000) + blockNumber := big.NewInt(1) + blockHash := genesis.Hash() + + vmContext := NewEVMBlockContext(blockchain.CurrentBlock().Header(), blockchain, nil) + evm := vm.NewEVM(vmContext, vm.TxContext{}, statedb, nil, blockchain.Config(), vmConfig) + + // Apply transaction + var usedGas uint64 + _, _, _, err = ApplyTransactionWithEVM(msg, config, gasPool, statedb, blockNumber, blockHash, signedTx, &usedGas, evm, big.NewInt(0), common.Address{}) + // NOTE: Some special transactions (like BlockSignersBinary or XDCXAddrBinary) + // may fail in test environment due to missing configuration or state, but + // the tracer should still be called at the beginning of ApplyTransactionWithEVM. + // We don't fail the test on transaction execution error as long as tracer was invoked. + if err != nil { + t.Logf("Transaction execution returned error (expected for some special txs): %v", err) + } + + // Verify tracer was called + if tt.expectOnTx { + if !onTxStartCalled { + t.Error("OnTxStart was not called") + } + if !onTxEndCalled { + t.Error("OnTxEnd was not called") + } + } + }) + } +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 5b76636e3d..671916962d 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -490,6 +490,39 @@ func (tx *Transaction) IsSkipNonceTransaction() bool { } } +// IsNonEVMTx returns true if the transaction is a "special transaction" that +// does not execute EVM code, but is instead handled by native code. +// Returns false if `tx` is nil or if `tx.To()` is nil. +// +// "Special transactions" are those sent to specific system addresses, which are: +// - common.BlockSignersBinary +// - common.XDCXAddrBinary +// - common.TradingStateAddrBinary +// - common.XDCXLendingAddressBinary +// - common.XDCXLendingFinalizedTradeAddressBinary +// +// These addresses are defined in the `common` package. +func (tx *Transaction) IsNonEVMTx() bool { + if tx == nil { + return false + } + to := tx.To() + if to == nil { + return false + } + + switch *to { + case common.BlockSignersBinary, + common.XDCXAddrBinary, + common.TradingStateAddrBinary, + common.XDCXLendingAddressBinary, + common.XDCXLendingFinalizedTradeAddressBinary: + return true + default: + return false + } +} + func (tx *Transaction) IsSigningTransaction() bool { to := tx.To() if to == nil || *to != common.BlockSignersBinary { diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go index 3ff09a63f1..e873385a1d 100644 --- a/core/types/transaction_test.go +++ b/core/types/transaction_test.go @@ -609,3 +609,112 @@ func TestTransactionSizes(t *testing.T) { } } } + +// TestIsNonEVMTx tests the IsNonEVMTx method to ensure it correctly identifies +// transactions that are handled by native code rather than EVM execution. +func TestIsNonEVMTx(t *testing.T) { + tests := []struct { + name string + tx *Transaction + expected bool + }{ + { + name: "nil transaction", + tx: nil, + expected: false, + }, + { + name: "contract creation (nil to)", + expected: false, + }, + { + name: "regular transaction", + tx: NewTransaction( + 0, + common.HexToAddress("0x1234567890123456789012345678901234567890"), + big.NewInt(0), + 0, + big.NewInt(0), + nil, + ), + expected: false, + }, + { + name: "BlockSignersBinary transaction", + tx: NewTransaction( + 0, + common.BlockSignersBinary, + big.NewInt(0), + 0, + big.NewInt(0), + nil, + ), + expected: true, + }, + { + name: "XDCXAddrBinary transaction", + tx: NewTransaction( + 0, + common.XDCXAddrBinary, + big.NewInt(0), + 0, + big.NewInt(0), + nil, + ), + expected: true, + }, + { + name: "TradingStateAddrBinary transaction", + tx: NewTransaction( + 0, + common.TradingStateAddrBinary, + big.NewInt(0), + 0, + big.NewInt(0), + nil, + ), + expected: true, + }, + { + name: "XDCXLendingAddressBinary transaction", + tx: NewTransaction( + 0, + common.XDCXLendingAddressBinary, + big.NewInt(0), + 0, + big.NewInt(0), + nil, + ), + expected: true, + }, + { + name: "XDCXLendingFinalizedTradeAddressBinary transaction", + tx: NewTransaction( + 0, + common.XDCXLendingFinalizedTradeAddressBinary, + big.NewInt(0), + 0, + big.NewInt(0), + nil, + ), + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Handle the contract creation case separately + var tx *Transaction + if tt.name == "contract creation (nil to)" { + tx = NewContractCreation(0, big.NewInt(0), 0, big.NewInt(0), nil) + } else { + tx = tt.tx + } + + result := tx.IsNonEVMTx() + if result != tt.expected { + t.Errorf("IsNonEVMTx() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go index 6fb0b7a6e4..eeffd586e0 100644 --- a/eth/tracers/native/call.go +++ b/eth/tracers/native/call.go @@ -117,6 +117,16 @@ type callTracer struct { depth int interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption + + // `isNonEVMTx` marks transactions that do not execute EVM opcodes and + // therefore require the tracer to return a synthetic top-level call + // frame instead of relying on the EVM callstack. + isNonEVMTx bool + + // `nonEVMCall` holds the prepared virtual top-level callFrame for + // non-EVM special transactions. It is marshalled directly by + // `GetResult()` to preserve debug API compatibility. + nonEVMCall callFrame } type callTracerConfig struct { @@ -208,6 +218,10 @@ func (t *callTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, t.callstack[size-1].Calls = append(t.callstack[size-1].Calls, call) } +// captureEnd is called after a transaction execution completes and processes +// the final output for the top-level call frame. This method is only applicable +// to normal EVM transactions where the callstack has exactly one frame. For +// non-EVM special transactions, the callstack is empty and this becomes a no-op. func (t *callTracer) captureEnd(output []byte, gasUsed uint64, err error, reverted bool) { if len(t.callstack) != 1 { return @@ -216,7 +230,38 @@ func (t *callTracer) captureEnd(output []byte, gasUsed uint64, err error, revert } func (t *callTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { + // Reset any previous non-EVM call state to ensure tracer is clean + // for the new transaction, preventing state leakage between txs. + t.isNonEVMTx = false + t.nonEVMCall = callFrame{} + t.callstack = make([]callFrame, 0, 1) + + if tx == nil { + return + } t.gasLimit = tx.Gas() + + // Detect non-EVM special transactions so GetResult can return a virtual + // top-level call when the EVM did not execute any opcodes for this tx. + if tx.IsNonEVMTx() { + t.isNonEVMTx = true + t.nonEVMCall.Type = vm.CALL // synthetic for compatibility + t.nonEVMCall.From = from + if to := tx.To(); to != nil { + addr := *to // intentional copy + t.nonEVMCall.To = &addr + } + t.nonEVMCall.Gas = tx.Gas() + t.nonEVMCall.Value = tx.Value() + t.nonEVMCall.GasUsed = 0 + t.nonEVMCall.Input = common.CopyBytes(tx.Data()) + // Non-EVM transactions don't produce output or errors + t.nonEVMCall.Output = nil + t.nonEVMCall.Error = "" + // Initialize to nil to avoid heap allocation for empty slices + t.nonEVMCall.Calls = nil + t.nonEVMCall.Logs = nil + } } func (t *callTracer) OnTxEnd(receipt *types.Receipt, err error) { @@ -224,6 +269,21 @@ func (t *callTracer) OnTxEnd(receipt *types.Receipt, err error) { if err != nil { return } + + // Handle non-EVM special tx: update the synthetic call frame. + if t.isNonEVMTx { + if receipt != nil { + t.nonEVMCall.GasUsed = receipt.GasUsed + } + return + } + + // Handle normal EVM tx: update the top-level call frame. + if len(t.callstack) == 0 { + // Guard against empty callstack to avoid panic. + return + } + if receipt != nil { t.callstack[0].GasUsed = receipt.GasUsed } @@ -246,18 +306,48 @@ func (t *callTracer) OnLog(log *types.Log) { if t.interrupt.Load() { return } + // If this is a non-EVM transaction, append to nonEVMCall.Logs + if t.isNonEVMTx { + l := callLog{ + Address: log.Address, + Topics: log.Topics, + Data: log.Data, + Position: hexutil.Uint(len(t.nonEVMCall.Calls)), + } + t.nonEVMCall.Logs = append(t.nonEVMCall.Logs, l) + return + } + // Skip if callstack is empty + if len(t.callstack) == 0 { + return + } + // Cache the index of the current frame to avoid repeated calculations + lastIdx := len(t.callstack) - 1 l := callLog{ Address: log.Address, Topics: log.Topics, Data: log.Data, - Position: hexutil.Uint(len(t.callstack[len(t.callstack)-1].Calls)), + Position: hexutil.Uint(len(t.callstack[lastIdx].Calls)), } - t.callstack[len(t.callstack)-1].Logs = append(t.callstack[len(t.callstack)-1].Logs, l) + t.callstack[lastIdx].Logs = append(t.callstack[lastIdx].Logs, l) } // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). +// For non-EVM transactions, a synthetic virtual frame is returned to maintain +// debug API compatibility even though the EVM did not execute opcodes. func (t *callTracer) GetResult() (json.RawMessage, error) { + // For non-EVM special transactions (e.g. BlockSignersBinary) the EVM + // did not execute opcodes, so return the prepared virtual top-level + // callFrame directly without mutating the tracer state. + if t.isNonEVMTx { + res, err := json.Marshal(t.nonEVMCall) + if err != nil { + return nil, err + } + return res, t.reason + } + if len(t.callstack) != 1 { return nil, errors.New("incorrect number of top-level calls") } diff --git a/eth/tracers/native/call_test.go b/eth/tracers/native/call_test.go new file mode 100644 index 0000000000..3d471112b9 --- /dev/null +++ b/eth/tracers/native/call_test.go @@ -0,0 +1,526 @@ +// Copyright 2024 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 native_test + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/common/hexutil" + "github.com/XinFinOrg/XDPoSChain/core/tracing" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/core/vm" + "github.com/XinFinOrg/XDPoSChain/eth/tracers" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/stretchr/testify/require" +) + +// TestCallTracerNonEVMTx tests the call tracer with non-EVM special transactions +// to ensure it returns a synthetic top-level callFrame. +func TestCallTracerNonEVMTx(t *testing.T) { + tests := []struct { + name string + to common.Address + isNonEVM bool + }{ + { + name: "BlockSignersBinary transaction", + to: common.BlockSignersBinary, + isNonEVM: true, + }, + { + name: "XDCXAddrBinary transaction", + to: common.XDCXAddrBinary, + isNonEVM: true, + }, + { + name: "TradingStateAddrBinary transaction", + to: common.TradingStateAddrBinary, + isNonEVM: true, + }, + { + name: "XDCXLendingAddressBinary transaction", + to: common.XDCXLendingAddressBinary, + isNonEVM: true, + }, + { + name: "XDCXLendingFinalizedTradeAddressBinary transaction", + to: common.XDCXLendingFinalizedTradeAddressBinary, + isNonEVM: true, + }, + { + name: "Regular transaction", + to: common.HexToAddress("0x1234567890123456789012345678901234567890"), + isNonEVM: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tracer, err := tracers.DefaultDirectory.New("callTracer", &tracers.Context{}, nil, params.MainnetChainConfig) + require.NoError(t, err) + + from := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + gasLimit := uint64(100000) + value := big.NewInt(1000) + data := []byte{0x01, 0x02, 0x03} + + tx := types.NewTx(&types.LegacyTx{ + Nonce: 0, + To: &tt.to, + Value: value, + Gas: gasLimit, + GasPrice: big.NewInt(1), + Data: data, + }) + + vmContext := &tracing.VMContext{ + BlockNumber: big.NewInt(1), + } + + // Start transaction tracing + tracer.OnTxStart(vmContext, tx, from) + + if tt.isNonEVM { + // For non-EVM transactions, we don't call OnEnter/OnExit + // because the EVM doesn't execute. We only call OnTxEnd. + receipt := &types.Receipt{ + GasUsed: 21000, + } + tracer.OnTxEnd(receipt, nil) + + // Get the result + result, err := tracer.GetResult() + require.NoError(t, err) + + // Parse the result + var callFrame map[string]interface{} + err = json.Unmarshal(result, &callFrame) + require.NoError(t, err) + + // Verify the synthetic callFrame + require.Equal(t, "CALL", callFrame["type"]) + // Just verify addresses are present and not empty + fromStr, ok := callFrame["from"].(string) + require.True(t, ok, "from field should be a string") + require.NotEmpty(t, fromStr, "from address should not be empty") + + toStr, ok := callFrame["to"].(string) + require.True(t, ok, "to field should be a string") + require.NotEmpty(t, toStr, "to address should not be empty") + + require.Equal(t, hexutil.Uint64(gasLimit).String(), callFrame["gas"]) + require.Equal(t, (*hexutil.Big)(value).String(), callFrame["value"]) + require.Equal(t, hexutil.Uint64(21000).String(), callFrame["gasUsed"]) + require.Equal(t, hexutil.Bytes(data).String(), callFrame["input"]) + require.Nil(t, callFrame["output"]) + // error field should be nil (no error) for non-EVM transactions + if callFrame["error"] != nil { + require.Equal(t, "", callFrame["error"]) + } + } else { + // For regular transactions, simulate normal EVM execution + tracer.OnEnter(0, byte(vm.CALL), from, tt.to, data, gasLimit, value) + tracer.OnExit(0, []byte{0x04, 0x05}, 50000, nil, false) + + receipt := &types.Receipt{ + GasUsed: 50000, + } + tracer.OnTxEnd(receipt, nil) + + // Get the result + result, err := tracer.GetResult() + require.NoError(t, err) + + // Verify we got a proper result + var callFrame map[string]interface{} + err = json.Unmarshal(result, &callFrame) + require.NoError(t, err) + require.NotNil(t, callFrame) + } + }) + } +} + +// TestCallTracerEmptyCallstack tests that OnLog and OnTxEnd don't panic +// when the callstack is empty (guards against issue #1863). +func TestCallTracerEmptyCallstack(t *testing.T) { + tracer, err := tracers.DefaultDirectory.New("callTracer", &tracers.Context{}, nil, params.MainnetChainConfig) + require.NoError(t, err) + + from := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + to := common.BlockSignersBinary + + tx := types.NewTx(&types.LegacyTx{ + Nonce: 0, + To: &to, + Value: big.NewInt(0), + Gas: 100000, + GasPrice: big.NewInt(1), + Data: nil, + }) + + vmContext := &tracing.VMContext{ + BlockNumber: big.NewInt(1), + } + + // Start non-EVM transaction + tracer.OnTxStart(vmContext, tx, from) + + // Try to call OnLog with empty callstack - should not panic + log := &types.Log{ + Address: to, + Topics: []common.Hash{common.HexToHash("0x1234")}, + Data: []byte{0x01, 0x02}, + } + require.NotPanics(t, func() { + tracer.OnLog(log) + }) + + // Try to call OnTxEnd with empty callstack - should not panic + receipt := &types.Receipt{ + GasUsed: 21000, + } + require.NotPanics(t, func() { + tracer.OnTxEnd(receipt, nil) + }) + + // Verify we can still get a result + result, err := tracer.GetResult() + require.NoError(t, err) + require.NotNil(t, result) +} + +// TestCallTracerStateReset tests that tracer state is properly reset +// between transactions to prevent state leakage. +func TestCallTracerStateReset(t *testing.T) { + tracer, err := tracers.DefaultDirectory.New("callTracer", &tracers.Context{}, nil, params.MainnetChainConfig) + require.NoError(t, err) + + from := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + nonEVMTo := common.BlockSignersBinary + regularTo := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // First transaction: non-EVM + tx1 := types.NewTx(&types.LegacyTx{ + Nonce: 0, + To: &nonEVMTo, + Value: big.NewInt(0), + Gas: 100000, + GasPrice: big.NewInt(1), + Data: nil, + }) + + vmContext := &tracing.VMContext{ + BlockNumber: big.NewInt(1), + } + + tracer.OnTxStart(vmContext, tx1, from) + receipt1 := &types.Receipt{ + GasUsed: 21000, + } + tracer.OnTxEnd(receipt1, nil) + + result1, err := tracer.GetResult() + require.NoError(t, err) + require.NotNil(t, result1) + + // Second transaction: regular EVM transaction + tx2 := types.NewTx(&types.LegacyTx{ + Nonce: 1, + To: ®ularTo, + Value: big.NewInt(1000), + Gas: 100000, + GasPrice: big.NewInt(1), + Data: []byte{0x01}, + }) + + tracer.OnTxStart(vmContext, tx2, from) + tracer.OnEnter(0, byte(vm.CALL), from, regularTo, []byte{0x01}, 100000, big.NewInt(1000)) + tracer.OnExit(0, []byte{0x02}, 50000, nil, false) + receipt2 := &types.Receipt{ + GasUsed: 50000, + } + tracer.OnTxEnd(receipt2, nil) + + result2, err := tracer.GetResult() + require.NoError(t, err) + require.NotNil(t, result2) + + // Verify that the results are different + require.NotEqual(t, string(result1), string(result2)) +} + +// TestCallTracerNilTransaction tests that OnTxStart handles nil transaction gracefully. +func TestCallTracerNilTransaction(t *testing.T) { + tracer, err := tracers.DefaultDirectory.New("callTracer", &tracers.Context{}, nil, params.MainnetChainConfig) + require.NoError(t, err) + + vmContext := &tracing.VMContext{ + BlockNumber: big.NewInt(1), + } + from := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + + // Should not panic with nil transaction + require.NotPanics(t, func() { + tracer.OnTxStart(vmContext, nil, from) + }) +} + +// TestCallTracerNonEVMTxWithLog tests that the call tracer correctly captures logs +// for non-EVM transactions when WithLog configuration is enabled. +// This test verifies: +// 1. Logs are captured when WithLog=true +// 2. Logs are not captured when WithLog=false +// 3. Log position tracking is correct for non-EVM transactions +// 4. No log duplication occurs between transactions +func TestCallTracerNonEVMTxWithLog(t *testing.T) { + tests := []struct { + name string + withLog bool + to common.Address + logCount int + expectLogsInResult bool + }{ + { + name: "BlockSignersBinary with WithLog=true", + withLog: true, + to: common.BlockSignersBinary, + logCount: 3, + expectLogsInResult: true, + }, + { + name: "BlockSignersBinary with WithLog=false", + withLog: false, + to: common.BlockSignersBinary, + logCount: 3, + expectLogsInResult: false, + }, + { + name: "XDCXAddrBinary with WithLog=true", + withLog: true, + to: common.XDCXAddrBinary, + logCount: 2, + expectLogsInResult: true, + }, + { + name: "TradingStateAddrBinary with WithLog=true", + withLog: true, + to: common.TradingStateAddrBinary, + logCount: 1, + expectLogsInResult: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create tracer with WithLog configuration + config := json.RawMessage(`{"withLog":` + func() string { + if tt.withLog { + return "true" + } + return "false" + }() + `}`) + + tracer, err := tracers.DefaultDirectory.New("callTracer", &tracers.Context{}, config, params.MainnetChainConfig) + require.NoError(t, err) + + from := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + gasLimit := uint64(100000) + value := big.NewInt(0) + data := []byte{0x01, 0x02, 0x03} + + tx := types.NewTx(&types.LegacyTx{ + Nonce: 0, + To: &tt.to, + Value: value, + Gas: gasLimit, + GasPrice: big.NewInt(1), + Data: data, + }) + + vmContext := &tracing.VMContext{ + BlockNumber: big.NewInt(1), + } + + // Start transaction tracing + tracer.OnTxStart(vmContext, tx, from) + + // Simulate log events for non-EVM transaction + for i := 0; i < tt.logCount; i++ { + log := &types.Log{ + Address: tt.to, + Topics: []common.Hash{ + common.HexToHash("0x1234"), + common.BigToHash(big.NewInt(int64(i))), + }, + Data: []byte{byte(i), 0xff}, + } + tracer.OnLog(log) + } + + receipt := &types.Receipt{ + GasUsed: 21000, + } + tracer.OnTxEnd(receipt, nil) + + // Get the result + result, err := tracer.GetResult() + require.NoError(t, err) + + // Parse the result + var callFrame map[string]interface{} + err = json.Unmarshal(result, &callFrame) + require.NoError(t, err) + + // Verify logs are included or excluded based on WithLog config + logs, logsExist := callFrame["logs"] + if tt.expectLogsInResult { + require.True(t, logsExist, "logs field should exist when WithLog=true") + logArray, ok := logs.([]interface{}) + require.True(t, ok, "logs should be an array") + require.Len(t, logArray, tt.logCount, "should have correct number of logs") + + // Verify log structure and position tracking + for _, logItem := range logArray { + logMap, ok := logItem.(map[string]interface{}) + require.True(t, ok, "each log should be a map") + + // Verify required log fields + require.NotNil(t, logMap["address"], "log should have address") + require.NotNil(t, logMap["topics"], "log should have topics") + require.NotNil(t, logMap["data"], "log should have data") + + // Verify position tracking (should be 0 for non-EVM tx with no sub-calls) + position, ok := logMap["position"].(string) + require.True(t, ok, "position should be a string") + require.Equal(t, "0x0", position, "log position should be 0x0 for non-EVM tx") + + // Verify topics array contains expected values + topics, ok := logMap["topics"].([]interface{}) + require.True(t, ok, "topics should be an array") + require.Len(t, topics, 2, "should have 2 topics") + + // Verify log data is correctly captured + data, ok := logMap["data"].(string) + require.True(t, ok, "data should be a string") + require.NotEmpty(t, data, "data should not be empty") + } + } else { + // When WithLog=false, logs field may not exist or should be empty + if logsExist { + logArray, ok := logs.([]interface{}) + if ok { + require.Len(t, logArray, 0, "logs array should be empty when WithLog=false") + } + } + } + }) + } +} + +// TestCallTracerNonEVMTxLogNoDuplication tests that logs from one non-EVM +// transaction don't leak into the next transaction. +func TestCallTracerNonEVMTxLogNoDuplication(t *testing.T) { + config := json.RawMessage(`{"withLog":true}`) + tracer, err := tracers.DefaultDirectory.New("callTracer", &tracers.Context{}, config, params.MainnetChainConfig) + require.NoError(t, err) + + from := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + to := common.BlockSignersBinary + + vmContext := &tracing.VMContext{ + BlockNumber: big.NewInt(1), + } + + // First transaction: non-EVM with 2 logs + tx1 := types.NewTx(&types.LegacyTx{ + Nonce: 0, + To: &to, + Value: big.NewInt(0), + Gas: 100000, + GasPrice: big.NewInt(1), + Data: nil, + }) + + tracer.OnTxStart(vmContext, tx1, from) + + // Add 2 logs to first transaction + for i := 0; i < 2; i++ { + log := &types.Log{ + Address: to, + Topics: []common.Hash{common.HexToHash("0xaaaa")}, + Data: []byte{0xaa}, + } + tracer.OnLog(log) + } + + receipt1 := &types.Receipt{GasUsed: 21000} + tracer.OnTxEnd(receipt1, nil) + + result1, err := tracer.GetResult() + require.NoError(t, err) + + var callFrame1 map[string]interface{} + err = json.Unmarshal(result1, &callFrame1) + require.NoError(t, err) + + logs1, ok := callFrame1["logs"].([]interface{}) + require.True(t, ok) + require.Len(t, logs1, 2, "first transaction should have 2 logs") + + // Second transaction: non-EVM with 1 log + tx2 := types.NewTx(&types.LegacyTx{ + Nonce: 1, + To: &to, + Value: big.NewInt(0), + Gas: 100000, + GasPrice: big.NewInt(1), + Data: nil, + }) + + tracer.OnTxStart(vmContext, tx2, from) + + // Add only 1 log to second transaction + log := &types.Log{ + Address: to, + Topics: []common.Hash{common.HexToHash("0xbbbb")}, + Data: []byte{0xbb}, + } + tracer.OnLog(log) + + receipt2 := &types.Receipt{GasUsed: 21000} + tracer.OnTxEnd(receipt2, nil) + + result2, err := tracer.GetResult() + require.NoError(t, err) + + var callFrame2 map[string]interface{} + err = json.Unmarshal(result2, &callFrame2) + require.NoError(t, err) + + logs2, ok := callFrame2["logs"].([]interface{}) + require.True(t, ok) + require.Len(t, logs2, 1, "second transaction should have only 1 log (no duplication from first tx)") + + // Verify the log in second transaction is different from first + log2Map := logs2[0].(map[string]interface{}) + topics2 := log2Map["topics"].([]interface{}) + require.Contains(t, topics2[0].(string), "bbbb", "second transaction should have different log topics") +}