Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for logging custom errors on tx execution revert in not only real tx execution but also gas estimation #39

Merged
merged 13 commits into from
Apr 26, 2024
Merged
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) {
bluele marked this conversation as resolved.
Show resolved Hide resolved
errs = append(errs, fmt.Errorf("config attribute \"abi_paths[%d]\" is empty", i))
}
}
return errors.Join(errs...)
}

Expand Down
Loading