diff --git a/bchain/coins/eth/ethparser.go b/bchain/coins/eth/ethparser.go index 8b8bdf1d..2c975fac 100644 --- a/bchain/coins/eth/ethparser.go +++ b/bchain/coins/eth/ethparser.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "math/big" "strconv" + "strings" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/proto" @@ -88,7 +89,7 @@ func (p *EthereumParser) ethTxToTx(tx *bchain.RpcTransaction, receipt *bchain.Rp } if internalData != nil { // ignore empty internal data - if internalData.Type == bchain.CALL && len(internalData.Transfers) == 0 { + if internalData.Type == bchain.CALL && len(internalData.Transfers) == 0 && len(internalData.Error) == 0 { internalData = nil } else { if fixEIP55 { @@ -505,3 +506,45 @@ func GetEthereumTxDataFromSpecificData(coinSpecificData interface{}) *EthereumTx } return &etd } + +const errorOutputSignature = "08c379a0" + +// ParseErrorFromOutput takes output field from internal transaction data and extracts an error message from it +// the output must have errorOutputSignature to be parsed +func ParseErrorFromOutput(output string) string { + if has0xPrefix(output) { + output = output[2:] + } + if len(output) < 8+64+64+64 || output[:8] != errorOutputSignature { + return "" + } + return parseErc20StringProperty(nil, output[8:]) +} + +// PackInternalTransactionError packs common error messages to single byte to save DB space +func PackInternalTransactionError(e string) string { + if e == "execution reverted" { + return "\x01" + } + if e == "out of gas" { + return "\x02" + } + if e == "contract creation code storage out of gas" { + return "\x03" + } + if e == "max code size exceeded" { + return "\x04" + } + + return e +} + +// UnpackInternalTransactionError unpacks common error messages packed by PackInternalTransactionError +func UnpackInternalTransactionError(data []byte) string { + e := string(data) + e = strings.ReplaceAll(e, "\x01", "Reverted. ") + e = strings.ReplaceAll(e, "\x02", "Out of gas. ") + e = strings.ReplaceAll(e, "\x03", "Contract creation code storage out of gas. ") + e = strings.ReplaceAll(e, "\x04", "Max code size exceeded. ") + return strings.TrimSpace(e) +} diff --git a/bchain/coins/eth/ethparser_test.go b/bchain/coins/eth/ethparser_test.go index c990343c..ac54e6b3 100644 --- a/bchain/coins/eth/ethparser_test.go +++ b/bchain/coins/eth/ethparser_test.go @@ -400,3 +400,97 @@ func TestEthereumParser_GetEthereumTxData(t *testing.T) { }) } } + +func TestEthereumParser_ParseErrorFromOutput(t *testing.T) { + tests := []struct { + name string + output string + want string + }{ + { + name: "ParseErrorFromOutput 1", + output: "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000031546f74616c206e756d626572206f662067726f757073206d7573742062652067726561746572207468616e207a65726f2e000000000000000000000000000000", + want: "Total number of groups must be greater than zero.", + }, + { + name: "ParseErrorFromOutput 2", + output: "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000126e6f7420656e6f7567682062616c616e63650000000000000000000000000000", + want: "not enough balance", + }, + { + name: "ParseErrorFromOutput empty", + output: "", + want: "", + }, + { + name: "ParseErrorFromOutput short", + output: "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012", + want: "", + }, + { + name: "ParseErrorFromOutput invalid signature", + output: "0x08c379b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000126e6f7420656e6f7567682062616c616e63650000000000000000000000000000", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ParseErrorFromOutput(tt.output) + if got != tt.want { + t.Errorf("EthereumParser.ParseErrorFromOutput() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEthereumParser_PackInternalTransactionError_UnpackInternalTransactionError(t *testing.T) { + tests := []struct { + name string + original string + packed string + unpacked string + }{ + { + name: "execution reverted", + original: "execution reverted", + packed: "\x01", + unpacked: "Reverted.", + }, + { + name: "out of gas", + original: "out of gas", + packed: "\x02", + unpacked: "Out of gas.", + }, + { + name: "contract creation code storage out of gas", + original: "contract creation code storage out of gas", + packed: "\x03", + unpacked: "Contract creation code storage out of gas.", + }, + { + name: "max code size exceeded", + original: "max code size exceeded", + packed: "\x04", + unpacked: "Max code size exceeded.", + }, + { + name: "unknown error", + original: "unknown error", + packed: "unknown error", + unpacked: "unknown error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + packed := PackInternalTransactionError(tt.original) + if packed != tt.packed { + t.Errorf("EthereumParser.PackInternalTransactionError() = %v, want %v", packed, tt.packed) + } + unpacked := UnpackInternalTransactionError([]byte(packed)) + if unpacked != tt.unpacked { + t.Errorf("EthereumParser.UnpackInternalTransactionError() = %v, want %v", unpacked, tt.unpacked) + } + }) + } +} diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index df3dc52f..ac3ad3d9 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "strconv" + "strings" "sync" "time" @@ -512,19 +513,20 @@ func (b *EthereumRPC) getERC20EventsForBlock(blockNumber string) (map[string][]* 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 string `json:"type"` + From string `json:"from"` + To string `json:"to"` + Value string `json:"value"` + Error string `json:"error"` + Output string `json:"output"` + Calls []rpcCallTrace `json:"calls"` } type rpcTraceResult struct { Result rpcCallTrace `json:"result"` } -func (b *EthereumRPC) processCallTrace(call rpcCallTrace, d *bchain.EthereumInternalData) { +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{ @@ -548,8 +550,11 @@ func (b *EthereumRPC) processCallTrace(call rpcCallTrace, d *bchain.EthereumInte To: call.To, }) } + if call.Error != "" { + d.Error = call.Error + } for i := range call.Calls { - b.processCallTrace(call.Calls[i], d) + b.processCallTrace(&call.Calls[i], d) } } @@ -579,7 +584,28 @@ func (b *EthereumRPC) getInternalDataForBlock(blockHash string, transactions []b d.Type = bchain.SELFDESTRUCT } for j := range r.Calls { - b.processCallTrace(r.Calls[j], d) + b.processCallTrace(&r.Calls[j], d) + } + if r.Error != "" { + baseError := PackInternalTransactionError(r.Error) + if len(baseError) > 1 { + // n, _ := ethNumber(transactions[i].BlockNumber) + // glog.Infof("Internal Data Error %d %s: unknown base error %s", n, transactions[i].Hash, baseError) + baseError = strings.ToUpper(baseError[:1]) + baseError[1:] + ". " + } + outputError := ParseErrorFromOutput(r.Output) + if len(outputError) > 0 { + d.Error = baseError + strings.ToUpper(outputError[:1]) + outputError[1:] + } else { + traceError := PackInternalTransactionError(d.Error) + if traceError == baseError { + d.Error = baseError + } else { + d.Error = baseError + traceError + } + } + // n, _ := ethNumber(transactions[i].BlockNumber) + // glog.Infof("Internal Data Error %d %s: %s", n, transactions[i].Hash, UnpackInternalTransactionError([]byte(d.Error))) } } } @@ -719,7 +745,6 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { if err != nil { return nil, errors.Annotatef(err, "txid %v", txid) } - // 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_ethereum_type.go b/bchain/types_ethereum_type.go index 93bf3c97..f4ca166b 100644 --- a/bchain/types_ethereum_type.go +++ b/bchain/types_ethereum_type.go @@ -27,6 +27,7 @@ type EthereumInternalData struct { Type EthereumInternalTransactionType `json:"type"` Contract string `json:"contract,omitempty"` Transfers []EthereumInternalTransfer `json:"transfers,omitempty"` + Error string } // Erc20Contract contains info about ERC20 contract diff --git a/db/rocksdb_ethereumtype.go b/db/rocksdb_ethereumtype.go index cde8eab0..4abc0bd4 100644 --- a/db/rocksdb_ethereumtype.go +++ b/db/rocksdb_ethereumtype.go @@ -189,6 +189,7 @@ type ethInternalData struct { internalType bchain.EthereumInternalTransactionType contract bchain.AddressDescriptor transfers []ethInternalTransfer + errorMsg string } type ethBlockTx struct { @@ -242,6 +243,7 @@ func (d *RocksDB) processAddressesEthereumType(block *bchain.Block, addresses ad if eid.InternalData != nil { blockTx.internalData = ðInternalData{ internalType: eid.InternalData.Type, + errorMsg: eid.InternalData.Error, } // index contract creation if eid.InternalData.Type == bchain.CREATE { @@ -365,6 +367,9 @@ func packEthInternalData(data *ethInternalData) []byte { l = packBigint(&t.value, varBuf) buf = append(buf, varBuf[:l]...) } + if len(data.errorMsg) > 0 { + buf = append(buf, []byte(data.errorMsg)...) + } return buf } @@ -398,6 +403,7 @@ func (d *RocksDB) unpackEthInternalData(buf []byte) (*bchain.EthereumInternalDat t.Value, ll = unpackBigint(buf[l:]) l += ll } + id.Error = eth.UnpackInternalTransactionError(buf[l:]) return &id, nil } @@ -423,7 +429,7 @@ func (d *RocksDB) storeInternalDataEthereumType(wb *gorocksdb.WriteBatch, blockT for i := range blockTxs { blockTx := &blockTxs[i] if blockTx.internalData != nil { - wb.PutCF(d.cfh[cfInternalData], blockTx.btxID, packEthInternalData(blockTx.internalData)) + wb.PutCF(d.cfh[cfInternalData], blockTx.btxID, packEthInternalData(blockTx.internalData)) } } return nil diff --git a/db/rocksdb_ethereumtype_test.go b/db/rocksdb_ethereumtype_test.go index d3ce848b..fb5d70eb 100644 --- a/db/rocksdb_ethereumtype_test.go +++ b/db/rocksdb_ethereumtype_test.go @@ -163,6 +163,11 @@ func verifyAfterEthereumTypeBlock2(t *testing.T, d *RocksDB, wantBlockInternalDa "00" + dbtestdata.EthAddr3e + dbtestdata.EthAddr3e + "030f4242", nil, }, + { + dbtestdata.EthTxidB2T1, + "00" + hex.EncodeToString([]byte(dbtestdata.EthTx3InternalData.Error)), + nil, + }, { dbtestdata.EthTxidB2T2, "05" + dbtestdata.EthAddrContract0d + @@ -231,6 +236,7 @@ func formatInternalData(in *bchain.EthereumInternalData) *bchain.EthereumInterna t.From = eth.EIP55AddressFromAddress(t.From) t.To = eth.EIP55AddressFromAddress(t.To) } + out.Error = eth.UnpackInternalTransactionError([]byte(in.Error)) return &out } @@ -295,6 +301,10 @@ func TestRocksDB_Index_EthereumType(t *testing.T) { if err != nil || !reflect.DeepEqual(id, formatInternalData(dbtestdata.EthTx2InternalData)) { t.Errorf("GetEthereumInternalData(%s) = %+v, want %+v, err %v", dbtestdata.EthTxidB1T2, id, formatInternalData(dbtestdata.EthTx2InternalData), err) } + id, err = d.GetEthereumInternalData(dbtestdata.EthTxidB2T1) + if err != nil || !reflect.DeepEqual(id, formatInternalData(dbtestdata.EthTx3InternalData)) { + t.Errorf("GetEthereumInternalData(%s) = %+v, want %+v, err %v", dbtestdata.EthTxidB2T1, id, formatInternalData(dbtestdata.EthTx3InternalData), err) + } id, err = d.GetEthereumInternalData(dbtestdata.EthTxidB2T2) if err != nil || !reflect.DeepEqual(id, formatInternalData(dbtestdata.EthTx4InternalData)) { t.Errorf("GetEthereumInternalData(%s) = %+v, want %+v, err %v", dbtestdata.EthTxidB2T2, id, formatInternalData(dbtestdata.EthTx4InternalData), err) @@ -348,7 +358,15 @@ func TestRocksDB_Index_EthereumType(t *testing.T) { // Test tx caching functionality, leave one tx in db to test cleanup in DisconnectBlock testTxCache(t, d, block1, &block1.Txs[0]) + // InternalData are not packed and stored in DB, remove them so that the test does not fail + esd, _ := block2.Txs[0].CoinSpecificData.(bchain.EthereumSpecificData) + eid := esd.InternalData + esd.InternalData = nil + block2.Txs[0].CoinSpecificData = esd testTxCache(t, d, block2, &block2.Txs[0]) + // restore InternalData + esd.InternalData = eid + block2.Txs[0].CoinSpecificData = esd if err = d.PutTx(&block2.Txs[1], block2.Height, block2.Txs[1].Blocktime); err != nil { t.Fatal(err) } diff --git a/db/rocksdb_test.go b/db/rocksdb_test.go index c74e8b23..f5118501 100644 --- a/db/rocksdb_test.go +++ b/db/rocksdb_test.go @@ -515,7 +515,7 @@ func testTxCache(t *testing.T, d *RocksDB, b *bchain.Block, tx *bchain.Tx) { // Confirmations are not stored in the DB, set them from input tx gtx.Confirmations = tx.Confirmations if !reflect.DeepEqual(gtx, tx) { - t.Errorf("GetTx: %v, want %v", gtx, tx) + t.Errorf("GetTx: %+v, want %+v", gtx, tx) } if err := d.DeleteTx(tx.Txid); err != nil { t.Fatal(err) diff --git a/tests/dbtestdata/dbtestdata_ethereumtype.go b/tests/dbtestdata/dbtestdata_ethereumtype.go index 56e04ec5..65683ec5 100644 --- a/tests/dbtestdata/dbtestdata_ethereumtype.go +++ b/tests/dbtestdata/dbtestdata_ethereumtype.go @@ -54,6 +54,12 @@ var EthTx2InternalData = &bchain.EthereumInternalData{ }, } +var EthTx3InternalData = &bchain.EthereumInternalData{ + Type: bchain.CALL, + Transfers: []bchain.EthereumInternalTransfer{}, + Error: "\x01Something wrong", +} + var EthTx4InternalData = &bchain.EthereumInternalData{ Type: bchain.CREATE, Contract: EthAddrContract0d, @@ -127,7 +133,8 @@ func GetTestEthereumTypeBlock2(parser bchain.BlockChainParser) *bchain.Block { Confirmations: 1, }, Txs: unpackTxs([]packedAndInternal{{ - packed: EthTx3Packed, + packed: EthTx3Packed, + internal: EthTx3InternalData, }, { packed: EthTx4Packed, internal: EthTx4InternalData,