internal/ethapi: return revert reason for eth_call (#21083)

* internal/ethapi: return revert reason for eth_call

* internal/ethapi: moved revert reason logic to doCall

* accounts/abi/bind/backends: added revert reason logic to simulated backend

* internal/ethapi: fixed linting error

* internal/ethapi: check if require reason can be unpacked

* internal/ethapi: better error logic

* internal/ethapi: simplify logic

* internal/ethapi: return vmError()

* internal/ethapi: move handling of revert out of docall

* graphql: removed revert logic until spec change

* rpc: internal/ethapi: added custom error types

* graphql: use returndata instead of return

Return() checks if there is an error. If an error is found, we return nil.
For most use cases it can be beneficial to return the output even if there
was an error. This code should be changed anyway once the spec supports
error reasons in graphql responses

* accounts/abi/bind/backends: added tests for revert reason

* internal/ethapi: add errorCode to revert error

* internal/ethapi: add errorCode of 3 to revertError

* internal/ethapi: unified estimateGasErrors, simplified logic

* internal/ethapi: unified handling of errors in DoEstimateGas

* rpc: print error data field

* accounts/abi/bind/backends: unify simulatedBackend and RPC

* internal/ethapi: added binary data to revertError data

* internal/ethapi: refactored unpacking logic into newRevertError

* accounts/abi/bind/backends: fix EstimateGas

* accounts, console, internal, rpc: minor error interface cleanups

* Revert "accounts, console, internal, rpc: minor error interface cleanups"

This reverts commit 2d3ef53c5304e429a04983210a417c1f4e0dafb7.

* re-apply the good parts of 2d3ef53c53

* rpc: add test for returning server error data from client

Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
Co-authored-by: Péter Szilágyi <peterke@gmail.com>
Co-authored-by: Felix Lange <fjl@twurst.com>
This commit is contained in:
Marius van der Wijden 2020-06-08 10:09:49 +02:00 committed by GitHub
parent 88125d8bd0
commit 0b3f3be2b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 321 additions and 132 deletions

View File

@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
@ -344,6 +345,36 @@ func (b *SimulatedBackend) PendingCodeAt(ctx context.Context, contract common.Ad
return b.pendingState.GetCode(contract), nil return b.pendingState.GetCode(contract), nil
} }
func newRevertError(result *core.ExecutionResult) *revertError {
reason, errUnpack := abi.UnpackRevert(result.Revert())
err := errors.New("execution reverted")
if errUnpack == nil {
err = fmt.Errorf("execution reverted: %v", reason)
}
return &revertError{
error: err,
reason: hexutil.Encode(result.Revert()),
}
}
// revertError is an API error that encompassas an EVM revertal 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 revertal.
// 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
}
// CallContract executes a contract call. // CallContract executes a contract call.
func (b *SimulatedBackend) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { func (b *SimulatedBackend) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
b.mu.Lock() b.mu.Lock()
@ -360,7 +391,11 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call ethereum.CallM
if err != nil { if err != nil {
return nil, err return nil, err
} }
return res.Return(), nil // If the result contains a revert reason, try to unpack and return it.
if len(res.Revert()) > 0 {
return nil, newRevertError(res)
}
return res.Return(), res.Err
} }
// PendingCallContract executes a contract call on the pending state. // PendingCallContract executes a contract call on the pending state.
@ -373,7 +408,11 @@ func (b *SimulatedBackend) PendingCallContract(ctx context.Context, call ethereu
if err != nil { if err != nil {
return nil, err return nil, err
} }
return res.Return(), nil // If the result contains a revert reason, try to unpack and return it.
if len(res.Revert()) > 0 {
return nil, newRevertError(res)
}
return res.Return(), res.Err
} }
// PendingNonceAt implements PendingStateReader.PendingNonceAt, retrieving // PendingNonceAt implements PendingStateReader.PendingNonceAt, retrieving
@ -472,16 +511,10 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs
} }
if failed { if failed {
if result != nil && result.Err != vm.ErrOutOfGas { if result != nil && result.Err != vm.ErrOutOfGas {
errMsg := fmt.Sprintf("always failing transaction (%v)", result.Err)
if len(result.Revert()) > 0 { if len(result.Revert()) > 0 {
ret, err := abi.UnpackRevert(result.Revert()) return 0, newRevertError(result)
if err != nil {
errMsg += fmt.Sprintf(" (%#x)", result.Revert())
} else {
errMsg += fmt.Sprintf(" (%s)", ret)
} }
} return 0, result.Err
return 0, errors.New(errMsg)
} }
// Otherwise, the specified gas cap is too low // Otherwise, the specified gas cap is too low
return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap)

View File

@ -21,6 +21,7 @@ import (
"context" "context"
"errors" "errors"
"math/big" "math/big"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -106,14 +107,18 @@ const deployedCode = `60806040526004361061003b576000357c010000000000000000000000
// expected return value contains "hello world" // expected return value contains "hello world"
var expectedReturn = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} var expectedReturn = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
func simTestBackend(testAddr common.Address) *SimulatedBackend {
return NewSimulatedBackend(
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
}
func TestNewSimulatedBackend(t *testing.T) { func TestNewSimulatedBackend(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
expectedBal := big.NewInt(10000000000) expectedBal := big.NewInt(10000000000)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: expectedBal},
}, 10000000,
)
defer sim.Close() defer sim.Close()
if sim.config != params.AllEthashProtocolChanges { if sim.config != params.AllEthashProtocolChanges {
@ -152,11 +157,7 @@ func TestSimulatedBackend_AdjustTime(t *testing.T) {
func TestSimulatedBackend_BalanceAt(t *testing.T) { func TestSimulatedBackend_BalanceAt(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
expectedBal := big.NewInt(10000000000) expectedBal := big.NewInt(10000000000)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: expectedBal},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -229,11 +230,7 @@ func TestSimulatedBackend_BlockByNumber(t *testing.T) {
func TestSimulatedBackend_NonceAt(t *testing.T) { func TestSimulatedBackend_NonceAt(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -283,11 +280,7 @@ func TestSimulatedBackend_NonceAt(t *testing.T) {
func TestSimulatedBackend_SendTransaction(t *testing.T) { func TestSimulatedBackend_SendTransaction(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -395,6 +388,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
message ethereum.CallMsg message ethereum.CallMsg
expect uint64 expect uint64
expectError error expectError error
expectData interface{}
}{ }{
{"plain transfer(valid)", ethereum.CallMsg{ {"plain transfer(valid)", ethereum.CallMsg{
From: addr, From: addr,
@ -403,7 +397,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
GasPrice: big.NewInt(0), GasPrice: big.NewInt(0),
Value: big.NewInt(1), Value: big.NewInt(1),
Data: nil, Data: nil,
}, params.TxGas, nil}, }, params.TxGas, nil, nil},
{"plain transfer(invalid)", ethereum.CallMsg{ {"plain transfer(invalid)", ethereum.CallMsg{
From: addr, From: addr,
@ -412,7 +406,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
GasPrice: big.NewInt(0), GasPrice: big.NewInt(0),
Value: big.NewInt(1), Value: big.NewInt(1),
Data: nil, Data: nil,
}, 0, errors.New("always failing transaction (execution reverted)")}, }, 0, errors.New("execution reverted"), nil},
{"Revert", ethereum.CallMsg{ {"Revert", ethereum.CallMsg{
From: addr, From: addr,
@ -421,7 +415,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
GasPrice: big.NewInt(0), GasPrice: big.NewInt(0),
Value: nil, Value: nil,
Data: common.Hex2Bytes("d8b98391"), Data: common.Hex2Bytes("d8b98391"),
}, 0, errors.New("always failing transaction (execution reverted) (revert reason)")}, }, 0, errors.New("execution reverted: revert reason"), "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d72657665727420726561736f6e00000000000000000000000000000000000000"},
{"PureRevert", ethereum.CallMsg{ {"PureRevert", ethereum.CallMsg{
From: addr, From: addr,
@ -430,7 +424,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
GasPrice: big.NewInt(0), GasPrice: big.NewInt(0),
Value: nil, Value: nil,
Data: common.Hex2Bytes("aa8b1d30"), Data: common.Hex2Bytes("aa8b1d30"),
}, 0, errors.New("always failing transaction (execution reverted)")}, }, 0, errors.New("execution reverted"), nil},
{"OOG", ethereum.CallMsg{ {"OOG", ethereum.CallMsg{
From: addr, From: addr,
@ -439,7 +433,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
GasPrice: big.NewInt(0), GasPrice: big.NewInt(0),
Value: nil, Value: nil,
Data: common.Hex2Bytes("50f6fe34"), Data: common.Hex2Bytes("50f6fe34"),
}, 0, errors.New("gas required exceeds allowance (100000)")}, }, 0, errors.New("gas required exceeds allowance (100000)"), nil},
{"Assert", ethereum.CallMsg{ {"Assert", ethereum.CallMsg{
From: addr, From: addr,
@ -448,7 +442,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
GasPrice: big.NewInt(0), GasPrice: big.NewInt(0),
Value: nil, Value: nil,
Data: common.Hex2Bytes("b9b046f9"), Data: common.Hex2Bytes("b9b046f9"),
}, 0, errors.New("always failing transaction (invalid opcode: opcode 0xfe not defined)")}, }, 0, errors.New("invalid opcode: opcode 0xfe not defined"), nil},
{"Valid", ethereum.CallMsg{ {"Valid", ethereum.CallMsg{
From: addr, From: addr,
@ -457,7 +451,7 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
GasPrice: big.NewInt(0), GasPrice: big.NewInt(0),
Value: nil, Value: nil,
Data: common.Hex2Bytes("e09fface"), Data: common.Hex2Bytes("e09fface"),
}, 21275, nil}, }, 21275, nil, nil},
} }
for _, c := range cases { for _, c := range cases {
got, err := sim.EstimateGas(context.Background(), c.message) got, err := sim.EstimateGas(context.Background(), c.message)
@ -468,6 +462,13 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
if c.expectError.Error() != err.Error() { if c.expectError.Error() != err.Error() {
t.Fatalf("Expect error, want %v, got %v", c.expectError, err) t.Fatalf("Expect error, want %v, got %v", c.expectError, err)
} }
if c.expectData != nil {
if err, ok := err.(*revertError); !ok {
t.Fatalf("Expect revert error, got %T", err)
} else if !reflect.DeepEqual(err.ErrorData(), c.expectData) {
t.Fatalf("Error data mismatch, want %v, got %v", c.expectData, err.ErrorData())
}
}
continue continue
} }
if got != c.expect { if got != c.expect {
@ -546,11 +547,7 @@ func TestSimulatedBackend_EstimateGasWithPrice(t *testing.T) {
func TestSimulatedBackend_HeaderByHash(t *testing.T) { func TestSimulatedBackend_HeaderByHash(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -571,11 +568,7 @@ func TestSimulatedBackend_HeaderByHash(t *testing.T) {
func TestSimulatedBackend_HeaderByNumber(t *testing.T) { func TestSimulatedBackend_HeaderByNumber(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -622,11 +615,7 @@ func TestSimulatedBackend_HeaderByNumber(t *testing.T) {
func TestSimulatedBackend_TransactionCount(t *testing.T) { func TestSimulatedBackend_TransactionCount(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
currentBlock, err := sim.BlockByNumber(bgCtx, nil) currentBlock, err := sim.BlockByNumber(bgCtx, nil)
@ -676,11 +665,7 @@ func TestSimulatedBackend_TransactionCount(t *testing.T) {
func TestSimulatedBackend_TransactionInBlock(t *testing.T) { func TestSimulatedBackend_TransactionInBlock(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -743,11 +728,7 @@ func TestSimulatedBackend_TransactionInBlock(t *testing.T) {
func TestSimulatedBackend_PendingNonceAt(t *testing.T) { func TestSimulatedBackend_PendingNonceAt(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -809,11 +790,7 @@ func TestSimulatedBackend_PendingNonceAt(t *testing.T) {
func TestSimulatedBackend_TransactionReceipt(t *testing.T) { func TestSimulatedBackend_TransactionReceipt(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
}, 10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -859,12 +836,7 @@ func TestSimulatedBackend_SuggestGasPrice(t *testing.T) {
func TestSimulatedBackend_PendingCodeAt(t *testing.T) { func TestSimulatedBackend_PendingCodeAt(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
},
10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
code, err := sim.CodeAt(bgCtx, testAddr, nil) code, err := sim.CodeAt(bgCtx, testAddr, nil)
@ -900,12 +872,7 @@ func TestSimulatedBackend_PendingCodeAt(t *testing.T) {
func TestSimulatedBackend_CodeAt(t *testing.T) { func TestSimulatedBackend_CodeAt(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
},
10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
code, err := sim.CodeAt(bgCtx, testAddr, nil) code, err := sim.CodeAt(bgCtx, testAddr, nil)
@ -944,12 +911,7 @@ func TestSimulatedBackend_CodeAt(t *testing.T) {
// receipt{status=1 cgas=23949 bloom=00000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000040200000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 logs=[log: b6818c8064f645cd82d99b59a1a267d6d61117ef [75fd880d39c1daf53b6547ab6cb59451fc6452d27caa90e5b6649dd8293b9eed] 000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158 9ae378b6d4409eada347a5dc0c180f186cb62dc68fcc0f043425eb917335aa28 0 95d429d309bb9d753954195fe2d69bd140b4ae731b9b5b605c34323de162cf00 0]} // receipt{status=1 cgas=23949 bloom=00000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000040200000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 logs=[log: b6818c8064f645cd82d99b59a1a267d6d61117ef [75fd880d39c1daf53b6547ab6cb59451fc6452d27caa90e5b6649dd8293b9eed] 000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158 9ae378b6d4409eada347a5dc0c180f186cb62dc68fcc0f043425eb917335aa28 0 95d429d309bb9d753954195fe2d69bd140b4ae731b9b5b605c34323de162cf00 0]}
func TestSimulatedBackend_PendingAndCallContract(t *testing.T) { func TestSimulatedBackend_PendingAndCallContract(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := NewSimulatedBackend( sim := simTestBackend(testAddr)
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000)},
},
10000000,
)
defer sim.Close() defer sim.Close()
bgCtx := context.Background() bgCtx := context.Background()
@ -965,7 +927,7 @@ func TestSimulatedBackend_PendingAndCallContract(t *testing.T) {
input, err := parsed.Pack("receive", []byte("X")) input, err := parsed.Pack("receive", []byte("X"))
if err != nil { if err != nil {
t.Errorf("could pack receive function on contract: %v", err) t.Errorf("could not pack receive function on contract: %v", err)
} }
// make sure you can call the contract in pending state // make sure you can call the contract in pending state
@ -1005,3 +967,113 @@ func TestSimulatedBackend_PendingAndCallContract(t *testing.T) {
t.Errorf("response from calling contract was expected to be 'hello world' instead received %v", string(res)) t.Errorf("response from calling contract was expected to be 'hello world' instead received %v", string(res))
} }
} }
// This test is based on the following contract:
/*
contract Reverter {
function revertString() public pure{
require(false, "some error");
}
function revertNoString() public pure {
require(false, "");
}
function revertASM() public pure {
assembly {
revert(0x0, 0x0)
}
}
function noRevert() public pure {
assembly {
// Assembles something that looks like require(false, "some error") but is not reverted
mstore(0x0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(0x4, 0x0000000000000000000000000000000000000000000000000000000000000020)
mstore(0x24, 0x000000000000000000000000000000000000000000000000000000000000000a)
mstore(0x44, 0x736f6d65206572726f7200000000000000000000000000000000000000000000)
return(0x0, 0x64)
}
}
}*/
func TestSimulatedBackend_CallContractRevert(t *testing.T) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
sim := simTestBackend(testAddr)
defer sim.Close()
bgCtx := context.Background()
reverterABI := `[{"inputs": [],"name": "noRevert","outputs": [],"stateMutability": "pure","type": "function"},{"inputs": [],"name": "revertASM","outputs": [],"stateMutability": "pure","type": "function"},{"inputs": [],"name": "revertNoString","outputs": [],"stateMutability": "pure","type": "function"},{"inputs": [],"name": "revertString","outputs": [],"stateMutability": "pure","type": "function"}]`
reverterBin := "608060405234801561001057600080fd5b506101d3806100206000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c80634b409e01146100515780639b340e361461005b5780639bd6103714610065578063b7246fc11461006f575b600080fd5b610059610079565b005b6100636100ca565b005b61006d6100cf565b005b610077610145565b005b60006100c8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526000815260200160200191505060405180910390fd5b565b600080fd5b6000610143576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252600a8152602001807f736f6d65206572726f720000000000000000000000000000000000000000000081525060200191505060405180910390fd5b565b7f08c379a0000000000000000000000000000000000000000000000000000000006000526020600452600a6024527f736f6d65206572726f720000000000000000000000000000000000000000000060445260646000f3fea2646970667358221220cdd8af0609ec4996b7360c7c780bad5c735740c64b1fffc3445aa12d37f07cb164736f6c63430006070033"
parsed, err := abi.JSON(strings.NewReader(reverterABI))
if err != nil {
t.Errorf("could not get code at test addr: %v", err)
}
contractAuth := bind.NewKeyedTransactor(testKey)
addr, _, _, err := bind.DeployContract(contractAuth, parsed, common.FromHex(reverterBin), sim)
if err != nil {
t.Errorf("could not deploy contract: %v", err)
}
inputs := make(map[string]interface{}, 3)
inputs["revertASM"] = nil
inputs["revertNoString"] = ""
inputs["revertString"] = "some error"
call := make([]func([]byte) ([]byte, error), 2)
call[0] = func(input []byte) ([]byte, error) {
return sim.PendingCallContract(bgCtx, ethereum.CallMsg{
From: testAddr,
To: &addr,
Data: input,
})
}
call[1] = func(input []byte) ([]byte, error) {
return sim.CallContract(bgCtx, ethereum.CallMsg{
From: testAddr,
To: &addr,
Data: input,
}, nil)
}
// Run pending calls then commit
for _, cl := range call {
for key, val := range inputs {
input, err := parsed.Pack(key)
if err != nil {
t.Errorf("could not pack %v function on contract: %v", key, err)
}
res, err := cl(input)
if err == nil {
t.Errorf("call to %v was not reverted", key)
}
if res != nil {
t.Errorf("result from %v was not nil: %v", key, res)
}
if val != nil {
rerr, ok := err.(*revertError)
if !ok {
t.Errorf("expect revert error")
}
if rerr.Error() != "execution reverted: "+val.(string) {
t.Errorf("error was malformed: got %v want %v", rerr.Error(), val)
}
} else {
// revert(0x0,0x0)
if err.Error() != "execution reverted" {
t.Errorf("error was malformed: got %v want %v", err, "execution reverted")
}
}
}
input, err := parsed.Pack("noRevert")
if err != nil {
t.Errorf("could not pack noRevert function on contract: %v", err)
}
res, err := cl(input)
if err != nil {
t.Error("call to noRevert was reverted")
}
if res == nil {
t.Errorf("result from noRevert was nil")
}
sim.Commit()
}
}

View File

@ -413,9 +413,7 @@ func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
resp.Set("id", req.ID) resp.Set("id", req.ID)
var result json.RawMessage var result json.RawMessage
err = b.client.Call(&result, req.Method, req.Params...) if err = b.client.Call(&result, req.Method, req.Params...); err == nil {
switch err := err.(type) {
case nil:
if result == nil { if result == nil {
// Special case null because it is decoded as an empty // Special case null because it is decoded as an empty
// raw message for some reason. // raw message for some reason.
@ -428,19 +426,24 @@ func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
} }
resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result))) resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result)))
if err != nil { if err != nil {
setError(resp, -32603, err.Error()) setError(resp, -32603, err.Error(), nil)
} else { } else {
resp.Set("result", resultVal) resp.Set("result", resultVal)
} }
} }
case rpc.Error: } else {
setError(resp, err.ErrorCode(), err.Error()) code := -32603
default: var data interface{}
setError(resp, -32603, err.Error()) if err, ok := err.(rpc.Error); ok {
code = err.ErrorCode()
}
if err, ok := err.(rpc.DataError); ok {
data = err.ErrorData()
}
setError(resp, code, err.Error(), data)
} }
resps = append(resps, resp) resps = append(resps, resp)
} }
// Return the responses either to the callback (if supplied) // Return the responses either to the callback (if supplied)
// or directly as the return value. // or directly as the return value.
var result goja.Value var result goja.Value
@ -456,8 +459,14 @@ func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
return result, nil return result, nil
} }
func setError(resp *goja.Object, code int, msg string) { func setError(resp *goja.Object, code int, msg string, data interface{}) {
resp.Set("error", map[string]interface{}{"code": code, "message": msg}) err := make(map[string]interface{})
err["code"] = code
err["message"] = msg
if data != nil {
err["data"] = data
}
resp.Set("error", err)
} }
// isNumber returns true if input value is a JS number. // isNumber returns true if input value is a JS number.

View File

@ -811,8 +811,9 @@ func (b *Block) Call(ctx context.Context, args struct {
if result.Failed() { if result.Failed() {
status = 0 status = 0
} }
return &CallResult{ return &CallResult{
data: result.Return(), data: result.ReturnData,
gasUsed: hexutil.Uint64(result.UsedGas), gasUsed: hexutil.Uint64(result.UsedGas),
status: status, status: status,
}, nil }, nil
@ -880,8 +881,9 @@ func (p *Pending) Call(ctx context.Context, args struct {
if result.Failed() { if result.Failed() {
status = 0 status = 0
} }
return &CallResult{ return &CallResult{
data: result.Return(), data: result.ReturnData,
gasUsed: hexutil.Uint64(result.UsedGas), gasUsed: hexutil.Uint64(result.UsedGas),
status: status, status: status,
}, nil }, nil

View File

@ -864,6 +864,36 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.Blo
return result, err return result, err
} }
func newRevertError(result *core.ExecutionResult) *revertError {
reason, errUnpack := abi.UnpackRevert(result.Revert())
err := errors.New("execution reverted")
if errUnpack == nil {
err = fmt.Errorf("execution reverted: %v", reason)
}
return &revertError{
error: err,
reason: hexutil.Encode(result.Revert()),
}
}
// revertError is an API error that encompassas an EVM revertal 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 revertal.
// 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
}
// Call executes the given transaction on the state for the given block number. // Call executes the given transaction on the state for the given block number.
// //
// Additionally, the caller can specify a batch of contract for fields overriding. // Additionally, the caller can specify a batch of contract for fields overriding.
@ -879,24 +909,11 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr
if err != nil { if err != nil {
return nil, err return nil, err
} }
return result.Return(), nil // If the result contains a revert reason, try to unpack and return it.
} if len(result.Revert()) > 0 {
return nil, newRevertError(result)
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 != "" { return result.Return(), result.Err
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) { func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap *big.Int) (hexutil.Uint64, error) {
@ -991,23 +1008,13 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
} }
if failed { if failed {
if result != nil && result.Err != vm.ErrOutOfGas { if result != nil && result.Err != vm.ErrOutOfGas {
var revert string
if len(result.Revert()) > 0 { if len(result.Revert()) > 0 {
ret, err := abi.UnpackRevert(result.Revert()) return 0, newRevertError(result)
if err != nil {
revert = hexutil.Encode(result.Revert())
} else {
revert = ret
}
}
return 0, estimateGasError{
error: "always failing transaction",
vmerr: result.Err,
revert: revert,
} }
return 0, result.Err
} }
// Otherwise, the specified gas cap is too low // Otherwise, the specified gas cap is too low
return 0, estimateGasError{error: fmt.Sprintf("gas required exceeds allowance (%d)", cap)} return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap)
} }
} }
return hexutil.Uint64(hi), nil return hexutil.Uint64(hi), nil

View File

@ -66,6 +66,33 @@ func TestClientResponseType(t *testing.T) {
} }
} }
// This test checks that server-returned errors with code and data come out of Client.Call.
func TestClientErrorData(t *testing.T) {
server := newTestServer()
defer server.Stop()
client := DialInProc(server)
defer client.Close()
var resp interface{}
err := client.Call(&resp, "test_returnError")
if err == nil {
t.Fatal("expected error")
}
// Check code.
if e, ok := err.(Error); !ok {
t.Fatalf("client did not return rpc.Error, got %#v", e)
} else if e.ErrorCode() != (testError{}.ErrorCode()) {
t.Fatalf("wrong error code %d, want %d", e.ErrorCode(), testError{}.ErrorCode())
}
// Check data.
if e, ok := err.(DataError); !ok {
t.Fatalf("client did not return rpc.DataError, got %#v", e)
} else if e.ErrorData() != (testError{}.ErrorData()) {
t.Fatalf("wrong error data %#v, want %#v", e.ErrorData(), testError{}.ErrorData())
}
}
func TestClientBatchRequest(t *testing.T) { func TestClientBatchRequest(t *testing.T) {
server := newTestServer() server := newTestServer()
defer server.Stop() defer server.Stop()

View File

@ -18,6 +18,15 @@ package rpc
import "fmt" import "fmt"
var (
_ Error = new(methodNotFoundError)
_ Error = new(subscriptionNotFoundError)
_ Error = new(parseError)
_ Error = new(invalidRequestError)
_ Error = new(invalidMessageError)
_ Error = new(invalidParamsError)
)
const defaultErrorCode = -32000 const defaultErrorCode = -32000
type methodNotFoundError struct{ method string } type methodNotFoundError struct{ method string }

View File

@ -296,10 +296,16 @@ func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMess
return nil return nil
case msg.isCall(): case msg.isCall():
resp := h.handleCall(ctx, msg) resp := h.handleCall(ctx, msg)
var ctx []interface{}
ctx = append(ctx, "reqid", idForLog{msg.ID}, "t", time.Since(start))
if resp.Error != nil { if resp.Error != nil {
h.log.Warn("Served "+msg.Method, "reqid", idForLog{msg.ID}, "t", time.Since(start), "err", resp.Error.Message) ctx = append(ctx, "err", resp.Error.Message)
if resp.Error.Data != nil {
ctx = append(ctx, "errdata", resp.Error.Data)
}
h.log.Warn("Served "+msg.Method, ctx...)
} else { } else {
h.log.Debug("Served "+msg.Method, "reqid", idForLog{msg.ID}, "t", time.Since(start)) h.log.Debug("Served "+msg.Method, ctx...)
} }
return resp return resp
case msg.hasValidID(): case msg.hasValidID():

View File

@ -115,6 +115,10 @@ func errorMessage(err error) *jsonrpcMessage {
if ok { if ok {
msg.Error.Code = ec.ErrorCode() msg.Error.Code = ec.ErrorCode()
} }
de, ok := err.(DataError)
if ok {
msg.Error.Data = de.ErrorData()
}
return msg return msg
} }
@ -135,6 +139,10 @@ func (err *jsonError) ErrorCode() int {
return err.Code return err.Code
} }
func (err *jsonError) ErrorData() interface{} {
return err.Data
}
// Conn is a subset of the methods of net.Conn which are sufficient for ServerCodec. // Conn is a subset of the methods of net.Conn which are sufficient for ServerCodec.
type Conn interface { type Conn interface {
io.ReadWriteCloser io.ReadWriteCloser

View File

@ -45,7 +45,7 @@ func TestServerRegisterName(t *testing.T) {
t.Fatalf("Expected service calc to be registered") t.Fatalf("Expected service calc to be registered")
} }
wantCallbacks := 8 wantCallbacks := 9
if len(svc.callbacks) != wantCallbacks { if len(svc.callbacks) != wantCallbacks {
t.Errorf("Expected %d callbacks for service 'service', got %d", wantCallbacks, len(svc.callbacks)) t.Errorf("Expected %d callbacks for service 'service', got %d", wantCallbacks, len(svc.callbacks))
} }

View File

@ -63,6 +63,12 @@ type echoResult struct {
Args *echoArgs Args *echoArgs
} }
type testError struct{}
func (testError) Error() string { return "testError" }
func (testError) ErrorCode() int { return 444 }
func (testError) ErrorData() interface{} { return "testError data" }
func (s *testService) NoArgsRets() {} func (s *testService) NoArgsRets() {}
func (s *testService) Echo(str string, i int, args *echoArgs) echoResult { func (s *testService) Echo(str string, i int, args *echoArgs) echoResult {
@ -99,6 +105,10 @@ func (s *testService) InvalidRets3() (string, string, error) {
return "", "", nil return "", "", nil
} }
func (s *testService) ReturnError() error {
return testError{}
}
func (s *testService) CallMeBack(ctx context.Context, method string, args []interface{}) (interface{}, error) { func (s *testService) CallMeBack(ctx context.Context, method string, args []interface{}) (interface{}, error) {
c, ok := ClientFromContext(ctx) c, ok := ClientFromContext(ctx)
if !ok { if !ok {

View File

@ -41,6 +41,12 @@ type Error interface {
ErrorCode() int // returns the code ErrorCode() int // returns the code
} }
// A DataError contains some data in addition to the error message.
type DataError interface {
Error() string // returns the message
ErrorData() interface{} // returns the error data
}
// ServerCodec implements reading, parsing and writing RPC messages for the server side of // 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 // a RPC session. Implementations must be go-routine safe since the codec can be called in
// multiple go-routines concurrently. // multiple go-routines concurrently.