From 784dbd8c4114a0ea47aa23278631b4b571b1b905 Mon Sep 17 00:00:00 2001 From: Eric <92196970+ericlee42@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:41:06 +0800 Subject: [PATCH] l2geth/rpc: support solidity custom errors (#97) * l2geth/rpc: support solidity custom errors * Add execution reverted error --- l2geth/accounts/abi/abi.go | 55 ++++++++++++++++++++++++++------ l2geth/accounts/abi/abi_test.go | 36 +++++++++++++++++++++ l2geth/internal/ethapi/api.go | 17 ++-------- l2geth/internal/ethapi/errors.go | 41 ++++++++++++++++++++++++ l2geth/rpc/json.go | 4 +++ l2geth/rpc/types.go | 5 +++ 6 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 l2geth/internal/ethapi/errors.go diff --git a/l2geth/accounts/abi/abi.go b/l2geth/accounts/abi/abi.go index a5a44aa32cd1..0171bc15637e 100755 --- a/l2geth/accounts/abi/abi.go +++ b/l2geth/accounts/abi/abi.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "io" + "math/big" "github.com/ethereum-optimism/optimism/l2geth/common" "github.com/ethereum-optimism/optimism/l2geth/crypto" @@ -189,13 +190,34 @@ func (abi *ABI) EventByID(topic common.Hash) (*Event, error) { return nil, fmt.Errorf("no event with id: %#x", topic.Hex()) } +// panicReasons map is for readable panic codes +// see this linkage for the details +// https://docs.soliditylang.org/en/v0.8.21/control-structures.html#panic-via-assert-and-error-via-require +// the reason string list is copied from ether.js +// https://github.com/ethers-io/ethers.js/blob/fa3a883ff7c88611ce766f58bdd4b8ac90814470/src.ts/abi/interface.ts#L207-L218 +var panicReasons = map[uint64]string{ + 0x00: "generic panic", + 0x01: "assert(false)", + 0x11: "arithmetic underflow or overflow", + 0x12: "division or modulo by zero", + 0x21: "enum overflow", + 0x22: "invalid encoded storage byte array accessed", + 0x31: "out-of-bounds array access; popping on an empty array", + 0x32: "out-of-bounds access of an array or bytesN", + 0x41: "out of memory", + 0x51: "uninitialized function", +} + // UsingOVM // Both RevertSelector and UnpackRevert were pulled from upstream // geth as they were not present in the version of geth that this // codebase was forked from. These are useful for displaying revert // messages to users when they use `eth_call` // RevertSelector is a special function selector for revert reason unpacking. -var RevertSelector = crypto.Keccak256([]byte("Error(string)"))[:4] +var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4] + +// panicSelector is a special function selector for panic reason unpacking. +var panicSelector = crypto.Keccak256([]byte("Panic(uint256)"))[:4] // UnpackRevert resolves the abi-encoded revert reason. According to the solidity // docs https://docs.soliditylang.org/en/v0.8.4/control-structures.html#revert, @@ -205,13 +227,28 @@ func UnpackRevert(data []byte) (string, error) { if len(data) < 4 { return "", errors.New("invalid data for unpacking") } - if !bytes.Equal(data[:4], RevertSelector) { - return "", errors.New("invalid data for unpacking") - } - typ, _ := NewType("string", "", nil) - unpacked, err := (Arguments{{Type: typ}}).UnpackValues(data[4:]) - if err != nil { - return "", err + switch { + case bytes.Equal(data[:4], revertSelector): + typ, _ := NewType("string", "", nil) + unpacked, err := (Arguments{{Type: typ}}).UnpackValues(data[4:]) + if err != nil { + return "", err + } + return unpacked[0].(string), nil + case bytes.Equal(data[:4], panicSelector): + typ, _ := NewType("uint256", "", nil) + unpacked, err := (Arguments{{Type: typ}}).UnpackValues(data[4:]) + if err != nil { + return "", err + } + pCode := unpacked[0].(*big.Int) + if pCode.IsUint64() { + if reason, ok := panicReasons[pCode.Uint64()]; ok { + return reason, nil + } + } + return fmt.Sprintf("unknown panic code: %#x", pCode), nil } - return unpacked[0].(string), nil + + return "", errors.New("invalid data for unpacking") } diff --git a/l2geth/accounts/abi/abi_test.go b/l2geth/accounts/abi/abi_test.go index 7ab509d8fb4f..37fad84607f0 100755 --- a/l2geth/accounts/abi/abi_test.go +++ b/l2geth/accounts/abi/abi_test.go @@ -19,6 +19,7 @@ package abi import ( "bytes" "encoding/hex" + "errors" "fmt" "math/big" "reflect" @@ -1054,3 +1055,38 @@ func TestDoubleDuplicateMethodNames(t *testing.T) { t.Fatalf("Should not have found extra method") } } + +func TestUnpackRevert(t *testing.T) { + t.Parallel() + + var cases = []struct { + input string + expect string + expectErr error + }{ + {"", "", errors.New("invalid data for unpacking")}, + {"08c379a1", "", errors.New("invalid data for unpacking")}, + {"08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d72657665727420726561736f6e00000000000000000000000000000000000000", "revert reason", nil}, + {"4e487b710000000000000000000000000000000000000000000000000000000000000000", "generic panic", nil}, + {"4e487b7100000000000000000000000000000000000000000000000000000000000000ff", "unknown panic code: 0xff", nil}, + } + for index, c := range cases { + index, c := index, c + t.Run(fmt.Sprintf("case %d", index), func(t *testing.T) { + t.Parallel() + got, err := UnpackRevert(common.Hex2Bytes(c.input)) + if c.expectErr != nil { + if err == nil { + t.Fatalf("Expected non-nil error") + } + if err.Error() != c.expectErr.Error() { + t.Fatalf("Expected error mismatch, want %v, got %v", c.expectErr, err) + } + return + } + if c.expect != got { + t.Fatalf("Output mismatch, want %v, got %v", c.expect, got) + } + }) + } +} diff --git a/l2geth/internal/ethapi/api.go b/l2geth/internal/ethapi/api.go index db801424c1b9..1c42b1d04884 100755 --- a/l2geth/internal/ethapi/api.go +++ b/l2geth/internal/ethapi/api.go @@ -27,7 +27,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/ethereum-optimism/optimism/l2geth/accounts" - "github.com/ethereum-optimism/optimism/l2geth/accounts/abi" "github.com/ethereum-optimism/optimism/l2geth/accounts/keystore" "github.com/ethereum-optimism/optimism/l2geth/accounts/scwallet" "github.com/ethereum-optimism/optimism/l2geth/common" @@ -978,12 +977,7 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr return nil, err } if failed { - reason, errUnpack := abi.UnpackRevert(result) - err := errors.New("execution reverted") - if errUnpack == nil { - err = fmt.Errorf("execution reverted: %v", reason) - } - return (hexutil.Bytes)(result), err + return (hexutil.Bytes)(result), newRevertError(result) } return (hexutil.Bytes)(result), err } @@ -1092,13 +1086,8 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash if hi == cap { ok, res, _ := executable(hi) if !ok { - if len(res) >= 4 && bytes.Equal(res[:4], abi.RevertSelector) { - reason, errUnpack := abi.UnpackRevert(res) - err := errors.New("execution reverted") - if errUnpack == nil { - err = fmt.Errorf("execution reverted: %v", reason) - } - return 0, err + if len(res) >= 4 { + return 0, newRevertError(res) } return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) } diff --git a/l2geth/internal/ethapi/errors.go b/l2geth/internal/ethapi/errors.go new file mode 100644 index 000000000000..02ab615c2187 --- /dev/null +++ b/l2geth/internal/ethapi/errors.go @@ -0,0 +1,41 @@ +package ethapi + +import ( + "errors" + "fmt" + + "github.com/ethereum-optimism/optimism/l2geth/accounts/abi" + "github.com/ethereum-optimism/optimism/l2geth/common/hexutil" +) + +// revertError is an API error that encompasses an EVM revert with JSON error +// code and a binary data blob. +type revertError struct { + error + reason string // revert reason hex encoded +} + +// ErrorCode returns the JSON error code for a revert. +// See: https://github.com/ethereum/wiki/wiki/JSON-RPC-Error-Codes-Improvement-Proposal +func (e *revertError) ErrorCode() int { + return 3 +} + +// ErrorData returns the hex encoded revert reason. +func (e *revertError) ErrorData() interface{} { + return e.reason +} + +// newRevertError creates a revertError instance with the provided revert data. +func newRevertError(revert []byte) *revertError { + err := errors.New("execution reverted") + + reason, errUnpack := abi.UnpackRevert(revert) + if errUnpack == nil { + err = fmt.Errorf("execution reverted: %s", reason) + } + return &revertError{ + error: err, + reason: hexutil.Encode(revert), + } +} diff --git a/l2geth/rpc/json.go b/l2geth/rpc/json.go index 61631a3d7660..1d139620d6a9 100755 --- a/l2geth/rpc/json.go +++ b/l2geth/rpc/json.go @@ -115,6 +115,10 @@ func errorMessage(err error) *jsonrpcMessage { if ok { msg.Error.Code = ec.ErrorCode() } + de, ok := err.(ErrorWithData) + if ok { + msg.Error.Data = de.ErrorData() + } return msg } diff --git a/l2geth/rpc/types.go b/l2geth/rpc/types.go index 6f6b047c3223..aab335b589a4 100755 --- a/l2geth/rpc/types.go +++ b/l2geth/rpc/types.go @@ -41,6 +41,11 @@ type Error interface { ErrorCode() int // returns the code } +type ErrorWithData interface { + Error() string + ErrorData() interface{} +} + // ServerCodec implements reading, parsing and writing RPC messages for the server side of // a RPC session. Implementations must be go-routine safe since the codec can be called in // multiple go-routines concurrently.