Process ETH transaction failure reasons

This commit is contained in:
Martin Boehm 2021-12-29 23:53:27 +01:00 committed by Martin
parent 91031715f7
commit 45a53e41a1
8 changed files with 208 additions and 14 deletions

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -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)

View File

@ -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

View File

@ -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 = &ethInternalData{
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

View File

@ -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)
}

View File

@ -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)

View File

@ -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,