Skip to content

Commit

Permalink
overhaul ErrorRepository
Browse files Browse the repository at this point in the history
Signed-off-by: Masanori Yoshida <[email protected]>
  • Loading branch information
siburu committed Apr 24, 2024
1 parent dc2faf6 commit d371625
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 154 deletions.
11 changes: 10 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,21 @@ 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
149 changes: 51 additions & 98 deletions pkg/relay/ethereum/error_parse.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ethereum

import (
"bytes"
"encoding/json"
"fmt"
"os"
Expand All @@ -10,26 +11,11 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
)

type Artifact struct {
ABI []interface{} `json:"abi"`
}

type ErrorsRepository struct {
errs map[[4]byte]abi.Error
}
type ErrorRepository map[[4]byte]abi.Error

var erepo *ErrorsRepository

func GetErepo(abiPaths []string) (*ErrorsRepository, error) {
var erepoErr error
if erepo == nil {
erepo, erepoErr = CreateErepo(abiPaths)
}
return erepo, erepoErr
}
func CreateErrorRepository(abiPaths []string) (ErrorRepository, error) {
var errABIs []abi.Error

func CreateErepo(abiPaths []string) (*ErrorsRepository, error) {
var abiErrors []abi.Error
for _, dir := range abiPaths {
if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
Expand All @@ -40,121 +26,88 @@ func CreateErepo(abiPaths []string) (*ErrorsRepository, error) {
if err != nil {
return err
}
var artifact Artifact
var abiData []byte
if err := json.Unmarshal(data, &artifact); err != nil {
abiData = data
} else {
abiData, err = json.Marshal(artifact.ABI)
if err != nil {
return err
}
}
abiABI, err := abi.JSON(strings.NewReader(string(abiData)))
if err != nil {
return err
}
for _, error := range abiABI.Errors {
abiErrors = append(abiErrors, error)

contractABI, err := abi.JSON(bytes.NewReader(data))
for _, error := range contractABI.Errors {
errABIs = append(errABIs, error)
}
}
return nil
}); err != nil {
return nil, err
}
}
return NewErrorsRepository(abiErrors)

return NewErrorRepository(errABIs)
}

func NewErrorsRepository(customErrors []abi.Error) (*ErrorsRepository, error) {
defaultErrs, err := defaultErrors()
if err != nil {
return nil, err
}
customErrors = append(customErrors, defaultErrs...)
er := ErrorsRepository{
errs: make(map[[4]byte]abi.Error),
}
for _, e := range customErrors {
e := abi.NewError(e.Name, e.Inputs)
if err := er.Add(e); err != nil {
func NewErrorRepository(errABIs []abi.Error) (ErrorRepository, error) {
repo := make(ErrorRepository)
for _, errABI := range errABIs {
if err := repo.Add(errABI); err != nil {
return nil, err
}
}
return &er, nil

return repo, nil
}

func (r *ErrorsRepository) Add(e0 abi.Error) error {
func (r ErrorRepository) Add(errABI abi.Error) error {
var sel [4]byte
copy(sel[:], e0.ID[:4])
if e1, ok := r.errs[sel]; ok {
if e1.Sig == e0.Sig {
copy(sel[:], errABI.ID[:4])
if existingErrABI, ok := r[sel]; ok {
if existingErrABI.Sig == errABI.Sig {
return nil
}
return fmt.Errorf("error selector collision: sel=%x e0=%v e1=%v", sel, e0, e1)
return fmt.Errorf("error selector collision: selector=%x, newErrABI=%v, existingErrABI=%v", sel, errABI, existingErrABI)
}
r.errs[sel] = e0
r[sel] = errABI
return nil
}

func (r *ErrorsRepository) GetError(sel [4]byte) (abi.Error, bool) {
e, ok := r.errs[sel]
return e, ok
}

func (r *ErrorsRepository) ParseError(bz []byte) (string, interface{}, error) {
if len(bz) < 4 {
return "", nil, fmt.Errorf("invalid error data: %v", bz)
func (r ErrorRepository) Get(errorData []byte) (abi.Error, error) {
if len(errorData) < 4 {
return abi.Error{}, fmt.Errorf("the size of error data is less than 4 bytes: errorData=%x", errorData)
}
var sel [4]byte
copy(sel[:], bz[:4])
e, ok := r.GetError(sel)
copy(sel[:], errorData[:4])
errABI, ok := r[sel]
if !ok {
return "", nil, fmt.Errorf("unknown error: sel=%x", sel)
return abi.Error{}, fmt.Errorf("error ABI not found")
}
v, err := e.Unpack(bz)
return e.Sig, v, err
return errABI, nil
}

func defaultErrors() ([]abi.Error, error) {
strT, err := abi.NewType("string", "", nil)
if err != nil {
return nil, err
func errorToJSON(errVal interface{}, errABI abi.Error) (string, error) {
m := make(map[string]interface{})
for i, v := range errVal.([]interface{}) {
m[errABI.Inputs[i].Name] = v
}
uintT, err := abi.NewType("uint256", "", nil)
bz, err := json.Marshal(m)
if err != nil {
return nil, err
return "", err
}
return string(bz), nil
}

errors := []abi.Error{
{
Name: "Error",
Inputs: []abi.Argument{
{
Type: strT,
},
},
},
{
Name: "Panic",
Inputs: []abi.Argument{
{
Type: uintT,
},
},
},
func (r ErrorRepository) ParseError(errorData []byte) (string, error) {
// handle Error(string) and Panic(uint256)
if revertReason, err := abi.UnpackRevert(errorData); err == nil {
return revertReason, nil
}
return errors, nil
}

func GetRevertReason(revertReason []byte, abiPaths []string) string {
erepo, err := GetErepo(abiPaths)
// handle custom error
errABI, err := r.Get(errorData)
if err != nil {
return "", fmt.Errorf("failed to find error ABI: %v", err)
}
errVal, err := errABI.Unpack(errorData)
if err != nil {
return fmt.Sprintf("GetErepo err=%v", err)
return "", fmt.Errorf("failed to unpack error: %v", err)
}
sig, args, err := erepo.ParseError(revertReason)
errStr, err := errorToJSON(errVal, errABI)
if err != nil {
return fmt.Sprintf("raw-revert-reason=\"%x\" parse-err=\"%v\"", revertReason, err)
return "", fmt.Errorf("failed to marshal error inputs into JSON: %v", err)
}
return fmt.Sprintf("revert-reason=\"%v\" args=\"%v\"", sig, args)
return fmt.Sprintf("%s%s", errABI.Name, errStr), nil
}
72 changes: 19 additions & 53 deletions pkg/relay/ethereum/error_parse_test.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
package ethereum

import (
"encoding/hex"
"math/big"
"strings"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)

func TestRevertReasonParserDefault(t *testing.T) {
customErrors := []abi.Error{}

erepo, err := NewErrorsRepository(customErrors)
erepo, err := NewErrorRepository(customErrors)
require.NoError(t, err)
s, args, err := erepo.ParseError(
hexToBytes("0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e000000000000"),

revertReason, err := erepo.ParseError(
common.FromHex("0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e000000000000"),
)
require.NoError(t, err)
require.Equal(t, "Error(string)", s)
require.Equal(t, []interface{}{"Not enough Ether provided."}, args)
require.Equal(t, "Not enough Ether provided.", revertReason)
}

func TestRevertReasonParserAddedCustomError(t *testing.T) {
Expand All @@ -29,24 +27,17 @@ func TestRevertReasonParserAddedCustomError(t *testing.T) {
panic(err)
}
customErrors := []abi.Error{
{
Name: "AppError",
Inputs: []abi.Argument{
{
Type: uintT,
},
},
},
abi.NewError("AppError", abi.Arguments{{Name: "x", Type: uintT}}),
}

erepo, err := NewErrorsRepository(customErrors)
erepo, err := NewErrorRepository(customErrors)
require.NoError(t, err)
s, args, err := erepo.ParseError(
hexToBytes("0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e000000000000"),

revertReason, err := erepo.ParseError(
common.FromHex("0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e000000000000"),
)
require.NoError(t, err)
require.Equal(t, "Error(string)", s)
require.Equal(t, []interface{}{"Not enough Ether provided."}, args)
require.Equal(t, "Not enough Ether provided.", revertReason)
}

func TestRevertReasonParserCustomError(t *testing.T) {
Expand All @@ -59,41 +50,16 @@ func TestRevertReasonParserCustomError(t *testing.T) {
panic(err)
}
customErrors := []abi.Error{
{
Name: "AppError",
Inputs: []abi.Argument{
{
Type: strT,
},
},
},
{
Name: "InsufficientBalance",
Inputs: []abi.Argument{
{
Type: uintT,
},
{
Type: uintT,
},
},
},
abi.NewError("AppError", abi.Arguments{{Name: "x", Type: strT}}),
abi.NewError("InsufficientBalance", abi.Arguments{{Name: "y", Type: uintT}, {Name: "z", Type: uintT}}),
}

erepo, err := NewErrorsRepository(customErrors)
erepo, err := NewErrorRepository(customErrors)
require.NoError(t, err)
s, args, err := erepo.ParseError(
hexToBytes("0xcf47918100000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009"),

revertReason, err := erepo.ParseError(
common.FromHex("0xcf47918100000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009"),
)
require.NoError(t, err)
require.Equal(t, "InsufficientBalance(uint256,uint256)", s)
require.Equal(t, []interface{}{big.NewInt(7), big.NewInt(9)}, args)
}

func hexToBytes(s string) []byte {
reason, err := hex.DecodeString(strings.TrimPrefix(s, "0x"))
if err != nil {
panic(err)
}
return reason
require.Equal(t, `InsufficientBalance{"y":7,"z":9}`, revertReason)
}
10 changes: 8 additions & 2 deletions pkg/relay/ethereum/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,10 @@ func (c *Chain) getRevertReasonFromReceipt(ctx context.Context, receipt *client.
return "", fmt.Errorf("no way to get revert reason")
}

revertReason := GetRevertReason(errorData, c.config.AbiPaths)
revertReason, err := c.errorRepository.ParseError(errorData)
if err != nil {
return "", fmt.Errorf("failed to parse error: %v", err)
}
return revertReason, nil
}

Expand All @@ -389,7 +392,10 @@ func (c *Chain) getRevertReasonFromEstimateGas(err error) (string, error) {
return "", fmt.Errorf("eth_estimateGas failed without error data")
} else {
errorData := common.FromHex(de.ErrorData().(string))
revertReason := GetRevertReason(errorData, c.config.AbiPaths)
revertReason, err := c.errorRepository.ParseError(errorData)
if err != nil {
return "", fmt.Errorf("failed to parse error: %v", err)
}
return revertReason, nil
}
}

0 comments on commit d371625

Please sign in to comment.