From 5818ce8aa284515099ced3f745741ea5e7cbeadb Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Wed, 8 Dec 2021 09:49:42 +0100 Subject: [PATCH] Ethereum: process call trace to extract internal transactions --- bchain/coins/eth/ethparser.go | 30 ++++++++--- bchain/coins/eth/ethrpc.go | 93 +++++++++++++++++++++++++++++++++-- bchain/types.go | 25 ++++++++++ 3 files changed, 137 insertions(+), 11 deletions(-) diff --git a/bchain/coins/eth/ethparser.go b/bchain/coins/eth/ethparser.go index 884b6e64..8526825d 100644 --- a/bchain/coins/eth/ethparser.go +++ b/bchain/coins/eth/ethparser.go @@ -77,8 +77,9 @@ type rpcReceipt struct { } type completeTransaction struct { - Tx *rpcTransaction `json:"tx"` - Receipt *rpcReceipt `json:"receipt,omitempty"` + Tx *rpcTransaction `json:"tx"` + InternalData *bchain.EthereumInternalData `json:"internalData,omitempty"` + Receipt *rpcReceipt `json:"receipt,omitempty"` } type rpcBlockTransactions struct { @@ -96,7 +97,7 @@ func ethNumber(n string) (int64, error) { return 0, errors.Errorf("Not a number: '%v'", n) } -func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, receipt *rpcReceipt, blocktime int64, confirmations uint32, fixEIP55 bool) (*bchain.Tx, error) { +func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, receipt *rpcReceipt, internalData *bchain.EthereumInternalData, blocktime int64, confirmations uint32, fixEIP55 bool) (*bchain.Tx, error) { txid := tx.Hash var ( fa, ta []string @@ -121,9 +122,24 @@ func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, receipt *rpcReceipt, bloc } } } + if internalData != nil { + // ignore empty internal data + if internalData.Type == bchain.CALL && len(internalData.Transfers) == 0 { + internalData = nil + } else { + if fixEIP55 { + for i := range internalData.Transfers { + it := &internalData.Transfers[i] + it.From = EIP55AddressFromAddress(it.From) + it.To = EIP55AddressFromAddress(it.To) + } + } + } + } ct := completeTransaction{ - Tx: tx, - Receipt: receipt, + Tx: tx, + InternalData: internalData, + Receipt: receipt, } vs, err := hexutil.DecodeBig(tx.Value) if err != nil { @@ -254,6 +270,7 @@ func hexEncodeBig(b []byte) string { } // PackTx packs transaction to byte array +// completeTransaction.InternalData are not packed, they are stored in a different table func (p *EthereumParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) { var err error var n uint64 @@ -396,7 +413,8 @@ func (p *EthereumParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { Logs: logs, } } - tx, err := p.ethTxToTx(&rt, rr, int64(pt.BlockTime), 0, false) + // TODO handle internal transactions + tx, err := p.ethTxToTx(&rt, rr, nil, int64(pt.BlockTime), 0, false) if err != nil { return nil, 0, err } diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index 551c650c..8bb585aa 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -42,6 +42,7 @@ type Configuration struct { BlockAddressesToKeep int `json:"block_addresses_to_keep"` MempoolTxTimeoutHours int `json:"mempoolTxTimeoutHours"` QueryBackendOnMempoolResync bool `json:"queryBackendOnMempoolResync"` + ProcessInternalTransactions bool `json:"processInternalTransactions"` } // EthereumRPC is an interface to JSON-RPC eth service. @@ -157,11 +158,9 @@ func (b *EthereumRPC) Initialize() error { case MainNet: b.Testnet = false b.Network = "livenet" - break case TestNet: b.Testnet = true b.Network = "testnet" - break case TestNetGoerli: b.Testnet = true b.Network = "goerli" @@ -511,6 +510,83 @@ func (b *EthereumRPC) getERC20EventsForBlock(blockNumber string) (map[string][]* return r, nil } +type rpcCallTrace struct { + // CREATE, CREATE2, SELFDESTRUCT, CALL, CALLCODE, DELEGATECALL, STATICCALL + Type string `json:"type"` + From string `json:"from"` + To string `json:"to"` + Value string `json:"value"` + Error string `json:"error"` + Calls []rpcCallTrace `json:"calls"` +} + +type rpcTraceResult struct { + Result rpcCallTrace `json:"result"` +} + +func (b *EthereumRPC) processCallTrace(call rpcCallTrace, d *bchain.EthereumInternalData) { + value, err := hexutil.DecodeBig(call.Value) + if call.Type == "CREATE" { + d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{ + Type: bchain.CREATE, + Value: *value, + From: call.From, + To: call.To, + }) + + } else if call.Type == "SELFDESTRUCT" { + d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{ + Type: bchain.SELFDESTRUCT, + Value: *value, + From: call.From, + To: call.To, + }) + } else if err == nil && value.BitLen() > 0 { + d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{ + Value: *value, + From: call.From, + To: call.To, + }) + } + for i := range call.Calls { + b.processCallTrace(call.Calls[i], d) + } +} + +// getInternalDataForBlock fetches debug trace using callTracer, extracts internal transfers and creations and destructions of contracts +// by design, it never returns error so that missing internal transactions do not stop the rest of the blockchain import +func (b *EthereumRPC) getInternalDataForBlock(blockHash string, transactions []rpcTransaction) ([]bchain.EthereumInternalData, error) { + data := make([]bchain.EthereumInternalData, len(transactions)) + if b.ChainConfig.ProcessInternalTransactions { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + var trace []rpcTraceResult + err := b.rpc.CallContext(ctx, &trace, "debug_traceBlockByHash", blockHash, map[string]interface{}{"tracer": "callTracer"}) + if err != nil { + glog.Error("debug_traceBlockByHash block ", blockHash, ", error ", err) + return data, nil + } + if len(trace) != len(data) { + glog.Error("debug_traceBlockByHash block ", blockHash, ", error: trace length does not match block length ", len(trace), "!=", len(data)) + return data, nil + } + for i, result := range trace { + r := &result.Result + d := &data[i] + if r.Type == "CREATE" { + d.Type = bchain.CREATE + d.Contract = r.To + } else if r.Type == "SELFDESTRUCT" { + d.Type = bchain.SELFDESTRUCT + } + for j := range r.Calls { + b.processCallTrace(r.Calls[j], d) + } + } + } + return data, nil +} + // GetBlock returns block with given hash or height, hash has precedence if both passed func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { raw, err := b.getBlockRaw(hash, height, true) @@ -534,10 +610,16 @@ func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error if err != nil { return nil, err } + + internalData, err := b.getInternalDataForBlock(head.Hash, body.Transactions) + if err != nil { + return nil, err + } + btxs := make([]bchain.Tx, len(body.Transactions)) for i := range body.Transactions { tx := &body.Transactions[i] - btx, err := b.Parser.ethTxToTx(tx, &rpcReceipt{Logs: logs[tx.Hash]}, bbh.Time, uint32(bbh.Confirmations), true) + btx, err := b.Parser.ethTxToTx(tx, &rpcReceipt{Logs: logs[tx.Hash]}, &internalData[i], bbh.Time, uint32(bbh.Confirmations), true) if err != nil { return nil, errors.Annotatef(err, "hash %v, height %v, txid %v", hash, height, tx.Hash) } @@ -603,7 +685,7 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { var btx *bchain.Tx if tx.BlockNumber == "" { // mempool tx - btx, err = b.Parser.ethTxToTx(tx, nil, 0, 0, true) + btx, err = b.Parser.ethTxToTx(tx, nil, nil, 0, 0, true) if err != nil { return nil, errors.Annotatef(err, "txid %v", txid) } @@ -636,7 +718,8 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { if err != nil { return nil, errors.Annotatef(err, "txid %v", txid) } - btx, err = b.Parser.ethTxToTx(tx, &receipt, time, confirmations, true) + // TODO - handle internal tx + btx, err = b.Parser.ethTxToTx(tx, &receipt, nil, time, confirmations, true) if err != nil { return nil, errors.Annotatef(err, "txid %v", txid) } diff --git a/bchain/types.go b/bchain/types.go index 29d8f2b2..7c96f773 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -202,6 +202,31 @@ func AddressDescriptorFromString(s string) (AddressDescriptor, error) { // EthereumType specific +// EthereumInternalTransfer contains data about internal transfer +type EthereumInternalTransfer struct { + Type EthereumInternalTransactionType `json:"type"` + From string `json:"from"` + To string `json:"to"` + Value big.Int `json:"value"` +} + +// EthereumInternalTransactionType - type of ethereum transaction from internal data +type EthereumInternalTransactionType int + +// EthereumInternalTransactionType enumeration +const ( + CALL = EthereumInternalTransactionType(iota) + CREATE + SELFDESTRUCT +) + +// EthereumInternalTransaction contains internal transfers +type EthereumInternalData struct { + Type EthereumInternalTransactionType `json:"type"` + Contract string `json:"contract,omitempty"` + Transfers []EthereumInternalTransfer `json:"transfers,omitempty"` +} + // Erc20Contract contains info about ERC20 contract type Erc20Contract struct { Contract string `json:"contract"`