accounts, internal: resolve revert reason if possible

This commit is contained in:
rjl493456442 2020-04-06 10:44:52 +08:00
parent bc1e1a1de0
commit b0821c0fcc
5 changed files with 94 additions and 7 deletions

View file

@ -24,6 +24,7 @@ import (
"io"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
// The ABI holds information about a contract's context and available
@ -234,3 +235,25 @@ func (abi *ABI) HasFallback() bool {
func (abi *ABI) HasReceive() bool {
return abi.Receive.Type == Receive
}
// revertSelector is a special function selector for revert reason unpacking.
var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4]
// UnpackRevert resolves the abi-encoded revert reason. According to the solidity
// spec https://solidity.readthedocs.io/en/latest/control-structures.html#revert,
// the provided revert reason is abi-encoded as if it were a call to a function
// `Error(string)`. So it's a special tool for it.
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")
}
var reason string
typ, _ := NewType("string", "", nil)
if err := (Arguments{{Type: typ}}).Unpack(&reason, data[4:]); err != nil {
return "", err
}
return reason, nil
}

View file

@ -19,6 +19,7 @@ package abi
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"math/big"
"reflect"
@ -1070,3 +1071,34 @@ func TestUnnamedEventParam(t *testing.T) {
t.Fatalf("Could not find input")
}
}
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},
}
for index, c := range cases {
t.Run(fmt.Sprintf("case %d", index), func(t *testing.T) {
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)
}
})
}
}

View file

@ -25,6 +25,7 @@ import (
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
@ -445,7 +446,12 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs
if result != nil && result.Err != vm.ErrOutOfGas {
errMsg := fmt.Sprintf("always failing transaction (%v)", result.Err)
if len(result.Revert()) > 0 {
ret, err := abi.UnpackRevert(result.Revert())
if err != nil {
errMsg += fmt.Sprintf(" (0x%x)", result.Revert())
} else {
errMsg += fmt.Sprintf(" (%s)", ret)
}
}
return 0, errors.New(errMsg)
}

View file

@ -411,7 +411,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
GasPrice: big.NewInt(0),
Value: nil,
Data: common.Hex2Bytes("d8b98391"),
}, 0, errors.New("always failing transaction (execution reverted) (0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d72657665727420726561736f6e00000000000000000000000000000000000000)")},
}, 0, errors.New("always failing transaction (execution reverted) (revert reason)")},
{"PureRevert", ethereum.CallMsg{
From: addr,

View file

@ -27,6 +27,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/accounts/scwallet"
"github.com/ethereum/go-ethereum/common"
@ -796,7 +797,6 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.Blo
if state == nil || err != nil {
return nil, err
}
// Override the fields of specified contracts before execution.
for addr, account := range overrides {
// Override account nonce.
@ -882,6 +882,23 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr
return result.Return(), nil
}
type estimateGasError struct {
error string // Concrete error type if it's failed to estimate gas usage
vmerr error // Additional field, it's non-nil if the given transaction is invalid
revert string // Additional field, it's non-empty if the transaction is reverted and reason is provided
}
func (e estimateGasError) Error() string {
errMsg := e.error
if e.vmerr != nil {
errMsg += fmt.Sprintf(" (%v)", e.vmerr)
}
if e.revert != "" {
errMsg += fmt.Sprintf(" (%s)", e.revert)
}
return errMsg
}
func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap *big.Int) (hexutil.Uint64, error) {
// Binary search the gas requirement, as it may be higher than the amount used
var (
@ -947,14 +964,23 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
}
if failed {
if result != nil && result.Err != vm.ErrOutOfGas {
errMsg := fmt.Sprintf("always failing transaction (%v)", result.Err)
var revert string
if len(result.Revert()) > 0 {
errMsg += fmt.Sprintf(" (0x%x)", result.Revert())
ret, err := abi.UnpackRevert(result.Revert())
if err != nil {
revert = "0x" + common.Bytes2Hex(result.Revert())
} else {
revert = ret
}
}
return 0, estimateGasError{
error: "always failing transaction",
vmerr: result.Err,
revert: revert,
}
return 0, errors.New(errMsg)
}
// Otherwise, the specified gas cap is too low
return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap)
return 0, estimateGasError{error: fmt.Sprintf("gas required exceeds allowance (%d)", cap)}
}
}
return hexutil.Uint64(hi), nil