From 7e17e4cf3a52fd39d296129a8d6777621bda7355 Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi Date: Mon, 4 Sep 2023 17:06:41 +0200 Subject: [PATCH] implement transfers as logs --- internal/ethapi/api.go | 22 ++--- internal/ethapi/api_test.go | 38 ++++++--- internal/ethapi/transfertracer.go | 135 +++++++++++++++++++++++++----- 3 files changed, 144 insertions(+), 51 deletions(-) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index c061b1700c..4fdb138ccc 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1199,7 +1199,6 @@ type blockResult struct { type callResult struct { ReturnValue hexutil.Bytes `json:"return"` Logs []*types.Log `json:"logs"` - Transfers []transfer `json:"transfers,omitempty"` GasUsed hexutil.Uint64 `json:"gasUsed"` Status hexutil.Uint64 `json:"status"` Error string `json:"error,omitempty"` @@ -1278,12 +1277,9 @@ func (s *BlockChainAPI) MulticallV1(ctx context.Context, opts multicallOpts, blo } gasUsed := uint64(0) for i, call := range block.Calls { - // Hack to get logs from statedb which stores logs by txhash. - txhash := common.BigToHash(big.NewInt(int64(i))) - state.SetTxContext(txhash, i) - vmConfig := &vm.Config{NoBaseFee: true} - if opts.TraceTransfers { - vmConfig.Tracer = newTracer() + vmConfig := &vm.Config{ + NoBaseFee: true, + Tracer: newTracer(opts.TraceTransfers, blockContext.BlockNumber.Uint64(), hash, common.Hash{}, uint(i)), } result, err := doCall(ctx, s.b, call, state, header, timeout, gp, &blockContext, vmConfig, precompiles, opts.Validation) if err != nil { @@ -1294,16 +1290,8 @@ func (s *BlockChainAPI) MulticallV1(ctx context.Context, opts multicallOpts, blo if len(result.Revert()) > 0 { result.Err = newRevertError(result) } - logs := state.GetLogs(txhash, blockContext.BlockNumber.Uint64(), common.Hash{}) - // Clear the garbage txhash that was filled in. - for _, l := range logs { - l.TxHash = common.Hash{} - } - var transfers []transfer - if opts.TraceTransfers { - transfers = vmConfig.Tracer.(*tracer).Transfers() - } - callRes := callResult{ReturnValue: result.Return(), Logs: logs, Transfers: transfers, GasUsed: hexutil.Uint64(result.UsedGas)} + logs := vmConfig.Tracer.(*tracer).Logs() + callRes := callResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas)} if result.Failed() { callRes.Status = hexutil.Uint64(types.ReceiptStatusFailed) callRes.Error = result.Err.Error() diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index bb4296436a..8ace919007 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -712,7 +712,6 @@ func TestMulticallV1(t *testing.T) { Logs []types.Log GasUsed string Status string - Transfers []transfer } type blockRes struct { Number string @@ -1010,6 +1009,7 @@ func TestMulticallV1(t *testing.T) { Topics: []common.Hash{common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")}, BlockNumber: 11, Data: []byte{}, + BlockHash: hex2Hash(n11hash), }}, GasUsed: "0x5508", Status: "0x1", @@ -1175,18 +1175,28 @@ func TestMulticallV1(t *testing.T) { Calls: []callRes{{ ReturnValue: "0x", GasUsed: "0xd984", - Transfers: []transfer{ - { - From: accounts[0].addr, - To: randomAccounts[0].addr, - Value: big.NewInt(50), - }, { - From: randomAccounts[0].addr, - To: randomAccounts[1].addr, - Value: big.NewInt(100), + Logs: []types.Log{{ + Address: common.Address{}, + Topics: []common.Hash{ + transferTopic, + accounts[0].addr.Hash(), + randomAccounts[0].addr.Hash(), }, - }, - Logs: []types.Log{}, + Data: common.BigToHash(big.NewInt(50)).Bytes(), + BlockNumber: 11, + BlockHash: hex2Hash(n11hash), + }, { + Address: common.Address{}, + Topics: []common.Hash{ + transferTopic, + randomAccounts[0].addr.Hash(), + randomAccounts[1].addr.Hash(), + }, + Data: common.BigToHash(big.NewInt(100)).Bytes(), + BlockNumber: 11, + BlockHash: hex2Hash(n11hash), + Index: 1, + }}, Status: "0x1", }}, }}, @@ -1532,3 +1542,7 @@ func TestRPCMarshalBlock(t *testing.T) { } } } + +func hex2Hash(s string) common.Hash { + return common.BytesToHash(common.FromHex(s)) +} diff --git a/internal/ethapi/transfertracer.go b/internal/ethapi/transfertracer.go index 4db9588783..62b6febd4f 100644 --- a/internal/ethapi/transfertracer.go +++ b/internal/ethapi/transfertracer.go @@ -17,38 +17,57 @@ package ethapi import ( + "fmt" "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" ) -type callLog struct { - Address common.Address `json:"address"` - Topics []common.Hash `json:"topics"` - Data hexutil.Bytes `json:"data"` -} +// keccak256("Transfer(address,address,uint256)") +var transferTopic = common.HexToHash("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") -type transfer struct { - From common.Address `json:"from"` - To common.Address `json:"to"` - Value *big.Int `json:"value"` -} - -// tracer is a simple tracer that records all ether transfers. -// This includes tx value, call value, and self destructs. +// tracer is a simple tracer that records all logs and +// ether transfers. Transfers are recorded as if they +// were logs. Transfer events include: +// - tx value +// - call value +// - self destructs +// +// The log format for a transfer is: +// - address: 0x0000000000000000000000000000000000000000 +// - data: Value +// - topics: +// - Transfer(address,address,uint256) +// - Sender address +// - Recipient address +// +// TODO: embed noopTracer type tracer struct { - transfers []transfer + logs []*types.Log + traceTransfers bool + // TODO: replace with tracers.Context once extended tracer PR is merged. + blockNumber uint64 + blockHash common.Hash + txHash common.Hash + txIdx uint } -func newTracer() *tracer { - return &tracer{transfers: make([]transfer, 0)} +func newTracer(traceTransfers bool, blockNumber uint64, blockHash, txHash common.Hash, txIdx uint) *tracer { + return &tracer{ + logs: make([]*types.Log, 0), + traceTransfers: traceTransfers, + blockNumber: blockNumber, + blockHash: blockHash, + txHash: txHash, + txIdx: txIdx, + } } func (t *tracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { if value.Cmp(common.Big0) > 0 { - t.transfers = append(t.transfers, transfer{From: from, To: to, Value: value}) + t.captureTransfer(from, to, value) } } @@ -56,6 +75,34 @@ func (t *tracer) CaptureEnd(output []byte, gasUsed uint64, err error) { } func (t *tracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + // skip if the previous op caused an error + if err != nil { + return + } + // TODO: Use OnLog instead of CaptureState once extended tracer PR is merged. + switch op { + case vm.LOG0, vm.LOG1, vm.LOG2, vm.LOG3, vm.LOG4: + size := int(op - vm.LOG0) + + stack := scope.Stack + stackData := stack.Data() + + // Don't modify the stack + mStart := stackData[len(stackData)-1] + mSize := stackData[len(stackData)-2] + topics := make([]common.Hash, size) + for i := 0; i < size; i++ { + topic := stackData[len(stackData)-2-(i+1)] + topics[i] = common.Hash(topic.Bytes32()) + } + + data, err := getMemoryCopyPadded(scope.Memory, int64(mStart.Uint64()), int64(mSize.Uint64())) + if err != nil { + // mSize was unrealistically large + return + } + t.captureLog(scope.Contract.Address(), topics, data) + } } func (t *tracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { @@ -63,8 +110,8 @@ func (t *tracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope * func (t *tracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { toCopy := to - if value.Cmp(common.Big0) > 0 { - t.transfers = append(t.transfers, transfer{From: from, To: toCopy, Value: value}) + if value != nil && value.Cmp(common.Big0) > 0 { + t.captureTransfer(from, toCopy, value) } } @@ -76,6 +123,50 @@ func (t *tracer) CaptureTxStart(gasLimit uint64) {} func (t *tracer) CaptureTxEnd(restGas uint64) {} -func (t *tracer) Transfers() []transfer { - return t.transfers +func (t *tracer) captureLog(address common.Address, topics []common.Hash, data []byte) { + t.logs = append(t.logs, &types.Log{ + Address: address, + Topics: topics, + Data: data, + BlockNumber: t.blockNumber, + BlockHash: t.blockHash, + TxHash: t.txHash, + TxIndex: t.txIdx, + Index: uint(len(t.logs)), + }) +} + +func (t *tracer) captureTransfer(from, to common.Address, value *big.Int) { + if !t.traceTransfers { + return + } + topics := []common.Hash{ + transferTopic, + common.BytesToHash(from.Bytes()), + common.BytesToHash(to.Bytes()), + } + t.captureLog(common.Address{}, topics, common.BigToHash(value).Bytes()) +} + +func (t *tracer) Logs() []*types.Log { + return t.logs +} + +// TODO: remove once extended tracer PR is merged. +func getMemoryCopyPadded(m *vm.Memory, offset, size int64) ([]byte, error) { + if offset < 0 || size < 0 { + return nil, fmt.Errorf("offset or size must not be negative") + } + if int(offset+size) < m.Len() { // slice fully inside memory + return m.GetCopy(offset, size), nil + } + paddingNeeded := int(offset+size) - m.Len() + if paddingNeeded > 1024*1024 { + return nil, fmt.Errorf("reached limit for padding memory slice: %d", paddingNeeded) + } + cpy := make([]byte, size) + if overlap := int64(m.Len()) - offset; overlap > 0 { + copy(cpy, m.GetPtr(offset, overlap)) + } + return cpy, nil }