Skip to content

Commit

Permalink
Merge pull request #39 from siburu/custom-error
Browse files Browse the repository at this point in the history
Support for logging custom errors on tx execution revert in not only real tx execution but also gas estimation

Signed-off-by: Jun Kimura <jun.kimura@datachain.jp>
  • Loading branch information
bluele authored Apr 26, 2024
2 parents 7a84862 + 99492cd commit 593dd65
Show file tree
Hide file tree
Showing 10 changed files with 486 additions and 187 deletions.
151 changes: 73 additions & 78 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package client

import (
"context"
"fmt"
"encoding/json"
"math/big"
"time"

Expand All @@ -14,7 +14,6 @@ import (
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/hyperledger-labs/yui-relayer/log"
)

type ETHClient struct {
Expand Down Expand Up @@ -62,70 +61,41 @@ func (cl *ETHClient) Raw() *rpc.Client {
return cl.Client.Client()
}

func (cl *ETHClient) GetTransactionReceipt(ctx context.Context, txHash common.Hash, enableDebugTrace bool) (rc *gethtypes.Receipt, revertReason string, err error) {
func (cl *ETHClient) GetTransactionReceipt(ctx context.Context, txHash common.Hash) (rc *Receipt, err error) {
var r *Receipt

if err := cl.Raw().CallContext(ctx, &r, "eth_getTransactionReceipt", txHash); err != nil {
return nil, "", err
return nil, err
} else if r == nil {
return nil, "", ethereum.NotFound
} else if r.Status == gethtypes.ReceiptStatusSuccessful {
return &r.Receipt, "", nil
} else if r.HasRevertReason() {
reason, err := r.GetRevertReason()
if err != nil {
// TODO: use more proper logger
logger := log.GetLogger().WithModule("ethereum.chain")
logger.Error("failed to get revert reason", err)
}
return &r.Receipt, reason, nil
} else if enableDebugTrace {
reason, err := cl.DebugTraceTransaction(ctx, txHash)
if err != nil {
// TODO: use more proper logger
logger := log.GetLogger().WithModule("ethereum.chain")
logger.Error("failed to call debug_traceTransaction", err)
}
return &r.Receipt, reason, nil
return nil, ethereum.NotFound
} else {
// TODO: use more proper logger
logger := log.GetLogger().WithModule("ethereum.chain")
logger.Info("tx execution failed but the reason couldn't be obtained", "tx_hash", txHash.Hex())
return &r.Receipt, "", nil
return r, nil
}
}

func (cl *ETHClient) WaitForReceiptAndGet(ctx context.Context, txHash common.Hash, enableDebugTrace bool) (*gethtypes.Receipt, string, error) {
var receipt *gethtypes.Receipt
var revertReason string
func (cl *ETHClient) WaitForReceiptAndGet(ctx context.Context, txHash common.Hash) (*Receipt, error) {
var receipt *Receipt
err := retry.Do(
func() error {
rc, reason, err := cl.GetTransactionReceipt(ctx, txHash, enableDebugTrace)
rc, err := cl.GetTransactionReceipt(ctx, txHash)
if err != nil {
return err
}
receipt = rc
revertReason = reason
return nil
},
cl.option.retryOpts...,
)
if err != nil {
return nil, "", err
return nil, err
}
return receipt, revertReason, nil
return receipt, nil
}

func (cl *ETHClient) DebugTraceTransaction(ctx context.Context, txHash common.Hash) (string, error) {
var result *callFrame
if err := cl.Raw().CallContext(ctx, &result, "debug_traceTransaction", txHash, map[string]string{"tracer": "callTracer"}); err != nil {
return "", err
}
revertReason, err := searchRevertReason(result)
if err != nil {
return "", err
}
return revertReason, nil
func (cl *ETHClient) DebugTraceTransaction(ctx context.Context, txHash common.Hash) (CallFrame, error) {
var callFrame CallFrame
err := cl.Raw().CallContext(ctx, &callFrame, "debug_traceTransaction", txHash, map[string]string{"tracer": "callTracer"})
return callFrame, err
}

type Receipt struct {
Expand All @@ -137,35 +107,14 @@ func (rc Receipt) HasRevertReason() bool {
return len(rc.RevertReason) > 0
}

func (rc Receipt) GetRevertReason() (string, error) {
return parseRevertReason(rc.RevertReason)
}

// A format of revertReason is:
// 4byte: Function selector for Error(string)
// 32byte: Data offset
// 32byte: String length
// Remains: String Data
func parseRevertReason(bz []byte) (string, error) {
if l := len(bz); l == 0 {
return "", nil
} else if l < 68 {
return "", fmt.Errorf("invalid length")
}

size := &big.Int{}
size.SetBytes(bz[36:68])
return string(bz[68 : 68+size.Int64()]), nil
}

type callLog struct {
type CallLog struct {
Address common.Address `json:"address"`
Topics []common.Hash `json:"topics"`
Data hexutil.Bytes `json:"data"`
}

// see: https://github.com/ethereum/go-ethereum/blob/v1.12.0/eth/tracers/native/call.go#L44-L59
type callFrame struct {
type CallFrame struct {
Type vm.OpCode `json:"-"`
From common.Address `json:"from"`
Gas uint64 `json:"gas"`
Expand All @@ -175,24 +124,70 @@ type callFrame struct {
Output []byte `json:"output,omitempty" rlp:"optional"`
Error string `json:"error,omitempty" rlp:"optional"`
RevertReason string `json:"revertReason,omitempty"`
Calls []callFrame `json:"calls,omitempty" rlp:"optional"`
Logs []callLog `json:"logs,omitempty" rlp:"optional"`
Calls []CallFrame `json:"calls,omitempty" rlp:"optional"`
Logs []CallLog `json:"logs,omitempty" rlp:"optional"`
// Placed at end on purpose. The RLP will be decoded to 0 instead of
// nil if there are non-empty elements after in the struct.
Value *big.Int `json:"value,omitempty" rlp:"optional"`
}

func searchRevertReason(result *callFrame) (string, error) {
if result.RevertReason != "" {
return result.RevertReason, nil
// UnmarshalJSON unmarshals from JSON.
func (c *CallFrame) UnmarshalJSON(input []byte) error {
type callFrame0 struct {
Type *vm.OpCode `json:"-"`
From *common.Address `json:"from"`
Gas *hexutil.Uint64 `json:"gas"`
GasUsed *hexutil.Uint64 `json:"gasUsed"`
To *common.Address `json:"to,omitempty" rlp:"optional"`
Input *hexutil.Bytes `json:"input" rlp:"optional"`
Output *hexutil.Bytes `json:"output,omitempty" rlp:"optional"`
Error *string `json:"error,omitempty" rlp:"optional"`
RevertReason *string `json:"revertReason,omitempty"`
Calls []CallFrame `json:"calls,omitempty" rlp:"optional"`
Logs []CallLog `json:"logs,omitempty" rlp:"optional"`
Value *hexutil.Big `json:"value,omitempty" rlp:"optional"`
}
var dec callFrame0
if err := json.Unmarshal(input, &dec); err != nil {
return err
}
if dec.Type != nil {
c.Type = *dec.Type
}
if dec.From != nil {
c.From = *dec.From
}
if dec.Gas != nil {
c.Gas = uint64(*dec.Gas)
}
if dec.GasUsed != nil {
c.GasUsed = uint64(*dec.GasUsed)
}
if dec.To != nil {
c.To = dec.To
}
if dec.Input != nil {
c.Input = *dec.Input
}
if dec.Output != nil {
c.Output = *dec.Output
}
if dec.Error != nil {
c.Error = *dec.Error
}
if dec.RevertReason != nil {
c.RevertReason = *dec.RevertReason
}
if dec.Calls != nil {
c.Calls = dec.Calls
}
if dec.Logs != nil {
c.Logs = dec.Logs
}
for _, call := range result.Calls {
reason, err := searchRevertReason(&call)
if err == nil {
return reason, nil
}
if dec.Value != nil {
c.Value = (*big.Int)(dec.Value)
}
return "", fmt.Errorf("revert reason not found")
return nil
}

func (cl *ETHClient) EstimateGasFromTx(ctx context.Context, tx *gethtypes.Transaction) (uint64, error) {
Expand Down
35 changes: 0 additions & 35 deletions pkg/client/client_test.go

This file was deleted.

12 changes: 11 additions & 1 deletion pkg/relay/ethereum/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type Chain struct {

signer Signer

errorRepository ErrorRepository

// cache
connectionOpenedConfirmed bool
allowLCFunctions *AllowLCFunctions
Expand Down Expand Up @@ -79,14 +81,22 @@ func NewChain(config ChainConfig) (*Chain, error) {
return nil, fmt.Errorf("failed to build allowLcFunctions: %v", err)
}
}
errorRepository, err := CreateErrorRepository(config.AbiPaths)
if err != nil {
return nil, fmt.Errorf("failed to create error repository: %v", err)
}

return &Chain{
config: config,
client: client,
chainID: id,

ibcHandler: ibcHandler,

signer: signer,
signer: signer,

errorRepository: errorRepository,

allowLCFunctions: alfs,
}, nil
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/relay/ethereum/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ func (c ChainConfig) Validate() error {
}
}
}
for i, path := range c.AbiPaths {
if isEmpty(path) {
errs = append(errs, fmt.Errorf("config attribute \"abi_paths[%d]\" is empty", i))
}
}
return errors.Join(errs...)
}

Expand Down
Loading

0 comments on commit 593dd65

Please sign in to comment.