diff --git a/go.sum b/go.sum index 0ad7cf2c8b..0e2e0f16ff 100644 --- a/go.sum +++ b/go.sum @@ -94,7 +94,6 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= -github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/bwesterb/go-ristretto v1.2.0 h1:xxWOVbN5m8NNKiSDZXE1jtZvZnC6JSJ9cYFADiZcWtw= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -668,9 +667,6 @@ github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cb github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= -github.com/umbracle/ethgo v0.1.4-0.20220722090909-c8ac32939570 h1:/KyTftQQhxq0iRIVRocn0F2D4zoHmstIfB4FTDjsZbw= -github.com/umbracle/ethgo v0.1.4-0.20220722090909-c8ac32939570/go.mod h1:g9zclCLixH8liBI27Py82klDkW7Oo33AxUOr+M9lzrU= github.com/umbracle/ethgo v0.1.4-0.20221117101647-b81ef2f07953 h1:ep+lwZyyeh1iw8UojTxslDsqPw0305Vu6lOiEebWS8k= github.com/umbracle/ethgo v0.1.4-0.20221117101647-b81ef2f07953/go.mod h1:8QIHEG/YfGnW4I5AND2Znl9W0LU3tXR9IGqgmSieiGo= github.com/umbracle/fastrlp v0.0.0-20220527094140-59d5dd30e722 h1:10Nbw6cACsnQm7r34zlpJky+IzxVLRk6MKTS2d3Vp0E= diff --git a/jsonrpc/debug_endpoint.go b/jsonrpc/debug_endpoint.go new file mode 100644 index 0000000000..4c7bc94849 --- /dev/null +++ b/jsonrpc/debug_endpoint.go @@ -0,0 +1,230 @@ +package jsonrpc + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/0xPolygon/polygon-edge/helper/hex" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer/structtracer" + "github.com/0xPolygon/polygon-edge/types" +) + +var ( + defaultTraceTimeout = 5 * time.Second + + // ErrExecutionTimeout indicates the execution was terminated due to timeout + ErrExecutionTimeout = errors.New("execution timeout") + // ErrTraceGenesisBlock is an error returned when tracing genesis block which can't be traced + ErrTraceGenesisBlock = errors.New("genesis is not traceable") +) + +type debugBlockchainStore interface { + // Header returns the current header of the chain (genesis if empty) + Header() *types.Header + + // GetHeaderByNumber gets a header using the provided number + GetHeaderByNumber(uint64) (*types.Header, bool) + + // ReadTxLookup returns a block hash in which a given txn was mined + ReadTxLookup(txnHash types.Hash) (types.Hash, bool) + + // GetBlockByHash gets a block using the provided hash + GetBlockByHash(hash types.Hash, full bool) (*types.Block, bool) + + // GetBlockByNumber gets a block using the provided height + GetBlockByNumber(num uint64, full bool) (*types.Block, bool) + + // TraceBlock traces all transactions in the given block + TraceBlock(*types.Block, tracer.Tracer) ([]interface{}, error) + + // TraceTxn traces a transaction in the block, associated with the given hash + TraceTxn(*types.Block, types.Hash, tracer.Tracer) (interface{}, error) + + // TraceCall traces a single call at the point when the given header is mined + TraceCall(*types.Transaction, *types.Header, tracer.Tracer) (interface{}, error) +} + +type debugTxPoolStore interface { + GetNonce(types.Address) uint64 +} + +type debugStateStore interface { + GetAccount(root types.Hash, addr types.Address) (*Account, error) +} + +type debugStore interface { + debugBlockchainStore + debugTxPoolStore + debugStateStore +} + +// Debug is the debug jsonrpc endpoint +type Debug struct { + store debugStore +} + +type TraceConfig struct { + EnableMemory bool `json:"enableMemory"` + DisableStack bool `json:"disableStack"` + DisableStorage bool `json:"disableStorage"` + EnableReturnData bool `json:"enableReturnData"` + Timeout *string `json:"timeout"` +} + +func (d *Debug) TraceBlockByNumber( + blockNumber BlockNumber, + config *TraceConfig, +) (interface{}, error) { + num, err := GetNumericBlockNumber(blockNumber, d.store) + if err != nil { + return nil, err + } + + block, ok := d.store.GetBlockByNumber(num, true) + if !ok { + return nil, fmt.Errorf("block %d not found", num) + } + + return d.traceBlock(block, config) +} + +func (d *Debug) TraceBlockByHash( + blockHash types.Hash, + config *TraceConfig, +) (interface{}, error) { + block, ok := d.store.GetBlockByHash(blockHash, true) + if !ok { + return nil, fmt.Errorf("block %s not found", blockHash) + } + + return d.traceBlock(block, config) +} + +func (d *Debug) TraceBlock( + input string, + config *TraceConfig, +) (interface{}, error) { + blockByte, decodeErr := hex.DecodeHex(input) + if decodeErr != nil { + return nil, fmt.Errorf("unable to decode block, %w", decodeErr) + } + + block := &types.Block{} + if err := block.UnmarshalRLP(blockByte); err != nil { + return nil, err + } + + return d.traceBlock(block, config) +} + +func (d *Debug) TraceTransaction( + txHash types.Hash, + config *TraceConfig, +) (interface{}, error) { + tx, block := GetTxAndBlockByTxHash(txHash, d.store) + if tx == nil { + return nil, fmt.Errorf("tx %s not found", txHash.String()) + } + + if block.Number() == 0 { + return nil, ErrTraceGenesisBlock + } + + tracer, cancel, err := newTracer(config) + defer cancel() + + if err != nil { + return nil, err + } + + return d.store.TraceTxn(block, tx.Hash, tracer) +} + +func (d *Debug) TraceCall( + arg *txnArgs, + filter BlockNumberOrHash, + config *TraceConfig, +) (interface{}, error) { + header, err := GetHeaderFromBlockNumberOrHash(filter, d.store) + if err != nil { + return nil, ErrHeaderNotFound + } + + tx, err := DecodeTxn(arg, d.store) + if err != nil { + return nil, err + } + + // If the caller didn't supply the gas limit in the message, then we set it to maximum possible => block gas limit + if tx.Gas == 0 { + tx.Gas = header.GasLimit + } + + tracer, cancel, err := newTracer(config) + defer cancel() + + if err != nil { + return nil, err + } + + return d.store.TraceCall(tx, header, tracer) +} + +func (d *Debug) traceBlock( + block *types.Block, + config *TraceConfig, +) (interface{}, error) { + if block.Number() == 0 { + return nil, ErrTraceGenesisBlock + } + + tracer, cancel, err := newTracer(config) + defer cancel() + + if err != nil { + return nil, err + } + + return d.store.TraceBlock(block, tracer) +} + +// newTracer creates new tracer by config +func newTracer(config *TraceConfig) ( + tracer.Tracer, + context.CancelFunc, + error, +) { + var ( + timeout = defaultTraceTimeout + err error + ) + + if config.Timeout != nil { + if timeout, err = time.ParseDuration(*config.Timeout); err != nil { + return nil, nil, err + } + } + + tracer := structtracer.NewStructTracer(structtracer.Config{ + EnableMemory: config.EnableMemory, + EnableStack: !config.DisableStack, + EnableStorage: !config.DisableStorage, + EnableReturnData: config.EnableReturnData, + }) + + timeoutCtx, cancel := context.WithTimeout(context.Background(), timeout) + + go func() { + <-timeoutCtx.Done() + + if errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) { + tracer.Cancel(ErrExecutionTimeout) + } + }() + + // cancellation of context is done by caller + return tracer, cancel, nil +} diff --git a/jsonrpc/debug_endpoint_test.go b/jsonrpc/debug_endpoint_test.go new file mode 100644 index 0000000000..67c5c42409 --- /dev/null +++ b/jsonrpc/debug_endpoint_test.go @@ -0,0 +1,759 @@ +package jsonrpc + +import ( + "encoding/json" + "math/big" + "testing" + "time" + + "github.com/0xPolygon/polygon-edge/helper/hex" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer" + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/assert" +) + +type debugEndpointMockStore struct { + headerFn func() *types.Header + getHeaderByNumberFn func(uint64) (*types.Header, bool) + readTxLookupFn func(types.Hash) (types.Hash, bool) + getBlockByHashFn func(types.Hash, bool) (*types.Block, bool) + getBlockByNumberFn func(uint64, bool) (*types.Block, bool) + traceBlockFn func(*types.Block, tracer.Tracer) ([]interface{}, error) + traceTxnFn func(*types.Block, types.Hash, tracer.Tracer) (interface{}, error) + traceCallFn func(*types.Transaction, *types.Header, tracer.Tracer) (interface{}, error) + getNonceFn func(types.Address) uint64 + getAccountFn func(types.Hash, types.Address) (*Account, error) +} + +func (s *debugEndpointMockStore) Header() *types.Header { + return s.headerFn() +} + +func (s *debugEndpointMockStore) GetHeaderByNumber(num uint64) (*types.Header, bool) { + return s.getHeaderByNumberFn(num) +} + +func (s *debugEndpointMockStore) ReadTxLookup(txnHash types.Hash) (types.Hash, bool) { + return s.readTxLookupFn(txnHash) +} + +func (s *debugEndpointMockStore) GetBlockByHash(hash types.Hash, full bool) (*types.Block, bool) { + return s.getBlockByHashFn(hash, full) +} + +func (s *debugEndpointMockStore) GetBlockByNumber(num uint64, full bool) (*types.Block, bool) { + return s.getBlockByNumberFn(num, full) +} + +func (s *debugEndpointMockStore) TraceBlock(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { + return s.traceBlockFn(block, tracer) +} + +func (s *debugEndpointMockStore) TraceTxn(block *types.Block, targetTx types.Hash, tracer tracer.Tracer) (interface{}, error) { + return s.traceTxnFn(block, targetTx, tracer) +} + +func (s *debugEndpointMockStore) TraceCall(tx *types.Transaction, parent *types.Header, tracer tracer.Tracer) (interface{}, error) { + return s.traceCallFn(tx, parent, tracer) +} + +func (s *debugEndpointMockStore) GetNonce(acc types.Address) uint64 { + return s.getNonceFn(acc) +} + +func (s *debugEndpointMockStore) GetAccount(root types.Hash, addr types.Address) (*Account, error) { + return s.getAccountFn(root, addr) +} + +func TestDebugTraceConfigDecode(t *testing.T) { + timeout15s := "15s" + + tests := []struct { + input string + expected TraceConfig + }{ + { + // default + input: `{}`, + expected: TraceConfig{ + EnableMemory: false, + DisableStack: false, + DisableStorage: false, + EnableReturnData: false, + }, + }, + { + input: `{ + "enableMemory": true + }`, + expected: TraceConfig{ + EnableMemory: true, + DisableStack: false, + DisableStorage: false, + EnableReturnData: false, + }, + }, + { + input: `{ + "disableStack": true + }`, + expected: TraceConfig{ + EnableMemory: false, + DisableStack: true, + DisableStorage: false, + EnableReturnData: false, + }, + }, + { + input: `{ + "disableStorage": true + }`, + expected: TraceConfig{ + EnableMemory: false, + DisableStack: false, + DisableStorage: true, + EnableReturnData: false, + }, + }, + { + input: `{ + "enableReturnData": true + }`, + expected: TraceConfig{ + EnableMemory: false, + DisableStack: false, + DisableStorage: false, + EnableReturnData: true, + }, + }, + { + input: `{ + "timeout": "15s" + }`, + expected: TraceConfig{ + EnableMemory: false, + DisableStack: false, + DisableStorage: false, + EnableReturnData: false, + Timeout: &timeout15s, + }, + }, + { + input: `{ + "enableMemory": true, + "disableStack": true, + "disableStorage": true, + "enableReturnData": true, + "timeout": "15s" + }`, + expected: TraceConfig{ + EnableMemory: true, + DisableStack: true, + DisableStorage: true, + EnableReturnData: true, + Timeout: &timeout15s, + }, + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result := TraceConfig{} + + assert.NoError( + t, + json.Unmarshal( + []byte(test.input), + &result, + ), + ) + + assert.Equal( + t, + test.expected, + result, + ) + }) + } +} + +func TestTraceBlockByNumber(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + blockNumber BlockNumber + config *TraceConfig + store *debugEndpointMockStore + result interface{} + err bool + }{ + { + name: "should trace the latest block", + blockNumber: LatestBlockNumber, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestHeader + }, + getBlockByNumberFn: func(num uint64, full bool) (*types.Block, bool) { + assert.Equal(t, testLatestHeader.Number, num) + assert.True(t, full) + + return testLatestBlock, true + }, + traceBlockFn: func(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { + assert.Equal(t, testLatestBlock, block) + + return testTraceResults, nil + }, + }, + result: testTraceResults, + err: false, + }, + { + name: "should trace the block at the given height", + blockNumber: 10, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + getBlockByNumberFn: func(num uint64, full bool) (*types.Block, bool) { + assert.Equal(t, testHeader10.Number, num) + assert.True(t, full) + + return testBlock10, true + }, + traceBlockFn: func(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { + assert.Equal(t, testBlock10, block) + + return testTraceResults, nil + }, + }, + result: testTraceResults, + err: false, + }, + { + name: "should return errTraceGenesisBlock for genesis block", + blockNumber: 0, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + getBlockByNumberFn: func(num uint64, full bool) (*types.Block, bool) { + assert.Equal(t, testGenesisHeader.Number, num) + assert.True(t, full) + + return testGenesisBlock, true + }, + }, + result: nil, + err: true, + }, + { + name: "should return errBlockNotFound", + blockNumber: 11, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + getBlockByNumberFn: func(num uint64, full bool) (*types.Block, bool) { + assert.Equal(t, uint64(11), num) + assert.True(t, full) + + return nil, false + }, + }, + result: nil, + err: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + endpoint := &Debug{test.store} + + res, err := endpoint.TraceBlockByNumber(test.blockNumber, test.config) + + assert.Equal(t, test.result, res) + + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTraceBlockByHash(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + blockHash types.Hash + config *TraceConfig + store *debugEndpointMockStore + result interface{} + err bool + }{ + { + name: "should trace the latest block", + blockHash: testHeader10.Hash, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testHeader10.Hash, hash) + assert.True(t, full) + + return testBlock10, true + }, + traceBlockFn: func(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { + assert.Equal(t, testBlock10, block) + + return testTraceResults, nil + }, + }, + result: testTraceResults, + err: false, + }, + { + name: "should return errBlockNotFound", + blockHash: testHash11, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testHash11, hash) + assert.True(t, full) + + return nil, false + }, + }, + result: nil, + err: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + endpoint := &Debug{test.store} + + res, err := endpoint.TraceBlockByHash(test.blockHash, test.config) + + assert.Equal(t, test.result, res) + + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTraceBlock(t *testing.T) { + t.Parallel() + + blockBytes := testLatestBlock.MarshalRLP() + blockHex := hex.EncodeToHex(blockBytes) + + tests := []struct { + name string + input string + config *TraceConfig + store *debugEndpointMockStore + result interface{} + err bool + }{ + { + name: "should trace the given block", + input: blockHex, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + traceBlockFn: func(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { + assert.Equal(t, testLatestBlock, block) + + return testTraceResults, nil + }, + }, + result: testTraceResults, + err: false, + }, + { + name: "should return error in case of invalid block", + input: "hoge", + config: &TraceConfig{}, + store: &debugEndpointMockStore{}, + result: nil, + err: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + endpoint := &Debug{test.store} + + res, err := endpoint.TraceBlock(test.input, test.config) + + assert.Equal(t, test.result, res) + + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTraceTransaction(t *testing.T) { + t.Parallel() + + blockWithTx := &types.Block{ + Header: testBlock10.Header, + Transactions: []*types.Transaction{ + testTx1, + }, + } + + tests := []struct { + name string + txHash types.Hash + config *TraceConfig + store *debugEndpointMockStore + result interface{} + err bool + }{ + { + name: "should trace the given transaction", + txHash: testTxHash1, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTxHash1, hash) + + return testBlock10.Hash(), true + }, + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testBlock10.Hash(), hash) + assert.True(t, full) + + return blockWithTx, true + }, + traceTxnFn: func(block *types.Block, txHash types.Hash, tracer tracer.Tracer) (interface{}, error) { + assert.Equal(t, blockWithTx, block) + assert.Equal(t, testTxHash1, txHash) + + return testTraceResult, nil + }, + }, + result: testTraceResult, + err: false, + }, + { + name: "should return error if ReadTxLookup returns null", + txHash: testTxHash1, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTxHash1, hash) + + return types.ZeroHash, false + }, + }, + result: nil, + err: true, + }, + { + name: "should return error if block not found", + txHash: testTxHash1, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTxHash1, hash) + + return testBlock10.Hash(), true + }, + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testBlock10.Hash(), hash) + assert.True(t, full) + + return nil, false + }, + }, + result: nil, + err: true, + }, + { + name: "should return error if the tx is not including the block", + txHash: testTxHash1, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTxHash1, hash) + + return testBlock10.Hash(), true + }, + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testBlock10.Hash(), hash) + assert.True(t, full) + + return testBlock10, true + }, + }, + result: nil, + err: true, + }, + { + name: "should return error if the block is genesis", + txHash: testTxHash1, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTxHash1, hash) + + return testBlock10.Hash(), true + }, + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testBlock10.Hash(), hash) + assert.True(t, full) + + return &types.Block{ + Header: testGenesisHeader, + Transactions: []*types.Transaction{ + testTx1, + }, + }, true + }, + }, + result: nil, + err: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + endpoint := &Debug{test.store} + + res, err := endpoint.TraceTransaction(test.txHash, test.config) + + assert.Equal(t, test.result, res) + + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTraceCall(t *testing.T) { + t.Parallel() + + var ( + from = types.StringToAddress("1") + to = types.StringToAddress("2") + gas = argUint64(10000) + gasPrice = argBytes(new(big.Int).SetUint64(10).Bytes()) + value = argBytes(new(big.Int).SetUint64(1000).Bytes()) + data = argBytes([]byte("data")) + input = argBytes([]byte("input")) + nonce = argUint64(1) + + blockNumber = BlockNumber(testBlock10.Number()) + + txArg = &txnArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: &gasPrice, + Value: &value, + Data: &data, + Input: &input, + Nonce: &nonce, + } + decodedTx = &types.Transaction{ + Nonce: uint64(nonce), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + Gas: uint64(gas), + To: &to, + Value: new(big.Int).SetBytes([]byte(value)), + Input: data, + From: from, + } + ) + + decodedTx.ComputeHash() + + tests := []struct { + name string + arg *txnArgs + filter BlockNumberOrHash + config *TraceConfig + store *debugEndpointMockStore + result interface{} + err bool + }{ + { + name: "should trace the given transaction", + arg: txArg, + filter: BlockNumberOrHash{ + BlockNumber: &blockNumber, + }, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, testBlock10.Number(), num) + + return testHeader10, true + }, + traceCallFn: func(tx *types.Transaction, header *types.Header, tracer tracer.Tracer) (interface{}, error) { + assert.Equal(t, decodedTx, tx) + assert.Equal(t, testHeader10, header) + + return testTraceResult, nil + }, + }, + result: testTraceResult, + err: false, + }, + { + name: "should return error if block not found", + arg: txArg, + filter: BlockNumberOrHash{ + BlockHash: &testHeader10.Hash, + }, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testHeader10.Hash, hash) + assert.False(t, full) + + return nil, false + }, + }, + result: nil, + err: true, + }, + { + name: "should return error if decoding transaction fails", + arg: &txnArgs{ + From: &from, + Gas: &gas, + GasPrice: &gasPrice, + Value: &value, + Nonce: &nonce, + }, + filter: BlockNumberOrHash{}, + config: &TraceConfig{}, + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestHeader + }, + }, + result: nil, + err: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + endpoint := &Debug{test.store} + + res, err := endpoint.TraceCall(test.arg, test.filter, test.config) + + assert.Equal(t, test.result, res) + + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_newTracer(t *testing.T) { + t.Parallel() + + t.Run("should create tracer", func(t *testing.T) { + t.Parallel() + + tracer, cancel, err := newTracer(&TraceConfig{ + EnableMemory: true, + EnableReturnData: true, + DisableStack: false, + DisableStorage: false, + }) + + t.Cleanup(func() { + cancel() + }) + + assert.NotNil(t, tracer) + assert.NoError(t, err) + }) + + t.Run("GetResult should return errExecutionTimeout if timeout happens", func(t *testing.T) { + t.Parallel() + + timeout := "0s" + tracer, cancel, err := newTracer(&TraceConfig{ + EnableMemory: true, + EnableReturnData: true, + DisableStack: false, + DisableStorage: false, + Timeout: &timeout, + }) + + t.Cleanup(func() { + cancel() + }) + + assert.NoError(t, err) + + // wait until timeout + time.Sleep(100 * time.Millisecond) + + res, err := tracer.GetResult() + assert.Nil(t, res) + assert.Equal(t, ErrExecutionTimeout, err) + }) + + t.Run("GetResult should not return if cancel is called beforre timeout", func(t *testing.T) { + t.Parallel() + + timeout := "5s" + tracer, cancel, err := newTracer(&TraceConfig{ + EnableMemory: true, + EnableReturnData: true, + DisableStack: false, + DisableStorage: false, + Timeout: &timeout, + }) + + assert.NoError(t, err) + + cancel() + + res, err := tracer.GetResult() + + assert.NotNil(t, res) + assert.NoError(t, err) + }) +} diff --git a/jsonrpc/dispatcher.go b/jsonrpc/dispatcher.go index 342fd99b55..5a536d4ff9 100644 --- a/jsonrpc/dispatcher.go +++ b/jsonrpc/dispatcher.go @@ -34,6 +34,7 @@ type endpoints struct { Web3 *Web3 Net *Net TxPool *TxPool + Debug *Debug } // Dispatcher handles all json rpc requests by delegating @@ -92,12 +93,18 @@ func (d *Dispatcher) registerEndpoints(store JSONRPCStore) { d.params.chainID, d.params.chainName, } - d.endpoints.TxPool = &TxPool{store} + d.endpoints.TxPool = &TxPool{ + store, + } + d.endpoints.Debug = &Debug{ + store, + } d.registerService("eth", d.endpoints.Eth) d.registerService("net", d.endpoints.Net) d.registerService("web3", d.endpoints.Web3) d.registerService("txpool", d.endpoints.TxPool) + d.registerService("debug", d.endpoints.Debug) } func (d *Dispatcher) getFnHandler(req Request) (*serviceData, *funcData, Error) { diff --git a/jsonrpc/eth_blockchain_test.go b/jsonrpc/eth_blockchain_test.go index 74025ea8c5..f29b689780 100644 --- a/jsonrpc/eth_blockchain_test.go +++ b/jsonrpc/eth_blockchain_test.go @@ -335,8 +335,12 @@ func TestEth_Call(t *testing.T) { }) } -type mockBlockStore struct { +type testStore interface { ethStore +} + +type mockBlockStore struct { + testStore blocks []*types.Block topics []types.Hash pendingTxns []*types.Transaction diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index 7997d23acf..83d8b876f9 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -17,14 +17,14 @@ import ( ) type ethTxPoolStore interface { - // GetNonce returns the next nonce for this address - GetNonce(addr types.Address) uint64 - // AddTx adds a new transaction to the tx pool AddTx(tx *types.Transaction) error // GetPendingTx gets the pending transaction from the transaction pool, if it's present GetPendingTx(txHash types.Hash) (*types.Transaction, bool) + + // GetNonce returns the next nonce for this address + GetNonce(addr types.Address) uint64 } type Account struct { @@ -43,6 +43,9 @@ type ethBlockchainStore interface { // Header returns the current header of the chain (genesis if empty) Header() *types.Header + // GetHeaderByNumber gets a header using the provided number + GetHeaderByNumber(uint64) (*types.Header, bool) + // GetBlockByHash gets a block using the provided hash GetBlockByHash(hash types.Hash, full bool) (*types.Block, bool) @@ -92,31 +95,6 @@ func (e *Eth) ChainId() (interface{}, error) { return argUintPtr(e.chainID), nil } -func (e *Eth) getHeaderFromBlockNumberOrHash(bnh BlockNumberOrHash) (*types.Header, error) { - // The filter is empty, use the latest block by default - if bnh.BlockNumber == nil && bnh.BlockHash == nil { - bnh.BlockNumber, _ = createBlockNumberPointer(latest) - } - - if bnh.BlockNumber != nil { - // block number - header, err := e.getBlockHeader(*bnh.BlockNumber) - if err != nil { - return nil, fmt.Errorf("failed to get the header of block %d: %w", *bnh.BlockNumber, err) - } - - return header, nil - } - - // block hash - block, ok := e.store.GetBlockByHash(*bnh.BlockHash, false) - if !ok { - return nil, fmt.Errorf("could not find block referenced by the hash %s", bnh.BlockHash.String()) - } - - return block.Header, nil -} - func (e *Eth) Syncing() (interface{}, error) { if syncProgression := e.store.GetSyncProgression(); syncProgression != nil { // Node is bulk syncing, return the status @@ -132,35 +110,14 @@ func (e *Eth) Syncing() (interface{}, error) { return false, nil } -func GetNumericBlockNumber(number BlockNumber, e *Eth) (uint64, error) { - switch number { - case LatestBlockNumber: - return e.store.Header().Number, nil - - case EarliestBlockNumber: - return 0, nil - - case PendingBlockNumber: - return 0, fmt.Errorf("fetching the pending header is not supported") - - default: - if number < 0 { - return 0, fmt.Errorf("invalid argument 0: block number larger than int64") - } - - return uint64(number), nil - } -} - // GetBlockByNumber returns information about a block by block number func (e *Eth) GetBlockByNumber(number BlockNumber, fullTx bool) (interface{}, error) { - num, err := GetNumericBlockNumber(number, e) + num, err := GetNumericBlockNumber(number, e.store) if err != nil { return nil, err } block, ok := e.store.GetBlockByNumber(num, true) - if !ok { return nil, nil } @@ -179,7 +136,7 @@ func (e *Eth) GetBlockByHash(hash types.Hash, fullTx bool) (interface{}, error) } func (e *Eth) GetBlockTransactionCountByNumber(number BlockNumber) (interface{}, error) { - num, err := GetNumericBlockNumber(number, e) + num, err := GetNumericBlockNumber(number, e.store) if err != nil { return nil, err } @@ -386,7 +343,7 @@ func (e *Eth) GetStorageAt( index types.Hash, filter BlockNumberOrHash, ) (interface{}, error) { - header, err := e.getHeaderFromBlockNumberOrHash(filter) + header, err := GetHeaderFromBlockNumberOrHash(filter, e.store) if err != nil { return nil, err } @@ -432,12 +389,12 @@ func (e *Eth) GasPrice() (interface{}, error) { // Call executes a smart contract call using the transaction object data func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash) (interface{}, error) { - header, err := e.getHeaderFromBlockNumberOrHash(filter) + header, err := GetHeaderFromBlockNumberOrHash(filter, e.store) if err != nil { return nil, err } - transaction, err := e.decodeTxn(arg) + transaction, err := DecodeTxn(arg, e.store) if err != nil { return nil, err } @@ -466,7 +423,7 @@ func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash) (interface{}, error) // EstimateGas estimates the gas needed to execute a transaction func (e *Eth) EstimateGas(arg *txnArgs, rawNum *BlockNumber) (interface{}, error) { - transaction, err := e.decodeTxn(arg) + transaction, err := DecodeTxn(arg, e.store) if err != nil { return nil, err } @@ -477,7 +434,7 @@ func (e *Eth) EstimateGas(arg *txnArgs, rawNum *BlockNumber) (interface{}, error } // Fetch the requested header - header, err := e.getBlockHeader(number) + header, err := GetBlockHeader(number, e.store) if err != nil { return nil, err } @@ -670,7 +627,7 @@ func (e *Eth) GetLogs(query *LogQuery) (interface{}, error) { // GetBalance returns the account's balance at the referenced block. func (e *Eth) GetBalance(address types.Address, filter BlockNumberOrHash) (interface{}, error) { - header, err := e.getHeaderFromBlockNumberOrHash(filter) + header, err := GetHeaderFromBlockNumberOrHash(filter, e.store) if err != nil { return nil, err } @@ -701,7 +658,7 @@ func (e *Eth) GetTransactionCount(address types.Address, filter BlockNumberOrHas } if filter.BlockNumber == nil { - header, err = e.getHeaderFromBlockNumberOrHash(filter) + header, err = GetHeaderFromBlockNumberOrHash(filter, e.store) if err != nil { return nil, fmt.Errorf("failed to get header from block hash or block number: %w", err) } @@ -711,7 +668,7 @@ func (e *Eth) GetTransactionCount(address types.Address, filter BlockNumberOrHas blockNumber = *filter.BlockNumber } - nonce, err := e.getNextNonce(address, blockNumber) + nonce, err := GetNextNonce(address, blockNumber, e.store) if err != nil { if errors.Is(err, ErrStateNotFound) { return argUintPtr(0), nil @@ -725,7 +682,7 @@ func (e *Eth) GetTransactionCount(address types.Address, filter BlockNumberOrHas // GetCode returns account code at given block number func (e *Eth) GetCode(address types.Address, filter BlockNumberOrHash) (interface{}, error) { - header, err := e.getHeaderFromBlockNumberOrHash(filter) + header, err := GetHeaderFromBlockNumberOrHash(filter, e.store) if err != nil { return nil, err } @@ -768,119 +725,3 @@ func (e *Eth) UninstallFilter(id string) (bool, error) { func (e *Eth) Unsubscribe(id string) (bool, error) { return e.filterManager.Uninstall(id), nil } - -func (e *Eth) getBlockHeader(number BlockNumber) (*types.Header, error) { - switch number { - case LatestBlockNumber: - return e.store.Header(), nil - - case EarliestBlockNumber: - block, ok := e.store.GetBlockByNumber(uint64(0), false) - if !ok { - return nil, fmt.Errorf("error fetching genesis block header") - } - - return block.Header, nil - - case PendingBlockNumber: - return nil, fmt.Errorf("fetching the pending header is not supported") - - default: - // Convert the block number from hex to uint64 - block, ok := e.store.GetBlockByNumber(uint64(number), false) - if !ok { - return nil, fmt.Errorf("error fetching block number %d header", uint64(number)) - } - - return block.Header, nil - } -} - -// getNextNonce returns the next nonce for the account for the specified block -func (e *Eth) getNextNonce(address types.Address, number BlockNumber) (uint64, error) { - if number == PendingBlockNumber { - // Grab the latest pending nonce from the TxPool - // - // If the account is not initialized in the local TxPool, - // return the latest nonce from the world state - res := e.store.GetNonce(address) - - return res, nil - } - - header, err := e.getBlockHeader(number) - if err != nil { - return 0, err - } - - acc, err := e.store.GetAccount(header.StateRoot, address) - if errors.Is(err, ErrStateNotFound) { - // If the account doesn't exist / isn't initialized, - // return a nonce value of 0 - return 0, nil - } else if err != nil { - return 0, err - } - - return acc.Nonce, nil -} - -func (e *Eth) decodeTxn(arg *txnArgs) (*types.Transaction, error) { - // set default values - if arg.From == nil { - arg.From = &types.ZeroAddress - arg.Nonce = argUintPtr(0) - } else if arg.Nonce == nil { - // get nonce from the pool - nonce, err := e.getNextNonce(*arg.From, LatestBlockNumber) - if err != nil { - return nil, err - } - arg.Nonce = argUintPtr(nonce) - } - - if arg.Value == nil { - arg.Value = argBytesPtr([]byte{}) - } - - if arg.GasPrice == nil { - arg.GasPrice = argBytesPtr([]byte{}) - } - - var input []byte - if arg.Data != nil { - input = *arg.Data - } else if arg.Input != nil { - input = *arg.Input - } - - if arg.To == nil { - if input == nil { - return nil, fmt.Errorf("contract creation without data provided") - } - } - - if input == nil { - input = []byte{} - } - - if arg.Gas == nil { - arg.Gas = argUintPtr(0) - } - - txn := &types.Transaction{ - From: *arg.From, - Gas: uint64(*arg.Gas), - GasPrice: new(big.Int).SetBytes(*arg.GasPrice), - Value: new(big.Int).SetBytes(*arg.Value), - Input: input, - Nonce: uint64(*arg.Nonce), - } - if arg.To != nil { - txn.To = arg.To - } - - txn.ComputeHash() - - return txn, nil -} diff --git a/jsonrpc/eth_endpoint_test.go b/jsonrpc/eth_endpoint_test.go index 580fb2ac91..696bed3a1d 100644 --- a/jsonrpc/eth_endpoint_test.go +++ b/jsonrpc/eth_endpoint_test.go @@ -156,8 +156,7 @@ func TestEth_DecodeTxn(t *testing.T) { store.SetAccount(addr, acc) } - eth := newTestEthEndpoint(store) - res, err := eth.decodeTxn(tt.arg) + res, err := DecodeTxn(tt.arg, store) assert.Equal(t, tt.res, res) assert.Equal(t, tt.err, err) }) @@ -220,7 +219,7 @@ func TestEth_GetNextNonce(t *testing.T) { t.Parallel() // Grab the nonce - nonce, err := eth.getNextNonce(testCase.account, testCase.number) + nonce, err := GetNextNonce(testCase.account, testCase.number, eth.store) // Assert errors assert.NoError(t, err) @@ -231,12 +230,16 @@ func TestEth_GetNextNonce(t *testing.T) { } } -func newTestEthEndpoint(store ethStore) *Eth { - return &Eth{hclog.NewNullLogger(), store, 100, nil, 0} +func newTestEthEndpoint(store testStore) *Eth { + return &Eth{ + hclog.NewNullLogger(), store, 100, nil, 0, + } } -func newTestEthEndpointWithPriceLimit(store ethStore, priceLimit uint64) *Eth { - return &Eth{hclog.NewNullLogger(), store, 100, nil, priceLimit} +func newTestEthEndpointWithPriceLimit(store testStore, priceLimit uint64) *Eth { + return &Eth{ + hclog.NewNullLogger(), store, 100, nil, priceLimit, + } } func TestEth_HeaderResolveBlock(t *testing.T) { @@ -244,8 +247,6 @@ func TestEth_HeaderResolveBlock(t *testing.T) { store := newMockStore() store.header.Number = 10 - eth := newTestEthEndpoint(store) - latest := LatestBlockNumber blockNum5 := BlockNumber(5) blockNum10 := BlockNumber(10) @@ -288,7 +289,7 @@ func TestEth_HeaderResolveBlock(t *testing.T) { } for _, c := range cases { - header, err := eth.getHeaderFromBlockNumberOrHash(c.filter) + header, err := GetHeaderFromBlockNumberOrHash(c.filter, store) if c.err { assert.Error(t, err) } else { diff --git a/jsonrpc/eth_state_test.go b/jsonrpc/eth_state_test.go index e9b1d2b482..8aec664ae8 100644 --- a/jsonrpc/eth_state_test.go +++ b/jsonrpc/eth_state_test.go @@ -801,6 +801,14 @@ func (m *mockSpecialStore) Header() *types.Header { return m.block.Header } +func (m *mockSpecialStore) GetHeaderByNumber(num uint64) (*types.Header, bool) { + if m.block.Header.Number != num { + return nil, false + } + + return m.block.Header, true +} + func (m *mockSpecialStore) GetNonce(addr types.Address) uint64 { return 1 } diff --git a/jsonrpc/eth_txpool_test.go b/jsonrpc/eth_txpool_test.go index 6bab83823b..9aa9eae512 100644 --- a/jsonrpc/eth_txpool_test.go +++ b/jsonrpc/eth_txpool_test.go @@ -61,6 +61,7 @@ func (m *mockStoreTxn) AddTx(tx *types.Transaction) error { func (m *mockStoreTxn) GetNonce(addr types.Address) uint64 { return 1 } + func (m *mockStoreTxn) AddAccount(addr types.Address) *mockAccount { if m.accounts == nil { m.accounts = map[types.Address]*mockAccount{} diff --git a/jsonrpc/filter_manager.go b/jsonrpc/filter_manager.go index 07ea05dffd..670bdc02c3 100644 --- a/jsonrpc/filter_manager.go +++ b/jsonrpc/filter_manager.go @@ -24,7 +24,6 @@ var ( ErrBlockNotFound = errors.New("block not found") ErrIncorrectBlockRange = errors.New("incorrect range") ErrBlockRangeTooHigh = errors.New("block range too high") - ErrPendingBlockNumber = errors.New("pending block number is not supported") ErrNoWSConnection = errors.New("no websocket connection") ) @@ -398,27 +397,12 @@ func (f *FilterManager) getLogsFromBlock(query *LogQuery, block *types.Block) ([ } func (f *FilterManager) getLogsFromBlocks(query *LogQuery) ([]*Log, error) { - latestBlockNumber := f.store.Header().Number - - resolveNum := func(num BlockNumber) (uint64, error) { - switch num { - case PendingBlockNumber: - return 0, ErrPendingBlockNumber - case EarliestBlockNumber: - num = 0 - case LatestBlockNumber: - return latestBlockNumber, nil - } - - return uint64(num), nil - } - - from, err := resolveNum(query.fromBlock) + from, err := GetNumericBlockNumber(query.fromBlock, f.store) if err != nil { return nil, err } - to, err := resolveNum(query.toBlock) + to, err := GetNumericBlockNumber(query.toBlock, f.store) if err != nil { return nil, err } diff --git a/jsonrpc/helper.go b/jsonrpc/helper.go new file mode 100644 index 0000000000..da54bce95e --- /dev/null +++ b/jsonrpc/helper.go @@ -0,0 +1,233 @@ +package jsonrpc + +import ( + "errors" + "fmt" + "math/big" + + "github.com/0xPolygon/polygon-edge/types" +) + +var ( + ErrHeaderNotFound = errors.New("header not found") + ErrLatestNotFound = errors.New("latest header not found") + ErrPendingBlockNumber = errors.New("fetching the pending header is not supported") + ErrNegativeBlockNumber = errors.New("invalid argument 0: block number must not be negative") + ErrFailedFetchGenesis = errors.New("error fetching genesis block header") + ErrNoDataInContractCreation = errors.New("contract creation without data provided") +) + +type latestHeaderGetter interface { + Header() *types.Header +} + +// GetNumericBlockNumber returns block number based on current state or specified number +func GetNumericBlockNumber(number BlockNumber, store latestHeaderGetter) (uint64, error) { + switch number { + case LatestBlockNumber: + latest := store.Header() + if latest == nil { + return 0, ErrLatestNotFound + } + + return latest.Number, nil + + case EarliestBlockNumber: + return 0, nil + + case PendingBlockNumber: + return 0, ErrPendingBlockNumber + + default: + if number < 0 { + return 0, ErrNegativeBlockNumber + } + + return uint64(number), nil + } +} + +type headerGetter interface { + Header() *types.Header + GetHeaderByNumber(uint64) (*types.Header, bool) +} + +// GetBlockHeader returns a header using the provided number +func GetBlockHeader(number BlockNumber, store headerGetter) (*types.Header, error) { + switch number { + case LatestBlockNumber: + return store.Header(), nil + + case EarliestBlockNumber: + header, ok := store.GetHeaderByNumber(uint64(0)) + if !ok { + return nil, ErrFailedFetchGenesis + } + + return header, nil + + case PendingBlockNumber: + return nil, ErrPendingBlockNumber + + default: + // Convert the block number from hex to uint64 + header, ok := store.GetHeaderByNumber(uint64(number)) + if !ok { + return nil, fmt.Errorf("error fetching block number %d header", uint64(number)) + } + + return header, nil + } +} + +type txLookupAndBlockGetter interface { + ReadTxLookup(types.Hash) (types.Hash, bool) + GetBlockByHash(types.Hash, bool) (*types.Block, bool) +} + +// GetTxAndBlockByTxHash returns the tx and the block including the tx by given tx hash +func GetTxAndBlockByTxHash(txHash types.Hash, store txLookupAndBlockGetter) (*types.Transaction, *types.Block) { + blockHash, ok := store.ReadTxLookup(txHash) + if !ok { + return nil, nil + } + + block, ok := store.GetBlockByHash(blockHash, true) + if !ok { + return nil, nil + } + + for _, txn := range block.Transactions { + if txn.Hash == txHash { + return txn, block + } + } + + return nil, nil +} + +type blockGetter interface { + Header() *types.Header + GetHeaderByNumber(uint64) (*types.Header, bool) + GetBlockByHash(types.Hash, bool) (*types.Block, bool) +} + +func GetHeaderFromBlockNumberOrHash(bnh BlockNumberOrHash, store blockGetter) (*types.Header, error) { + // The filter is empty, use the latest block by default + if bnh.BlockNumber == nil && bnh.BlockHash == nil { + bnh.BlockNumber, _ = createBlockNumberPointer(latest) + } + + if bnh.BlockNumber != nil { + // block number + header, err := GetBlockHeader(*bnh.BlockNumber, store) + if err != nil { + return nil, fmt.Errorf("failed to get the header of block %d: %w", *bnh.BlockNumber, err) + } + + return header, nil + } + + // block hash + block, ok := store.GetBlockByHash(*bnh.BlockHash, false) + if !ok { + return nil, fmt.Errorf("could not find block referenced by the hash %s", bnh.BlockHash.String()) + } + + return block.Header, nil +} + +type nonceGetter interface { + Header() *types.Header + GetHeaderByNumber(uint64) (*types.Header, bool) + GetNonce(types.Address) uint64 + GetAccount(root types.Hash, addr types.Address) (*Account, error) +} + +func GetNextNonce(address types.Address, number BlockNumber, store nonceGetter) (uint64, error) { + if number == PendingBlockNumber { + // Grab the latest pending nonce from the TxPool + // + // If the account is not initialized in the local TxPool, + // return the latest nonce from the world state + res := store.GetNonce(address) + + return res, nil + } + + header, err := GetBlockHeader(number, store) + if err != nil { + return 0, err + } + + acc, err := store.GetAccount(header.StateRoot, address) + + //nolint:govet + if errors.Is(err, ErrStateNotFound) { + // If the account doesn't exist / isn't initialized, + // return a nonce value of 0 + return 0, nil + } else if err != nil { + return 0, err + } + + return acc.Nonce, nil +} + +func DecodeTxn(arg *txnArgs, store nonceGetter) (*types.Transaction, error) { + // set default values + if arg.From == nil { + arg.From = &types.ZeroAddress + arg.Nonce = argUintPtr(0) + } else if arg.Nonce == nil { + // get nonce from the pool + nonce, err := GetNextNonce(*arg.From, LatestBlockNumber, store) + if err != nil { + return nil, err + } + arg.Nonce = argUintPtr(nonce) + } + + if arg.Value == nil { + arg.Value = argBytesPtr([]byte{}) + } + + if arg.GasPrice == nil { + arg.GasPrice = argBytesPtr([]byte{}) + } + + var input []byte + if arg.Data != nil { + input = *arg.Data + } else if arg.Input != nil { + input = *arg.Input + } + + if arg.To == nil && input == nil { + return nil, ErrNoDataInContractCreation + } + + if input == nil { + input = []byte{} + } + + if arg.Gas == nil { + arg.Gas = argUintPtr(0) + } + + txn := &types.Transaction{ + From: *arg.From, + Gas: uint64(*arg.Gas), + GasPrice: new(big.Int).SetBytes(*arg.GasPrice), + Value: new(big.Int).SetBytes(*arg.Value), + Input: input, + Nonce: uint64(*arg.Nonce), + } + if arg.To != nil { + txn.To = arg.To + } + + txn.ComputeHash() + + return txn, nil +} diff --git a/jsonrpc/helper_test.go b/jsonrpc/helper_test.go new file mode 100644 index 0000000000..08c964031c --- /dev/null +++ b/jsonrpc/helper_test.go @@ -0,0 +1,796 @@ +package jsonrpc + +import ( + "errors" + "fmt" + "math/big" + "testing" + + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/assert" +) + +func createTestTransaction(hash types.Hash) *types.Transaction { + return &types.Transaction{ + Hash: hash, + } +} + +func createTestHeader(height uint64) *types.Header { + h := &types.Header{ + Number: height, + } + + h.ComputeHash() + + return h +} + +func wrapHeaderWithTestBlock(h *types.Header) *types.Block { + return &types.Block{ + Header: h, + } +} + +var ( + testTxHash1 = types.BytesToHash([]byte{1}) + testTx1 = createTestTransaction(testTxHash1) + + testGenesisHeader = createTestHeader(0) + testGenesisBlock = wrapHeaderWithTestBlock(testGenesisHeader) + + testLatestHeader = createTestHeader(100) + testLatestBlock = wrapHeaderWithTestBlock(testLatestHeader) + + testHeader10 = createTestHeader(10) + testBlock10 = wrapHeaderWithTestBlock(testHeader10) + + testHash11 = types.BytesToHash([]byte{11}) + + testTraceResult = map[string]interface{}{ + "failed": false, + "gas": 1000, + "returnValue": "test return", + "structLogs": []interface{}{ + "log1", + "log2", + }, + } + testTraceResults = []interface{}{ + testTraceResult, + } +) + +func TestGetNumericBlockNumber(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + num BlockNumber + store latestHeaderGetter + expected uint64 + err error + }{ + { + name: "should return the latest block's number if latest is given", + num: LatestBlockNumber, + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return &types.Header{ + Number: 10, + } + }, + }, + expected: 10, + err: nil, + }, + { + name: "should return the latest block's number if latest is given", + num: LatestBlockNumber, + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return nil + }, + }, + expected: 0, + err: ErrLatestNotFound, + }, + { + name: "should return 0 number if earliest is given", + num: EarliestBlockNumber, + store: &debugEndpointMockStore{}, + expected: 0, + err: nil, + }, + { + name: "should return error if pending is given", + num: PendingBlockNumber, + store: &debugEndpointMockStore{}, + expected: 0, + err: ErrPendingBlockNumber, + }, + { + name: "should return error if negative number is given", + num: -5, + store: &debugEndpointMockStore{}, + expected: 0, + err: ErrNegativeBlockNumber, + }, + { + name: "should return the given block number otherwise", + num: 5, + store: &debugEndpointMockStore{}, + expected: 5, + err: nil, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + res, err := GetNumericBlockNumber(test.num, test.store) + + assert.Equal(t, test.expected, res) + assert.Equal(t, test.err, err) + }) + } +} + +func TestGetBlockHeader(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + num BlockNumber + store headerGetter + expected *types.Header + err error + }{ + { + name: "should return the latest block's number if latest is given", + num: LatestBlockNumber, + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestHeader + }, + }, + expected: testLatestHeader, + err: nil, + }, + { + name: "should return genesis block if Earliest is given", + num: EarliestBlockNumber, + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Zero(t, num) + + return testGenesisHeader, true + }, + }, + expected: testGenesisHeader, + err: nil, + }, + { + name: "should return error if genesis header not found", + num: EarliestBlockNumber, + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Zero(t, num) + + return nil, false + }, + }, + expected: nil, + err: ErrFailedFetchGenesis, + }, + { + name: "should return error if pending is given", + num: PendingBlockNumber, + store: &debugEndpointMockStore{}, + expected: nil, + err: ErrPendingBlockNumber, + }, + { + name: "should return header at arbitrary height", + num: 10, + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, uint64(10), num) + + return testHeader10, true + }, + }, + expected: testHeader10, + err: nil, + }, + { + name: "should return error if header not found", + num: 11, + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, uint64(11), num) + + return nil, false + }, + }, + expected: nil, + err: fmt.Errorf("error fetching block number %d header", 11), + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + res, err := GetBlockHeader(test.num, test.store) + + assert.Equal(t, test.expected, res) + assert.Equal(t, test.err, err) + }) + } +} + +func TestGetTxAndBlockByTxHash(t *testing.T) { + t.Parallel() + + blockWithTx := &types.Block{ + Header: testBlock10.Header, + Transactions: []*types.Transaction{ + testTx1, + }, + } + + tests := []struct { + name string + txHash types.Hash + store txLookupAndBlockGetter + tx *types.Transaction + block *types.Block + }{ + { + name: "should return tx and block", + txHash: testTx1.Hash, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTx1.Hash, hash) + + return blockWithTx.Hash(), true + }, + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, blockWithTx.Hash(), hash) + assert.True(t, full) + + return blockWithTx, true + }, + }, + tx: testTx1, + block: blockWithTx, + }, + { + name: "should return nil if ReadTxLookup returns nothing", + txHash: testTx1.Hash, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTx1.Hash, hash) + + return types.ZeroHash, false + }, + }, + tx: nil, + block: nil, + }, + { + name: "should return nil if GetBlockByHash returns nothing", + txHash: testTx1.Hash, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTx1.Hash, hash) + + return blockWithTx.Hash(), true + }, + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, blockWithTx.Hash(), hash) + assert.True(t, full) + + return nil, false + }, + }, + tx: nil, + block: nil, + }, + { + name: "should return nil if the block doesn't include the tx", + txHash: testTx1.Hash, + store: &debugEndpointMockStore{ + readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { + assert.Equal(t, testTx1.Hash, hash) + + return blockWithTx.Hash(), true + }, + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, blockWithTx.Hash(), hash) + assert.True(t, full) + + return testBlock10, true + }, + }, + tx: nil, + block: nil, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + tx, block := GetTxAndBlockByTxHash(test.txHash, test.store) + + assert.Equal(t, test.tx, tx) + assert.Equal(t, test.block, block) + }) + } +} + +func TestGetHeaderFromBlockNumberOrHash(t *testing.T) { + t.Parallel() + + block10Num := BlockNumber(testBlock10.Number()) + + tests := []struct { + name string + bnh BlockNumberOrHash + store *debugEndpointMockStore + header *types.Header + err bool + }{ + { + name: "should return latest header if neither block number nor hash is given", + bnh: BlockNumberOrHash{}, + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestHeader + }, + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testLatestHeader.Hash, hash) + assert.False(t, full) + + return testLatestBlock, true + }, + }, + header: testLatestHeader, + err: false, + }, + { + name: "should return header by number if both block number and hash are given", + bnh: BlockNumberOrHash{ + BlockNumber: &block10Num, + BlockHash: &testLatestBlock.Header.Hash, + }, + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, testHeader10.Number, num) + + return testHeader10, true + }, + }, + header: testHeader10, + err: false, + }, + { + name: "should return header by hash if both hash are given", + bnh: BlockNumberOrHash{ + BlockHash: &testHeader10.Hash, + }, + store: &debugEndpointMockStore{ + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testHeader10.Hash, hash) + assert.False(t, full) + + return testBlock10, true + }, + }, + header: testHeader10, + err: false, + }, + { + name: "should return error if header not found when block number is given", + bnh: BlockNumberOrHash{ + BlockNumber: &block10Num, + }, + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, testHeader10.Number, num) + + return nil, false + }, + }, + header: nil, + err: true, + }, + { + name: "should return header by hash if both hash are given", + bnh: BlockNumberOrHash{ + BlockHash: &testHeader10.Hash, + }, + store: &debugEndpointMockStore{ + getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { + assert.Equal(t, testHeader10.Hash, hash) + assert.False(t, full) + + return nil, false + }, + }, + header: nil, + err: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + header, err := GetHeaderFromBlockNumberOrHash(test.bnh, test.store) + + assert.Equal(t, test.header, header) + + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetNextNonce(t *testing.T) { + t.Parallel() + + var ( + testAddr = types.StringToAddress("1") + stateRoot = types.StringToHash("2") + blockNum = uint64(10) + nonce = uint64(16) + + testErr = errors.New("test") + ) + + tests := []struct { + name string + address types.Address + num BlockNumber + store nonceGetter + expected uint64 + err bool + }{ + { + name: "should return the nonce from txpool if pending num is given", + address: testAddr, + num: PendingBlockNumber, + store: &debugEndpointMockStore{ + getNonceFn: func(a types.Address) uint64 { + assert.Equal(t, testAddr, a) + + return nonce + }, + }, + expected: nonce, + err: false, + }, + { + name: "should return state nonce if block number is given", + address: testAddr, + num: BlockNumber(blockNum), + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, blockNum, num) + + return &types.Header{ + StateRoot: stateRoot, + }, true + }, + getAccountFn: func(hash types.Hash, addr types.Address) (*Account, error) { + assert.Equal(t, stateRoot, hash) + assert.Equal(t, testAddr, addr) + + return &Account{ + Nonce: nonce, + }, nil + }, + }, + expected: nonce, + err: false, + }, + { + name: "should return 0 if account state not found", + address: testAddr, + num: BlockNumber(blockNum), + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, blockNum, num) + + return &types.Header{ + StateRoot: stateRoot, + }, true + }, + getAccountFn: func(hash types.Hash, addr types.Address) (*Account, error) { + assert.Equal(t, stateRoot, hash) + assert.Equal(t, testAddr, addr) + + return nil, ErrStateNotFound + }, + }, + expected: 0, + err: false, + }, + { + name: "should return error if block header not found", + address: testAddr, + num: BlockNumber(blockNum), + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, blockNum, num) + + return nil, false + }, + }, + expected: 0, + err: true, + }, + { + name: "should return error if getting state fails", + address: testAddr, + num: BlockNumber(blockNum), + store: &debugEndpointMockStore{ + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + assert.Equal(t, blockNum, num) + + return &types.Header{ + StateRoot: stateRoot, + }, true + }, + getAccountFn: func(hash types.Hash, addr types.Address) (*Account, error) { + assert.Equal(t, stateRoot, hash) + assert.Equal(t, testAddr, addr) + + return nil, testErr + }, + }, + expected: 0, + err: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + nonce, err := GetNextNonce(test.address, test.num, test.store) + + assert.Equal(t, test.expected, nonce) + + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestDecodeTxn(t *testing.T) { + t.Parallel() + + var ( + from = types.StringToAddress("1") + to = types.StringToAddress("2") + gas = argUint64(uint64(1)) + gasPrice = argBytes(new(big.Int).SetUint64(2).Bytes()) + value = argBytes(new(big.Int).SetUint64(4).Bytes()) + data = argBytes(new(big.Int).SetUint64(8).Bytes()) + input = argBytes(new(big.Int).SetUint64(16).Bytes()) + nonce = argUint64(uint64(32)) + stateNonce = argUint64(uint64(64)) + + testError = errors.New("test error") + ) + + tests := []struct { + name string + arg *txnArgs + store nonceGetter + expected *types.Transaction + err bool + }{ + { + name: "should return mapped transaction", + arg: &txnArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: &gasPrice, + Value: &value, + Input: &input, + Nonce: &nonce, + }, + store: &debugEndpointMockStore{}, + expected: &types.Transaction{ + From: from, + To: &to, + Gas: uint64(gas), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + Value: new(big.Int).SetBytes([]byte(value)), + Input: input, + Nonce: uint64(nonce), + }, + err: false, + }, + { + name: "should set zero address to from and 0 to nonce if from is not given", + arg: &txnArgs{ + To: &to, + Gas: &gas, + GasPrice: &gasPrice, + Value: &value, + Input: &input, + Nonce: &nonce, + }, + store: &debugEndpointMockStore{}, + expected: &types.Transaction{ + From: types.ZeroAddress, + To: &to, + Gas: uint64(gas), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + Value: new(big.Int).SetBytes([]byte(value)), + Input: input, + Nonce: uint64(0), + }, + err: false, + }, + { + name: "should get from store if nonce is not given", + arg: &txnArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: &gasPrice, + Value: &value, + Input: &input, + }, + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestHeader + }, + getAccountFn: func(hash types.Hash, addr types.Address) (*Account, error) { + assert.Equal(t, from, addr) + + return &Account{ + Nonce: uint64(stateNonce), + }, nil + }, + }, + expected: &types.Transaction{ + From: from, + To: &to, + Gas: uint64(gas), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + Value: new(big.Int).SetBytes([]byte(value)), + Input: input, + Nonce: uint64(stateNonce), + }, + err: false, + }, + { + name: "should give priority to data than input if both are given", + arg: &txnArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: &gasPrice, + Value: &value, + Data: &data, + Input: &input, + Nonce: &nonce, + }, + store: &debugEndpointMockStore{}, + expected: &types.Transaction{ + From: from, + To: &to, + Gas: uint64(gas), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + Value: new(big.Int).SetBytes([]byte(value)), + Input: data, + Nonce: uint64(nonce), + }, + err: false, + }, + { + name: "should set zero to value, gas price, input, and gas as default", + arg: &txnArgs{ + From: &from, + To: &to, + Nonce: &nonce, + }, + store: &debugEndpointMockStore{}, + expected: &types.Transaction{ + From: from, + To: &to, + Gas: uint64(0), + GasPrice: new(big.Int), + Value: new(big.Int), + Input: []byte{}, + Nonce: uint64(nonce), + }, + err: false, + }, + { + name: "should return error if nonce fetch fails", + arg: &txnArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: &gasPrice, + Value: &value, + Input: &input, + }, + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestHeader + }, + getAccountFn: func(hash types.Hash, addr types.Address) (*Account, error) { + assert.Equal(t, from, addr) + + return nil, testError + }, + }, + expected: nil, + err: true, + }, + { + name: "should return error both to and input are not given", + arg: &txnArgs{ + From: &from, + Gas: &gas, + GasPrice: &gasPrice, + Value: &value, + Nonce: &nonce, + }, + store: &debugEndpointMockStore{}, + expected: nil, + err: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + tx, err := DecodeTxn(test.arg, test.store) + + // DecodeTxn computes hash of tx + if !test.err { + test.expected.ComputeHash() + } + + assert.Equal(t, test.expected, tx) + + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index 25a8b13738..05206be16f 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -55,6 +55,7 @@ type JSONRPCStore interface { networkStore txPoolStore filterManagerStore + debugStore } type Config struct { diff --git a/jsonrpc/mocks_test.go b/jsonrpc/mocks_test.go index c4df72857c..3fa7b97446 100644 --- a/jsonrpc/mocks_test.go +++ b/jsonrpc/mocks_test.go @@ -51,7 +51,7 @@ type mockStore struct { accounts map[types.Address]*Account // headers is the list of historical headers - headers []*types.Header + historicalHeaders []*types.Header } func newMockStore() *mockStore { @@ -66,15 +66,15 @@ func newMockStore() *mockStore { } func (m *mockStore) addHeader(header *types.Header) { - if m.headers == nil { - m.headers = []*types.Header{} + if m.historicalHeaders == nil { + m.historicalHeaders = []*types.Header{} } - m.headers = append(m.headers, header) + m.historicalHeaders = append(m.historicalHeaders, header) } func (m *mockStore) headerLoop(cond func(h *types.Header) bool) *types.Header { - for _, header := range m.headers { + for _, header := range m.historicalHeaders { if cond(header) { return header } @@ -137,6 +137,14 @@ func (m *mockStore) SubscribeEvents() blockchain.Subscription { return m.subscription } +func (m *mockStore) GetHeaderByNumber(num uint64) (*types.Header, bool) { + header := m.headerLoop(func(header *types.Header) bool { + return header.Number == num + }) + + return header, header != nil +} + func (m *mockStore) GetBlockByHash(hash types.Hash, full bool) (*types.Block, bool) { header := m.headerLoop(func(header *types.Header) bool { return header.Hash == hash diff --git a/server/server.go b/server/server.go index 40857ce27c..dccc0ac92d 100644 --- a/server/server.go +++ b/server/server.go @@ -26,6 +26,7 @@ import ( "github.com/0xPolygon/polygon-edge/state" itrie "github.com/0xPolygon/polygon-edge/state/immutable-trie" "github.com/0xPolygon/polygon-edge/state/runtime" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/txpool" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" @@ -508,7 +509,6 @@ func (j *jsonRPCHub) ApplyTxn( } transition, err := j.BeginTxn(header.StateRoot, header, blockCreator) - if err != nil { return } @@ -518,6 +518,126 @@ func (j *jsonRPCHub) ApplyTxn( return } +// TraceBlock traces all transactions in the given block and returns all results +func (j *jsonRPCHub) TraceBlock( + block *types.Block, + tracer tracer.Tracer, +) ([]interface{}, error) { + if block.Number() == 0 { + return nil, errors.New("genesis block can't have transaction") + } + + parentHeader, ok := j.GetHeaderByHash(block.ParentHash()) + if !ok { + return nil, errors.New("parent header not found") + } + + blockCreator, err := j.GetConsensus().GetBlockCreator(block.Header) + if err != nil { + return nil, err + } + + transition, err := j.BeginTxn(parentHeader.StateRoot, block.Header, blockCreator) + if err != nil { + return nil, err + } + + transition.SetTracer(tracer) + + results := make([]interface{}, len(block.Transactions)) + + for idx, tx := range block.Transactions { + tracer.Clear() + + if _, err := transition.Apply(tx); err != nil { + return nil, err + } + + if results[idx], err = tracer.GetResult(); err != nil { + return nil, err + } + } + + return results, nil +} + +// TraceTxn traces a transaction in the block, associated with the given hash +func (j *jsonRPCHub) TraceTxn( + block *types.Block, + targetTxHash types.Hash, + tracer tracer.Tracer, +) (interface{}, error) { + if block.Number() == 0 { + return nil, errors.New("genesis block can't have transaction") + } + + parentHeader, ok := j.GetHeaderByHash(block.ParentHash()) + if !ok { + return nil, errors.New("parent header not found") + } + + blockCreator, err := j.GetConsensus().GetBlockCreator(block.Header) + if err != nil { + return nil, err + } + + transition, err := j.BeginTxn(parentHeader.StateRoot, block.Header, blockCreator) + if err != nil { + return nil, err + } + + var targetTx *types.Transaction + + for _, tx := range block.Transactions { + if tx.Hash == targetTxHash { + targetTx = tx + + break + } + + // Execute transactions without tracer until reaching the target transaction + if _, err := transition.Apply(tx); err != nil { + return nil, err + } + } + + if targetTx == nil { + return nil, errors.New("target tx not found") + } + + transition.SetTracer(tracer) + + if _, err := transition.Apply(targetTx); err != nil { + return nil, err + } + + return tracer.GetResult() +} + +func (j *jsonRPCHub) TraceCall( + tx *types.Transaction, + parentHeader *types.Header, + tracer tracer.Tracer, +) (interface{}, error) { + blockCreator, err := j.GetConsensus().GetBlockCreator(parentHeader) + if err != nil { + return nil, err + } + + transition, err := j.BeginTxn(parentHeader.StateRoot, parentHeader, blockCreator) + if err != nil { + return nil, err + } + + transition.SetTracer(tracer) + + if _, err := transition.Apply(tx); err != nil { + return nil, err + } + + return tracer.GetResult() +} + func (j *jsonRPCHub) GetSyncProgression() *progress.Progression { // restore progression if restoreProg := j.restoreProgression.GetProgression(); restoreProg != nil { diff --git a/state/executor.go b/state/executor.go index 28a937e44c..a34e259546 100644 --- a/state/executor.go +++ b/state/executor.go @@ -6,14 +6,14 @@ import ( "math" "math/big" - "github.com/hashicorp/go-hclog" - "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/state/runtime/evm" "github.com/0xPolygon/polygon-edge/state/runtime/precompiled" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" + "github.com/hashicorp/go-hclog" ) const ( @@ -131,7 +131,7 @@ func (e *Executor) BeginTxn( header *types.Header, coinbaseReceiver types.Address, ) (*Transition, error) { - config := e.config.Forks.At(header.Number) + forkConfig := e.config.Forks.At(header.Number) auxSnap2, err := e.state.NewSnapshotAt(parentRoot) if err != nil { @@ -140,7 +140,7 @@ func (e *Executor) BeginTxn( newTxn := NewTxn(auxSnap2) - env2 := runtime.TxContext{ + txCtx := runtime.TxContext{ Coinbase: coinbaseReceiver, Timestamp: int64(header.Timestamp), Number: int64(header.Number), @@ -151,13 +151,13 @@ func (e *Executor) BeginTxn( txn := &Transition{ logger: e.logger, - ctx: env2, + ctx: txCtx, state: newTxn, snap: auxSnap2, getHash: e.GetHash(header), auxState: e.state, - config: config, - gasPool: uint64(env2.GasLimit), + config: forkConfig, + gasPool: uint64(txCtx.GasLimit), receipts: []*types.Receipt{}, totalGas: 0, @@ -328,8 +328,8 @@ func (t *Transition) Txn() *Txn { // Apply applies a new transaction func (t *Transition) Apply(msg *types.Transaction) (*runtime.ExecutionResult, error) { s := t.state.Snapshot() - result, err := t.apply(msg) + result, err := t.apply(msg) if err != nil { t.state.RevertToSnapshot(s) } @@ -438,6 +438,10 @@ func (t *Transition) apply(msg *types.Transaction) (*runtime.ExecutionResult, er return nil, NewGasLimitReachedTransitionApplicationError(err) } + if t.ctx.Tracer != nil { + t.ctx.Tracer.TxStart(msg.Gas) + } + // 4. there is no overflow when calculating intrinsic gas intrinsicGasCost, err := TransactionGasCost(msg, t.config.Homestead, t.config.Istanbul) if err != nil { @@ -474,6 +478,10 @@ func (t *Transition) apply(msg *types.Transaction) (*runtime.ExecutionResult, er refund := txn.GetRefund() result.UpdateGasUsed(msg.Gas, refund) + if t.ctx.Tracer != nil { + t.ctx.Tracer.TxEnd(result.GasLeft) + } + // refund the sender remaining := new(big.Int).Mul(new(big.Int).SetUint64(result.GasLeft), gasPrice) txn.AddBalance(msg.From, remaining) @@ -570,11 +578,17 @@ func (t *Transition) applyCall( } } - result := t.run(c, host) + var result *runtime.ExecutionResult + + t.captureCallStart(c, callType) + + result = t.run(c, host) if result.Failed() { t.state.RevertToSnapshot(snapshot) } + t.captureCallEnd(c, result) + return result } @@ -633,8 +647,16 @@ func (t *Transition) applyCreate(c *runtime.Contract, host runtime.Host) *runtim } } - result := t.run(c, host) + var result *runtime.ExecutionResult + + t.captureCallStart(c, evm.CREATE) + defer func() { + // pass result to be set later + t.captureCallEnd(c, result) + }() + + result = t.run(c, host) if result.Failed() { t.state.RevertToSnapshot(snapshot) @@ -774,6 +796,20 @@ func (t *Transition) SetCodeDirectly(addr types.Address, code []byte) error { return nil } +// SetTracer sets tracer to the context in order to enable it +func (t *Transition) SetTracer(tracer tracer.Tracer) { + t.ctx.Tracer = tracer +} + +// GetTracer returns a tracer in context +func (t *Transition) GetTracer() runtime.VMTracer { + return t.ctx.Tracer +} + +func (t *Transition) GetRefund() uint64 { + return t.state.GetRefund() +} + func TransactionGasCost(msg *types.Transaction, isHomestead, isIstanbul bool) (uint64, error) { cost := uint64(0) @@ -816,3 +852,33 @@ func TransactionGasCost(msg *types.Transaction, isHomestead, isIstanbul bool) (u return cost, nil } + +// captureCallStart calls CallStart in Tracer if context has the tracer +func (t *Transition) captureCallStart(c *runtime.Contract, callType runtime.CallType) { + if t.ctx.Tracer == nil { + return + } + + t.ctx.Tracer.CallStart( + c.Depth, + c.Caller, + c.Address, + int(callType), + c.Gas, + c.Value, + c.Input, + ) +} + +// captureCallEnd calls CallEnd in Tracer if context has the tracer +func (t *Transition) captureCallEnd(c *runtime.Contract, result *runtime.ExecutionResult) { + if t.ctx.Tracer == nil { + return + } + + t.ctx.Tracer.CallEnd( + c.Depth, + result.ReturnValue, + result.Err, + ) +} diff --git a/state/runtime/evm/evm.go b/state/runtime/evm/evm.go index 937c4aab1e..7563291da1 100644 --- a/state/runtime/evm/evm.go +++ b/state/runtime/evm/evm.go @@ -59,6 +59,7 @@ func (e *EVM) Run(c *runtime.Contract, host runtime.Host, config *chain.ForksInT return &runtime.ExecutionResult{ ReturnValue: returnValue, GasLeft: gasLeft, + GasUsed: c.Gas - gasLeft, Err: err, } } diff --git a/state/runtime/evm/evm_test.go b/state/runtime/evm/evm_test.go index 71aaaba33b..c86628cfe6 100644 --- a/state/runtime/evm/evm_test.go +++ b/state/runtime/evm/evm_test.go @@ -6,6 +6,7 @@ import ( "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/state/runtime" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" "github.com/stretchr/testify/assert" ) @@ -24,7 +25,9 @@ func newMockContract(value *big.Int, gas uint64, code []byte) *runtime.Contract // mockHost is a struct which meets the requirements of runtime.Host interface but throws panic in each methods // we don't test all opcodes in this test -type mockHost struct{} +type mockHost struct { + tracer runtime.VMTracer +} func (m *mockHost) AccountExists(addr types.Address) bool { panic("Not implemented in tests") @@ -87,6 +90,14 @@ func (m *mockHost) GetNonce(addr types.Address) uint64 { panic("Not implemented in tests") } +func (m *mockHost) GetTracer() runtime.VMTracer { + return m.tracer +} + +func (m *mockHost) GetRefund() uint64 { + panic("Not implemented in tests") +} + func TestRun(t *testing.T) { t.Parallel() @@ -120,6 +131,7 @@ func TestRun(t *testing.T) { expected: &runtime.ExecutionResult{ ReturnValue: []uint8{0x03}, GasLeft: 4976, + GasUsed: 24, }, }, { @@ -131,6 +143,7 @@ func TestRun(t *testing.T) { expected: &runtime.ExecutionResult{ ReturnValue: nil, GasLeft: 0, + GasUsed: 5000, Err: errStackUnderflow, }, }, @@ -145,6 +158,7 @@ func TestRun(t *testing.T) { }, expected: &runtime.ExecutionResult{ ReturnValue: nil, + GasUsed: 6, // gas consumed for 2 push1 ops GasLeft: 4994, Err: errRevert, @@ -169,3 +183,190 @@ func TestRun(t *testing.T) { }) } } + +type mockCall struct { + name string + args map[string]interface{} +} + +type mockTracer struct { + calls []mockCall +} + +func (m *mockTracer) CaptureState( + memory []byte, + stack []*big.Int, + opCode int, + contractAddress types.Address, + sp int, + _host tracer.RuntimeHost, + _state tracer.VMState, +) { + m.calls = append(m.calls, mockCall{ + name: "CaptureState", + args: map[string]interface{}{ + "memory": memory, + "stack": stack, + "opCode": opCode, + "contractAddress": contractAddress, + "sp": sp, + }, + }) +} + +func (m *mockTracer) ExecuteState( + contractAddress types.Address, + ip uint64, + opcode string, + availableGas uint64, + cost uint64, + lastReturnData []byte, + depth int, + err error, + _host tracer.RuntimeHost, +) { + m.calls = append(m.calls, mockCall{ + name: "ExecuteState", + args: map[string]interface{}{ + "contractAddress": contractAddress, + "ip": ip, + "opcode": opcode, + "availableGas": availableGas, + "cost": cost, + "lastReturnData": lastReturnData, + "depth": depth, + "err": err, + }, + }) +} + +func TestRunWithTracer(t *testing.T) { + t.Parallel() + + contractAddress := types.StringToAddress("1") + + tests := []struct { + name string + value *big.Int + gas uint64 + code []byte + config *chain.ForksInTime + expected []mockCall + }{ + { + name: "should call CaptureState and ExecuteState", + value: big.NewInt(0), + gas: 5000, + code: []byte{ + PUSH1, + 0x1, + }, + expected: []mockCall{ + { + name: "CaptureState", + args: map[string]interface{}{ + "memory": []byte{}, + "stack": []*big.Int{}, + "opCode": int(PUSH1), + "contractAddress": contractAddress, + "sp": 0, + }, + }, + { + name: "ExecuteState", + args: map[string]interface{}{ + "contractAddress": contractAddress, + "ip": uint64(0), + "opcode": opCodeToString[PUSH1], + "availableGas": uint64(5000), + "cost": uint64(3), + "lastReturnData": []byte{}, + "depth": 1, + "err": (error)(nil), + }, + }, + { + name: "CaptureState", + args: map[string]interface{}{ + "memory": []byte{}, + "stack": []*big.Int{ + big.NewInt(1), + }, + "opCode": int(0), + "contractAddress": contractAddress, + "sp": 1, + }, + }, + }, + }, + { + name: "should exit with error", + value: big.NewInt(0), + gas: 5000, + code: []byte{ + POP, + }, + expected: []mockCall{ + { + name: "CaptureState", + args: map[string]interface{}{ + "memory": []byte{}, + "stack": []*big.Int{}, + "opCode": int(POP), + "contractAddress": contractAddress, + "sp": 0, + }, + }, + { + name: "ExecuteState", + args: map[string]interface{}{ + "contractAddress": contractAddress, + "ip": uint64(0), + "opcode": opCodeToString[POP], + "availableGas": uint64(5000), + "cost": uint64(0), + "lastReturnData": []byte{}, + "depth": 1, + "err": errStackUnderflow, + }, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + contract := newMockContract(tt.value, tt.gas, tt.code) + contract.Address = contractAddress + tracer := &mockTracer{} + host := &mockHost{ + tracer: tracer, + } + config := tt.config + if config == nil { + config = &chain.ForksInTime{} + } + + state := acquireState() + state.resetReturnData() + state.msg = contract + state.code = contract.Code + state.gas = contract.Gas + state.host = host + state.config = config + + // make sure stack, memory, and returnData are empty + state.stack = make([]*big.Int, 0) + state.memory = make([]byte, 0) + state.returnData = make([]byte, 0) + + _, _ = state.Run() + + assert.Equal(t, tt.expected, tracer.calls) + }) + } +} diff --git a/state/runtime/evm/instructions.go b/state/runtime/evm/instructions.go index 6d403c32ee..f81383a7e1 100644 --- a/state/runtime/evm/instructions.go +++ b/state/runtime/evm/instructions.go @@ -936,7 +936,7 @@ func opSelfDestruct(c *state) { } c.host.Selfdestruct(c.msg.Address, address) - c.halt() + c.Halt() } func opJump(c *state) { @@ -1044,7 +1044,7 @@ func opLog(size int) instruction { } func opStop(c *state) { - c.halt() + c.Halt() } func opCreate(op OpCode) instruction { @@ -1396,7 +1396,7 @@ func opHalt(op OpCode) instruction { if op == REVERT { c.exit(errRevert) } else { - c.halt() + c.Halt() } } } diff --git a/state/runtime/evm/instructions_test.go b/state/runtime/evm/instructions_test.go index 0573744eec..945eea9e55 100644 --- a/state/runtime/evm/instructions_test.go +++ b/state/runtime/evm/instructions_test.go @@ -496,13 +496,14 @@ func Test_opReturnDataCopy(t *testing.T) { big.NewInt(0), big.NewInt(0), }, - sp: 0, - returnData: []byte{0xff}, - memory: []byte{0xff}, - gas: 7, - lastGasCost: 0, - stop: false, - err: nil, + sp: 0, + returnData: []byte{0xff}, + memory: []byte{0xff}, + gas: 7, + lastGasCost: 0, + currentConsumedGas: 3, + stop: false, + err: nil, }, }, { @@ -533,10 +534,11 @@ func Test_opReturnDataCopy(t *testing.T) { []byte{0x11, 0x22, 0x02, 0x03, 0x04, 0x05, 0x06}, make([]byte, 25)..., ), - gas: 14, - lastGasCost: 3, - stop: false, - err: nil, + gas: 14, + lastGasCost: 3, + currentConsumedGas: 6, + stop: false, + err: nil, }, }, } @@ -564,6 +566,7 @@ func Test_opReturnDataCopy(t *testing.T) { state.evm = nil state.bitmap = bitmap{} state.ret = nil + state.currentConsumedGas = 0 opReturnDataCopy(state) diff --git a/state/runtime/evm/state.go b/state/runtime/evm/state.go index 3ddc347c04..340889e332 100644 --- a/state/runtime/evm/state.go +++ b/state/runtime/evm/state.go @@ -72,7 +72,8 @@ type state struct { err error stop bool - gas uint64 + gas uint64 + currentConsumedGas uint64 // bitvec bitvec bitmap bitmap @@ -85,6 +86,7 @@ func (c *state) reset() { c.sp = 0 c.ip = 0 c.gas = 0 + c.currentConsumedGas = 0 c.lastGasCost = 0 c.stop = false c.err = nil @@ -97,6 +99,7 @@ func (c *state) reset() { c.memory[i] = 0 } + c.stack = c.stack[:0] c.tmp = c.tmp[:0] c.ret = c.ret[:0] c.code = c.code[:0] @@ -113,7 +116,7 @@ func (c *state) validJumpdest(dest *big.Int) bool { return c.bitmap.isSet(uint(udest)) } -func (c *state) halt() { +func (c *state) Halt() { c.stop = true } @@ -193,6 +196,8 @@ func (c *state) swap(n int) { } func (c *state) consumeGas(gas uint64) bool { + c.currentConsumedGas += gas + if c.gas < gas { c.exit(errOutOfGas) @@ -210,37 +215,51 @@ func (c *state) resetReturnData() { // Run executes the virtual machine func (c *state) Run() ([]byte, error) { - var vmerr error + var ( + vmerr error + + op OpCode + ok bool + ) - codeSize := len(c.code) for !c.stop { - if c.ip >= codeSize { - c.halt() + op, ok = c.CurrentOpCode() + gasCopy := c.gas + + c.captureState(int(op)) + + if !ok { + c.Halt() break } - op := OpCode(c.code[c.ip]) - inst := dispatchTable[op] if inst.inst == nil { c.exit(errOpCodeNotFound) + c.captureExecutionError(op.String(), c.ip, gasCopy) break } + // check if the depth of the stack is enough for the instruction if c.sp < inst.stack { c.exit(errStackUnderflow) + c.captureExecutionError(op.String(), c.ip, gasCopy) break } + // consume the gas of the instruction if !c.consumeGas(inst.gas) { c.exit(errOutOfGas) + c.captureExecutionError(op.String(), c.ip, gasCopy) break } + c.captureSuccessfulExecution(op.String(), gasCopy) + // execute the instruction inst.inst(c) @@ -250,6 +269,7 @@ func (c *state) Run() ([]byte, error) { break } + c.ip++ } @@ -354,3 +374,75 @@ func (c *state) Show() string { return strings.Join(str, "\n") } + +func (c *state) CurrentOpCode() (OpCode, bool) { + if codeSize := len(c.code); c.ip >= codeSize { + return STOP, false + } + + return OpCode(c.code[c.ip]), true +} + +func (c *state) captureState(opCode int) { + tracer := c.host.GetTracer() + if tracer == nil { + return + } + + tracer.CaptureState( + c.memory, + c.stack, + opCode, + c.msg.Address, + c.sp, + c.host, + c, + ) +} + +func (c *state) captureSuccessfulExecution( + opCode string, + gas uint64, +) { + tracer := c.host.GetTracer() + + if tracer == nil { + return + } + + tracer.ExecuteState( + c.msg.Address, + uint64(c.ip), + opCode, + gas, + c.currentConsumedGas, + c.returnData, + c.msg.Depth, + c.err, + c.host, + ) +} + +func (c *state) captureExecutionError( + opCode string, + ip int, + gas uint64, +) { + tracer := c.host.GetTracer() + + if tracer == nil { + return + } + + tracer.ExecuteState( + c.msg.Address, + uint64(ip), + opCode, + gas, + c.currentConsumedGas, + c.returnData, + c.msg.Depth, + c.err, + c.host, + ) +} diff --git a/state/runtime/evm/state_test.go b/state/runtime/evm/state_test.go index 56698e16fc..5000ac1381 100644 --- a/state/runtime/evm/state_test.go +++ b/state/runtime/evm/state_test.go @@ -54,6 +54,7 @@ func TestStackOverflow(t *testing.T) { s.code = code.buf s.gas = 10000 + s.host = &mockHost{} _, err := s.Run() assert.NoError(t, err) @@ -64,6 +65,7 @@ func TestStackOverflow(t *testing.T) { s.reset() s.code = code.buf s.gas = 10000 + s.host = &mockHost{} _, err = s.Run() assert.Equal(t, errStackOverflow, err) @@ -84,6 +86,7 @@ func TestStackUnderflow(t *testing.T) { s.code = code.buf s.gas = 10000 + s.host = &mockHost{} _, err := s.Run() assert.NoError(t, err) @@ -93,6 +96,7 @@ func TestStackUnderflow(t *testing.T) { s.reset() s.code = code.buf s.gas = 10000 + s.host = &mockHost{} _, err = s.Run() assert.Equal(t, errStackUnderflow, err) @@ -104,6 +108,7 @@ func TestOpcodeNotFound(t *testing.T) { s.code = []byte{0xA5} s.gas = 1000 + s.host = &mockHost{} _, err := s.Run() assert.Equal(t, errOpCodeNotFound, err) diff --git a/state/runtime/runtime.go b/state/runtime/runtime.go index 0d87f072cc..d1df1d487c 100644 --- a/state/runtime/runtime.go +++ b/state/runtime/runtime.go @@ -5,6 +5,7 @@ import ( "math/big" "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" ) @@ -18,6 +19,7 @@ type TxContext struct { GasLimit int64 ChainID int64 Difficulty types.Hash + Tracer tracer.Tracer } // StorageStatus is the status of the storage access @@ -69,6 +71,31 @@ type Host interface { Callx(*Contract, Host) *ExecutionResult Empty(addr types.Address) bool GetNonce(addr types.Address) uint64 + GetTracer() VMTracer + GetRefund() uint64 +} + +type VMTracer interface { + CaptureState( + memory []byte, + stack []*big.Int, + opCode int, + contractAddress types.Address, + sp int, + host tracer.RuntimeHost, + state tracer.VMState, + ) + ExecuteState( + contractAddress types.Address, + ip uint64, + opcode string, + availableGas uint64, + cost uint64, + lastReturnData []byte, + depth int, + err error, + host tracer.RuntimeHost, + ) } // ExecutionResult includes all output after executing given evm diff --git a/state/runtime/tracer/structtracer/tracer.go b/state/runtime/tracer/structtracer/tracer.go new file mode 100644 index 0000000000..e3ac3bc09e --- /dev/null +++ b/state/runtime/tracer/structtracer/tracer.go @@ -0,0 +1,378 @@ +package structtracer + +import ( + "errors" + "fmt" + "math/big" + "sync" + + "github.com/0xPolygon/polygon-edge/helper/hex" + "github.com/0xPolygon/polygon-edge/state/runtime" + "github.com/0xPolygon/polygon-edge/state/runtime/evm" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer" + "github.com/0xPolygon/polygon-edge/types" +) + +type Config struct { + EnableMemory bool // enable memory capture + EnableStack bool // enable stack capture + EnableStorage bool // enable storage capture + EnableReturnData bool // enable return data capture +} + +type StructLog struct { + Pc uint64 `json:"pc"` + Op string `json:"op"` + Gas uint64 `json:"gas"` + GasCost uint64 `json:"gasCost"` + Memory []byte `json:"memory,omitempty"` + MemorySize int `json:"memSize"` + Stack []*big.Int `json:"stack"` + ReturnData []byte `json:"returnData,omitempty"` + Storage map[types.Hash]types.Hash `json:"storage"` + Depth int `json:"depth"` + RefundCounter uint64 `json:"refund"` + Err error `json:"err"` +} + +func (l *StructLog) ErrorString() string { + if l.Err != nil { + return l.Err.Error() + } + + return "" +} + +type StructTracer struct { + Config Config + + cancelLock sync.RWMutex + reason error + interrupt bool + + logs []StructLog + gasLimit uint64 + consumedGas uint64 + output []byte + err error + storage map[types.Address]map[types.Hash]types.Hash + + currentMemory []byte + currentStack []*big.Int +} + +func NewStructTracer(config Config) *StructTracer { + return &StructTracer{ + Config: config, + cancelLock: sync.RWMutex{}, + storage: make(map[types.Address]map[types.Hash]types.Hash), + } +} + +func (t *StructTracer) Cancel(err error) { + t.cancelLock.Lock() + defer t.cancelLock.Unlock() + + t.reason = err + t.interrupt = true +} + +func (t *StructTracer) cancelled() bool { + t.cancelLock.RLock() + defer t.cancelLock.RUnlock() + + return t.interrupt +} + +func (t *StructTracer) Clear() { + t.reason = nil + t.interrupt = false + t.logs = t.logs[:0] + t.gasLimit = 0 + t.consumedGas = 0 + t.output = t.output[:0] + t.err = nil + t.storage = make(map[types.Address]map[types.Hash]types.Hash) + t.currentMemory = t.currentMemory[:0] + t.currentStack = t.currentStack[:0] +} + +func (t *StructTracer) TxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +func (t *StructTracer) TxEnd(gasLeft uint64) { + t.consumedGas = t.gasLimit - gasLeft +} + +func (t *StructTracer) CallStart( + depth int, + from, to types.Address, + callType int, + gas uint64, + value *big.Int, + input []byte, +) { +} + +func (t *StructTracer) CallEnd( + depth int, + output []byte, + err error, +) { + if depth == 1 { + t.output = output + t.err = err + } +} + +func (t *StructTracer) CaptureState( + memory []byte, + stack []*big.Int, + opCode int, + contractAddress types.Address, + sp int, + host tracer.RuntimeHost, + state tracer.VMState, +) { + if t.cancelled() { + state.Halt() + + return + } + + t.captureMemory(memory) + + t.captureStack(stack) + + t.captureStorage( + stack, + opCode, + contractAddress, + sp, + host, + ) +} + +func (t *StructTracer) captureMemory( + memory []byte, +) { + if !t.Config.EnableMemory { + return + } + + // always allocate new space to get new reference + t.currentMemory = make([]byte, len(memory)) + + copy(t.currentMemory, memory) +} + +func (t *StructTracer) captureStack( + stack []*big.Int, +) { + if !t.Config.EnableStack { + return + } + + t.currentStack = make([]*big.Int, len(stack)) + + for i, v := range stack { + t.currentStack[i] = new(big.Int).Set(v) + } +} + +func (t *StructTracer) captureStorage( + stack []*big.Int, + opCode int, + contractAddress types.Address, + sp int, + host tracer.RuntimeHost, +) { + if !t.Config.EnableStorage || (opCode != evm.SLOAD && opCode != evm.SSTORE) { + return + } + + _, initialized := t.storage[contractAddress] + + switch opCode { + case evm.SLOAD: + if sp < 1 { + return + } + + if !initialized { + t.storage[contractAddress] = make(map[types.Hash]types.Hash) + } + + slot := types.BytesToHash(stack[sp-1].Bytes()) + value := host.GetStorage(contractAddress, slot) + + t.storage[contractAddress][slot] = value + + case evm.SSTORE: + if sp < 2 { + return + } + + if !initialized { + t.storage[contractAddress] = make(map[types.Hash]types.Hash) + } + + slot := types.BytesToHash(stack[sp-2].Bytes()) + value := types.BytesToHash(stack[sp-1].Bytes()) + + t.storage[contractAddress][slot] = value + } +} + +func (t *StructTracer) ExecuteState( + contractAddress types.Address, + ip uint64, + opCode string, + availableGas uint64, + cost uint64, + lastReturnData []byte, + depth int, + err error, + host tracer.RuntimeHost, +) { + var ( + memory []byte + memorySize int + stack []*big.Int + returnData []byte + storage map[types.Hash]types.Hash + ) + + if t.Config.EnableMemory { + memorySize = len(t.currentMemory) + + memory = make([]byte, memorySize) + copy(memory, t.currentMemory) + } + + if t.Config.EnableStack { + stack = make([]*big.Int, len(t.currentStack)) + + for i, v := range t.currentStack { + stack[i] = new(big.Int).Set(v) + } + } + + if t.Config.EnableReturnData { + returnData = make([]byte, len(lastReturnData)) + + copy(returnData, lastReturnData) + } + + if t.Config.EnableStorage { + contractStorage, ok := t.storage[contractAddress] + if ok { + storage = make(map[types.Hash]types.Hash, len(contractStorage)) + + for k, v := range contractStorage { + storage[k] = v + } + } + } + + t.logs = append( + t.logs, + StructLog{ + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Memory: memory, + MemorySize: memorySize, + Stack: stack, + ReturnData: returnData, + Storage: storage, + Depth: depth, + RefundCounter: host.GetRefund(), + Err: err, + }, + ) +} + +type StructTraceResult struct { + Failed bool `json:"failed"` + Gas uint64 `json:"gas"` + ReturnValue string `json:"returnValue"` + StructLogs []StructLogRes `json:"structLogs"` +} + +type StructLogRes struct { + Pc uint64 `json:"pc"` + Op string `json:"op"` + Gas uint64 `json:"gas"` + GasCost uint64 `json:"gasCost"` + Depth int `json:"depth"` + Error string `json:"error,omitempty"` + Stack []string `json:"stack"` + Memory []string `json:"memory"` + Storage map[string]string `json:"storage"` + RefundCounter uint64 `json:"refund,omitempty"` +} + +func (t *StructTracer) GetResult() (interface{}, error) { + if t.reason != nil { + return nil, t.reason + } + + var returnValue string + + if t.err != nil && !errors.Is(t.err, runtime.ErrExecutionReverted) { + returnValue = "" + } else { + returnValue = fmt.Sprintf("%x", t.output) + } + + return &StructTraceResult{ + Failed: t.err != nil, + Gas: t.consumedGas, + ReturnValue: returnValue, + StructLogs: formatStructLogs(t.logs), + }, nil +} + +func formatStructLogs(originalLogs []StructLog) []StructLogRes { + res := make([]StructLogRes, len(originalLogs)) + + for index, log := range originalLogs { + res[index] = StructLogRes{ + Pc: log.Pc, + Op: log.Op, + Gas: log.Gas, + GasCost: log.GasCost, + Depth: log.Depth, + Error: log.ErrorString(), + RefundCounter: log.RefundCounter, + } + + res[index].Stack = make([]string, len(log.Stack)) + + for i, value := range log.Stack { + res[index].Stack[i] = hex.EncodeBig(value) + } + + res[index].Memory = make([]string, 0, (len(log.Memory)+31)/32) + + if log.Memory != nil { + for i := 0; i+32 <= len(log.Memory); i += 32 { + res[index].Memory = append( + res[index].Memory, + hex.EncodeToString(log.Memory[i:i+32]), + ) + } + } + + res[index].Storage = make(map[string]string) + + for key, value := range log.Storage { + res[index].Storage[hex.EncodeToString(key.Bytes())] = hex.EncodeToString(value.Bytes()) + } + } + + return res +} diff --git a/state/runtime/tracer/structtracer/tracer_test.go b/state/runtime/tracer/structtracer/tracer_test.go new file mode 100644 index 0000000000..f2660faedb --- /dev/null +++ b/state/runtime/tracer/structtracer/tracer_test.go @@ -0,0 +1,995 @@ +package structtracer + +import ( + "errors" + "fmt" + "math/big" + "strings" + "testing" + + "github.com/0xPolygon/polygon-edge/helper/hex" + "github.com/0xPolygon/polygon-edge/state/runtime" + "github.com/0xPolygon/polygon-edge/state/runtime/evm" + "github.com/0xPolygon/polygon-edge/state/runtime/tracer" + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/assert" +) + +var ( + testFrom = types.StringToAddress("1") + testTo = types.StringToAddress("2") + + testEmptyConfig = Config{} +) + +type mockState struct { + halted bool +} + +func (m *mockState) Halt() { + m.halted = true +} + +type mockHost struct { + getRefundFn func() uint64 + getStorageFunc func(types.Address, types.Hash) types.Hash +} + +func (m *mockHost) GetRefund() uint64 { + return m.getRefundFn() +} + +func (m *mockHost) GetStorage(a types.Address, h types.Hash) types.Hash { + return m.getStorageFunc(a, h) +} + +func TestStructLogErrorString(t *testing.T) { + t.Parallel() + + errMsg := "error message" + + tests := []struct { + name string + log StructLog + expected string + }{ + { + name: "should return error message", + log: StructLog{ + Err: errors.New(errMsg), + }, + expected: errMsg, + }, + { + name: "should return empty string", + log: StructLog{ + Err: nil, + }, + expected: "", + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expected, test.log.ErrorString()) + }) + } +} + +func TestStructTracerCancel(t *testing.T) { + t.Parallel() + + err := errors.New("timeout") + + tracer := NewStructTracer(testEmptyConfig) + + assert.Nil(t, tracer.reason) + assert.False(t, tracer.interrupt) + + tracer.Cancel(err) + + assert.Equal(t, err, tracer.reason) + assert.True(t, tracer.interrupt) +} + +func TestStructTracer_canceled(t *testing.T) { + t.Parallel() + + err := errors.New("timeout") + + tracer := NewStructTracer(testEmptyConfig) + + assert.False(t, tracer.cancelled()) + + tracer.Cancel(err) + + assert.True(t, tracer.cancelled()) +} + +func TestStructTracerClear(t *testing.T) { + t.Parallel() + + tracer := StructTracer{ + Config: Config{ + EnableMemory: true, + EnableStack: true, + EnableStorage: true, + EnableReturnData: true, + }, + reason: errors.New("timeout"), + interrupt: true, + logs: []StructLog{ + { + Pc: 1, + }, + }, + gasLimit: 1024, + consumedGas: 512, + output: []byte("output example"), + err: runtime.ErrInsufficientBalance, + storage: map[types.Address]map[types.Hash]types.Hash{ + types.StringToAddress("1"): { + types.StringToHash("2"): types.StringToHash("3"), + }, + }, + currentMemory: []byte("memory example"), + currentStack: []*big.Int{ + new(big.Int).SetUint64(1), + new(big.Int).SetUint64(2), + }, + } + + tracer.Clear() + + assert.Equal( + t, + StructTracer{ + Config: Config{ + EnableMemory: true, + EnableStack: true, + EnableStorage: true, + EnableReturnData: true, + }, + reason: nil, + interrupt: false, + logs: []StructLog{}, + gasLimit: 0, + consumedGas: 0, + output: []byte{}, + err: nil, + storage: map[types.Address]map[types.Hash]types.Hash{}, + currentMemory: []byte{}, + currentStack: []*big.Int{}, + }, + tracer, + ) +} + +func TestStructTracerTxStart(t *testing.T) { + t.Parallel() + + var ( + gasLimit uint64 = 1024 + ) + + tracer := NewStructTracer(testEmptyConfig) + + tracer.TxStart(gasLimit) + + assert.Equal( + t, + &StructTracer{ + Config: testEmptyConfig, + storage: make(map[types.Address]map[types.Hash]types.Hash), + gasLimit: gasLimit, + }, + tracer, + ) +} + +func TestStructTracerTxEnd(t *testing.T) { + t.Parallel() + + var ( + gasLimit uint64 = 1024 + gasLeft uint64 = 256 + ) + + tracer := NewStructTracer(testEmptyConfig) + + tracer.TxStart(gasLimit) + tracer.TxEnd(gasLeft) + + assert.Equal( + t, + &StructTracer{ + Config: testEmptyConfig, + storage: make(map[types.Address]map[types.Hash]types.Hash), + gasLimit: gasLimit, + consumedGas: gasLimit - gasLeft, + }, + tracer, + ) +} + +func TestStructTracerCallStart(t *testing.T) { + t.Parallel() + + tracer := NewStructTracer(testEmptyConfig) + + tracer.CallStart( + 1, + testFrom, + testTo, + 2, + 1024, + new(big.Int).SetUint64(10000), + []byte("input"), + ) + + // make sure the method updates nothing + assert.Equal( + t, + NewStructTracer(testEmptyConfig), + tracer, + ) +} + +func TestStructTracerCallEnd(t *testing.T) { + t.Parallel() + + var ( + output = []byte("output") + err = errors.New("call err") + ) + + tests := []struct { + name string + depth int + output []byte + err error + expected *StructTracer + }{ + { + name: "should set output and error if depth is 1", + depth: 1, + output: output, + err: err, + expected: &StructTracer{ + Config: testEmptyConfig, + storage: make(map[types.Address]map[types.Hash]types.Hash), + output: output, + err: err, + }, + }, + { + name: "should update nothing if depth exceeds 1", + depth: 2, + output: output, + err: err, + expected: &StructTracer{ + Config: testEmptyConfig, + storage: make(map[types.Address]map[types.Hash]types.Hash), + }, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + tracer := NewStructTracer(testEmptyConfig) + + tracer.CallEnd(test.depth, test.output, test.err) + + assert.Equal( + t, + test.expected, + tracer, + ) + }) + } +} + +func TestStructTracerCaptureState(t *testing.T) { + t.Parallel() + + var ( + memory = []byte("memory") + stack = []*big.Int{ + big.NewInt(1), + big.NewInt(2), + } + contractAddress = types.StringToAddress("3") + storageValue = types.StringToHash("4") + ) + + tests := []struct { + name string + + // initial state + tracer *StructTracer + + // input + memory []byte + stack []*big.Int + opCode int + contractAddress types.Address + sp int + host tracer.RuntimeHost + vmState tracer.VMState + + // expected state + expectedTracer *StructTracer + expectedVMState tracer.VMState + }{ + { + name: "should capture memory", + tracer: &StructTracer{ + Config: Config{ + EnableMemory: true, + }, + }, + memory: memory, + stack: stack, + opCode: 1, + contractAddress: contractAddress, + sp: 2, + host: nil, + vmState: &mockState{ + halted: false, + }, + expectedTracer: &StructTracer{ + Config: Config{ + EnableMemory: true, + }, + currentMemory: memory, + }, + expectedVMState: &mockState{}, + }, + { + name: "should capture stack", + tracer: &StructTracer{ + Config: Config{ + EnableStack: true, + }, + }, + memory: memory, + stack: stack, + opCode: 1, + contractAddress: contractAddress, + sp: 2, + host: nil, + vmState: &mockState{ + halted: false, + }, + expectedTracer: &StructTracer{ + Config: Config{ + EnableStack: true, + }, + currentStack: stack, + }, + expectedVMState: &mockState{}, + }, + { + name: "should capture storage by SLOAD", + tracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: make(map[types.Address]map[types.Hash]types.Hash), + }, + memory: memory, + stack: stack, + opCode: evm.SLOAD, + contractAddress: contractAddress, + sp: 2, + host: &mockHost{ + getStorageFunc: func(a types.Address, h types.Hash) types.Hash { + assert.Equal(t, contractAddress, a) + assert.Equal(t, types.BytesToHash(big.NewInt(2).Bytes()), h) + + return storageValue + }, + }, + vmState: &mockState{ + halted: false, + }, + expectedTracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: map[types.Address]map[types.Hash]types.Hash{ + contractAddress: { + types.BytesToHash(big.NewInt(2).Bytes()): storageValue, + }, + }, + }, + expectedVMState: &mockState{}, + }, + { + name: "should capture storage by SSTORE", + tracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: make(map[types.Address]map[types.Hash]types.Hash), + }, + memory: memory, + stack: stack, + opCode: evm.SSTORE, + contractAddress: contractAddress, + sp: 2, + host: nil, + vmState: &mockState{ + halted: false, + }, + expectedTracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: map[types.Address]map[types.Hash]types.Hash{ + contractAddress: { + types.BytesToHash(big.NewInt(1).Bytes()): types.BytesToHash(big.NewInt(2).Bytes()), + }, + }, + }, + expectedVMState: &mockState{}, + }, + { + name: "should call Halt() if it's been canceled", + tracer: &StructTracer{ + Config: testEmptyConfig, + interrupt: true, + }, + memory: memory, + stack: stack, + opCode: 1, + contractAddress: contractAddress, + sp: 2, + host: nil, + vmState: &mockState{ + halted: false, + }, + expectedTracer: &StructTracer{ + Config: testEmptyConfig, + interrupt: true, + }, + expectedVMState: &mockState{ + halted: true, + }, + }, + { + name: "should not capture if sp is less than 1 in case op SLOAD", + tracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: make(map[types.Address]map[types.Hash]types.Hash), + }, + memory: memory, + stack: stack, + opCode: evm.SLOAD, + contractAddress: contractAddress, + sp: 0, + host: &mockHost{}, + vmState: &mockState{ + halted: false, + }, + expectedTracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: map[types.Address]map[types.Hash]types.Hash{}, + }, + expectedVMState: &mockState{}, + }, + { + name: "should not capture if sp is less than 2 in case op SSTORE", + tracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: make(map[types.Address]map[types.Hash]types.Hash), + }, + memory: memory, + stack: stack, + opCode: evm.SSTORE, + contractAddress: contractAddress, + sp: 1, + host: nil, + vmState: &mockState{ + halted: false, + }, + expectedTracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: map[types.Address]map[types.Hash]types.Hash{}, + }, + expectedVMState: &mockState{}, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + test.tracer.CaptureState( + test.memory, + test.stack, + test.opCode, + test.contractAddress, + test.sp, + test.host, + test.vmState, + ) + + assert.Equal( + t, + test.expectedTracer, + test.tracer, + ) + + assert.Equal( + t, + test.expectedVMState, + test.vmState, + ) + }) + } +} + +func TestStructTracerExecuteState(t *testing.T) { + t.Parallel() + + var ( + contractAddress = types.StringToAddress("1") + ip = uint64(2) + opCode = "ADD" + availableGas = uint64(1000) + cost = uint64(100) + lastReturnData = []byte("return data") + depth = 1 + err = errors.New("err") + refund = uint64(10000) + + memory = []byte("memory sample") + storage = map[types.Address]map[types.Hash]types.Hash{ + contractAddress: { + types.StringToHash("1"): types.StringToHash("2"), + types.StringToHash("3"): types.StringToHash("4"), + }, + types.StringToAddress("x"): { + types.StringToHash("5"): types.StringToHash("6"), + types.StringToHash("7"): types.StringToHash("8"), + }, + } + ) + + tests := []struct { + name string + + // initial state + tracer *StructTracer + + // input + contractAddress types.Address + ip uint64 + opCode string + availableGas uint64 + cost uint64 + lastReturnData []byte + depth int + err error + host tracer.RuntimeHost + + // expected result + expected []StructLog + }{ + { + name: "should create minimal log", + tracer: &StructTracer{ + Config: testEmptyConfig, + }, + contractAddress: contractAddress, + ip: ip, + opCode: opCode, + availableGas: availableGas, + cost: cost, + lastReturnData: lastReturnData, + depth: depth, + err: err, + host: &mockHost{ + getRefundFn: func() uint64 { + return refund + }, + }, + expected: []StructLog{ + { + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Memory: nil, + MemorySize: 0, + Stack: nil, + ReturnData: nil, + Storage: nil, + Depth: depth, + RefundCounter: refund, + Err: err, + }, + }, + }, + { + name: "should save memory", + tracer: &StructTracer{ + Config: Config{ + EnableMemory: true, + }, + currentMemory: memory, + }, + contractAddress: contractAddress, + ip: ip, + opCode: opCode, + availableGas: availableGas, + cost: cost, + lastReturnData: lastReturnData, + depth: depth, + err: err, + host: &mockHost{ + getRefundFn: func() uint64 { + return refund + }, + }, + expected: []StructLog{ + { + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Memory: memory, + MemorySize: len(memory), + Stack: nil, + ReturnData: nil, + Storage: nil, + Depth: depth, + RefundCounter: refund, + Err: err, + }, + }, + }, + { + name: "should save stack", + tracer: &StructTracer{ + Config: Config{ + EnableStack: true, + }, + currentStack: []*big.Int{ + big.NewInt(1), + big.NewInt(2), + }, + }, + contractAddress: contractAddress, + ip: ip, + opCode: opCode, + availableGas: availableGas, + cost: cost, + lastReturnData: lastReturnData, + depth: depth, + err: err, + host: &mockHost{ + getRefundFn: func() uint64 { + return refund + }, + }, + expected: []StructLog{ + { + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Memory: nil, + MemorySize: 0, + Stack: []*big.Int{ + big.NewInt(1), + big.NewInt(2), + }, + ReturnData: nil, + Storage: nil, + Depth: depth, + RefundCounter: refund, + Err: err, + }, + }, + }, + { + name: "should save return data", + tracer: &StructTracer{ + Config: Config{ + EnableReturnData: true, + }, + }, + contractAddress: contractAddress, + ip: ip, + opCode: opCode, + availableGas: availableGas, + cost: cost, + lastReturnData: lastReturnData, + depth: depth, + err: err, + host: &mockHost{ + getRefundFn: func() uint64 { + return refund + }, + }, + expected: []StructLog{ + { + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Memory: nil, + MemorySize: 0, + Stack: nil, + ReturnData: lastReturnData, + Storage: nil, + Depth: depth, + RefundCounter: refund, + Err: err, + }, + }, + }, + { + name: "should save storage", + tracer: &StructTracer{ + Config: Config{ + EnableStorage: true, + }, + storage: storage, + }, + contractAddress: contractAddress, + ip: ip, + opCode: opCode, + availableGas: availableGas, + cost: cost, + lastReturnData: lastReturnData, + depth: depth, + err: err, + host: &mockHost{ + getRefundFn: func() uint64 { + return refund + }, + }, + expected: []StructLog{ + { + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Memory: nil, + MemorySize: 0, + Stack: nil, + ReturnData: nil, + Storage: map[types.Hash]types.Hash{ + types.StringToHash("1"): types.StringToHash("2"), + types.StringToHash("3"): types.StringToHash("4"), + }, + Depth: depth, + RefundCounter: refund, + Err: err, + }, + }, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + test.tracer.ExecuteState( + test.contractAddress, + test.ip, + test.opCode, + test.availableGas, + test.cost, + test.lastReturnData, + test.depth, + test.err, + test.host, + ) + + assert.Equal( + t, + test.expected, + test.tracer.logs, + ) + }) + } +} + +func TestStructTracerGetResult(t *testing.T) { + t.Parallel() + + var ( + ip = uint64(2) + opCode = "ADD" + availableGas = uint64(1000) + cost = uint64(100) + returnData = []byte("return data") + depth = 1 + // err = errors.New("err") + refund = uint64(10000) + + // memory chunk size must be 32 bytes + memory = append( + []byte("memory sample"), + make([]byte, 19)..., + ) + stack = []*big.Int{ + big.NewInt(1), + big.NewInt(2), + big.NewInt(4), + } + consumedGas = uint64(1024) + + reason = errors.New("timeout") + err = errors.New("out of gas") + + logs = []StructLog{ + { + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Memory: memory, + MemorySize: len(memory), + Stack: stack, + ReturnData: returnData, + Storage: map[types.Hash]types.Hash{ + types.StringToHash("1"): types.StringToHash("2"), + types.StringToHash("3"): types.StringToHash("4"), + }, + Depth: depth, + RefundCounter: refund, + Err: nil, + }, + } + ) + + tests := []struct { + name string + tracer *StructTracer + expected interface{} + err error + }{ + { + name: "should return result", + tracer: &StructTracer{ + Config: testEmptyConfig, + logs: logs, + consumedGas: consumedGas, + output: returnData, + }, + expected: &StructTraceResult{ + Failed: false, + Gas: consumedGas, + ReturnValue: hex.EncodeToString(returnData), + StructLogs: []StructLogRes{ + { + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Depth: depth, + Error: "", + RefundCounter: refund, + Stack: []string{ + "0x1", + "0x2", + "0x4", + }, + Memory: []string{ + fmt.Sprintf( + "%s%s", + hex.EncodeToString([]byte("memory sample")), + strings.Repeat("0", 19*2), + ), + }, + Storage: map[string]string{ + hex.EncodeToString(types.StringToHash("1").Bytes()): hex.EncodeToString(types.StringToHash("2").Bytes()), + hex.EncodeToString(types.StringToHash("3").Bytes()): hex.EncodeToString(types.StringToHash("4").Bytes()), + }, + }, + }, + }, + err: nil, + }, + { + name: "should return empty ReturnValue if error is marked", + tracer: &StructTracer{ + Config: testEmptyConfig, + logs: logs, + consumedGas: consumedGas, + output: returnData, + err: err, + }, + expected: &StructTraceResult{ + Failed: true, + Gas: consumedGas, + ReturnValue: "", + StructLogs: []StructLogRes{ + { + Pc: ip, + Op: opCode, + Gas: availableGas, + GasCost: cost, + Depth: depth, + Error: "", + RefundCounter: refund, + Stack: []string{ + "0x1", + "0x2", + "0x4", + }, + Memory: []string{ + fmt.Sprintf( + "%s%s", + hex.EncodeToString([]byte("memory sample")), + strings.Repeat("0", 19*2), + ), + }, + Storage: map[string]string{ + hex.EncodeToString(types.StringToHash("1").Bytes()): hex.EncodeToString(types.StringToHash("2").Bytes()), + hex.EncodeToString(types.StringToHash("3").Bytes()): hex.EncodeToString(types.StringToHash("4").Bytes()), + }, + }, + }, + }, + err: nil, + }, + { + name: "should return error", + tracer: &StructTracer{ + Config: testEmptyConfig, + reason: reason, + logs: logs, + }, + expected: nil, + err: reason, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + res, err := test.tracer.GetResult() + + assert.Equal( + t, + test.expected, + res, + ) + + assert.Equal( + t, + test.err, + err, + ) + }) + } +} diff --git a/state/runtime/tracer/types.go b/state/runtime/tracer/types.go new file mode 100644 index 0000000000..902d8d8156 --- /dev/null +++ b/state/runtime/tracer/types.go @@ -0,0 +1,70 @@ +package tracer + +import ( + "math/big" + + "github.com/0xPolygon/polygon-edge/types" +) + +// RuntimeHost is the interface defining the methods for accessing state by tracer +type RuntimeHost interface { + // GetRefund returns refunded value + GetRefund() uint64 + // GetStorage access the storage slot at the given address and slot hash + GetStorage(types.Address, types.Hash) types.Hash +} + +type VMState interface { + // Halt tells VM to terminate its process + Halt() +} + +type Tracer interface { + // Cancel tells termination of execution and tracing + Cancel(error) + // Clear clears the tracked data + Clear() + // GetResult returns a result based on tracked data + GetResult() (interface{}, error) + + // Tx-level + TxStart(gasLimit uint64) + TxEnd(gasLeft uint64) + + // Call-level + CallStart( + depth int, // begins from 1 + from, to types.Address, + callType int, + gas uint64, + value *big.Int, + input []byte, + ) + CallEnd( + depth int, // begins from 1 + output []byte, + err error, + ) + + // Op-level + CaptureState( + memory []byte, + stack []*big.Int, + opCode int, + contractAddress types.Address, + sp int, + host RuntimeHost, + state VMState, + ) + ExecuteState( + contractAddress types.Address, + ip uint64, + opcode string, + availableGas uint64, + cost uint64, + lastReturnData []byte, + depth int, + err error, + host RuntimeHost, + ) +}