Skip to content

Commit 7770e41

Browse files
authored
core: improve contextual information on core errors (ethereum#21869)
A lot of times when we hit 'core' errors, example: invalid tx, the information provided is insufficient. We miss several pieces of information: what account has nonce too high, and what transaction in that block was offending? This PR adds that information, using the new type of wrapped errors. It also adds a testcase which (partly) verifies the output from the errors. The first commit changes all usage of direct equality-checks on core errors, into using errors.Is. The second commit adds contextual information. This wraps most of the core errors with more information, and also wraps it one more time in stateprocessor, to further provide tx index and tx hash, if such a tx is encoutered in a block. The third commit uses the chainmaker to try to generate chains with such errors in them, thus triggering the errors and checking that the generated string meets expectations.
1 parent 62cedb3 commit 7770e41

File tree

8 files changed

+179
-20
lines changed

8 files changed

+179
-20
lines changed

accounts/abi/bind/backends/simulated.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs
479479
b.pendingState.RevertToSnapshot(snapshot)
480480

481481
if err != nil {
482-
if err == core.ErrIntrinsicGas {
482+
if errors.Is(err, core.ErrIntrinsicGas) {
483483
return true, nil, nil // Special case, raise gas limit
484484
}
485485
return true, nil, err // Bail out

core/blockchain_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package core
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"io/ioutil"
2223
"math/big"
@@ -468,7 +469,7 @@ func testBadHashes(t *testing.T, full bool) {
468469

469470
_, err = blockchain.InsertHeaderChain(headers, 1)
470471
}
471-
if err != ErrBlacklistedHash {
472+
if !errors.Is(err, ErrBlacklistedHash) {
472473
t.Errorf("error mismatch: have: %v, want: %v", err, ErrBlacklistedHash)
473474
}
474475
}

core/state_processor.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package core
1818

1919
import (
20+
"fmt"
21+
2022
"github.com/ethereum/go-ethereum/common"
2123
"github.com/ethereum/go-ethereum/consensus"
2224
"github.com/ethereum/go-ethereum/consensus/misc"
@@ -76,7 +78,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
7678
statedb.Prepare(tx.Hash(), block.Hash(), i)
7779
receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, header, tx, usedGas, vmenv)
7880
if err != nil {
79-
return nil, nil, 0, err
81+
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
8082
}
8183
receipts = append(receipts, receipt)
8284
allLogs = append(allLogs, receipt.Logs...)

core/state_processor_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2020 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package core
18+
19+
import (
20+
"math/big"
21+
"testing"
22+
23+
"github.com/ethereum/go-ethereum/common"
24+
"github.com/ethereum/go-ethereum/consensus"
25+
"github.com/ethereum/go-ethereum/consensus/ethash"
26+
"github.com/ethereum/go-ethereum/core/rawdb"
27+
"github.com/ethereum/go-ethereum/core/types"
28+
"github.com/ethereum/go-ethereum/core/vm"
29+
"github.com/ethereum/go-ethereum/crypto"
30+
"github.com/ethereum/go-ethereum/params"
31+
"github.com/ethereum/go-ethereum/trie"
32+
"golang.org/x/crypto/sha3"
33+
)
34+
35+
// TestStateProcessorErrors tests the output from the 'core' errors
36+
// as defined in core/error.go. These errors are generated when the
37+
// blockchain imports bad blocks, meaning blocks which have valid headers but
38+
// contain invalid transactions
39+
func TestStateProcessorErrors(t *testing.T) {
40+
var (
41+
signer = types.HomesteadSigner{}
42+
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
43+
db = rawdb.NewMemoryDatabase()
44+
gspec = &Genesis{
45+
Config: params.TestChainConfig,
46+
}
47+
genesis = gspec.MustCommit(db)
48+
blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil)
49+
)
50+
defer blockchain.Stop()
51+
var makeTx = func(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *types.Transaction {
52+
tx, _ := types.SignTx(types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data), signer, testKey)
53+
return tx
54+
}
55+
for i, tt := range []struct {
56+
txs []*types.Transaction
57+
want string
58+
}{
59+
{
60+
txs: []*types.Transaction{
61+
makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
62+
makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
63+
},
64+
want: "could not apply tx 1 [0x36bfa6d14f1cd35a1be8cc2322982a595fabc0e799f09c1de3bad7bd5b1f7626]: nonce too low: address 0x71562b71999873DB5b286dF957af199Ec94617F7, tx: 0 state: 1",
65+
},
66+
{
67+
txs: []*types.Transaction{
68+
makeTx(100, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
69+
},
70+
want: "could not apply tx 0 [0x51cd272d41ef6011d8138e18bf4043797aca9b713c7d39a97563f9bbe6bdbe6f]: nonce too high: address 0x71562b71999873DB5b286dF957af199Ec94617F7, tx: 100 state: 0",
71+
},
72+
{
73+
txs: []*types.Transaction{
74+
makeTx(0, common.Address{}, big.NewInt(0), 21000000, nil, nil),
75+
},
76+
want: "could not apply tx 0 [0x54c58b530824b0bb84b7a98183f08913b5d74e1cebc368515ef3c65edf8eb56a]: gas limit reached",
77+
},
78+
{
79+
txs: []*types.Transaction{
80+
makeTx(0, common.Address{}, big.NewInt(1), params.TxGas, nil, nil),
81+
},
82+
want: "could not apply tx 0 [0x3094b17498940d92b13baccf356ce8bfd6f221e926abc903d642fa1466c5b50e]: insufficient funds for transfer: address 0x71562b71999873DB5b286dF957af199Ec94617F7",
83+
},
84+
{
85+
txs: []*types.Transaction{
86+
makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(0xffffff), nil),
87+
},
88+
want: "could not apply tx 0 [0xaa3f7d86802b1f364576d9071bf231e31d61b392d306831ac9cf706ff5371ce0]: insufficient funds for gas * price + value: address 0x71562b71999873DB5b286dF957af199Ec94617F7 have 0 want 352321515000",
89+
},
90+
{
91+
txs: []*types.Transaction{
92+
makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
93+
makeTx(1, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
94+
makeTx(2, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
95+
makeTx(3, common.Address{}, big.NewInt(0), params.TxGas-1000, big.NewInt(0), nil),
96+
},
97+
want: "could not apply tx 3 [0x836fab5882205362680e49b311a20646de03b630920f18ec6ee3b111a2cf6835]: intrinsic gas too low: have 20000, want 21000",
98+
},
99+
// The last 'core' error is ErrGasUintOverflow: "gas uint64 overflow", but in order to
100+
// trigger that one, we'd have to allocate a _huge_ chunk of data, such that the
101+
// multiplication len(data) +gas_per_byte overflows uint64. Not testable at the moment
102+
} {
103+
block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs)
104+
_, err := blockchain.InsertChain(types.Blocks{block})
105+
if err == nil {
106+
t.Fatal("block imported without errors")
107+
}
108+
if have, want := err.Error(), tt.want; have != want {
109+
t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want)
110+
}
111+
}
112+
}
113+
114+
// GenerateBadBlock constructs a "block" which contains the transactions. The transactions are not expected to be
115+
// valid, and no proper post-state can be made. But from the perspective of the blockchain, the block is sufficiently
116+
// valid to be considered for import:
117+
// - valid pow (fake), ancestry, difficulty, gaslimit etc
118+
func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Transactions) *types.Block {
119+
header := &types.Header{
120+
ParentHash: parent.Hash(),
121+
Coinbase: parent.Coinbase(),
122+
Difficulty: engine.CalcDifficulty(&fakeChainReader{params.TestChainConfig}, parent.Time()+10, &types.Header{
123+
Number: parent.Number(),
124+
Time: parent.Time(),
125+
Difficulty: parent.Difficulty(),
126+
UncleHash: parent.UncleHash(),
127+
}),
128+
GasLimit: CalcGasLimit(parent, parent.GasLimit(), parent.GasLimit()),
129+
Number: new(big.Int).Add(parent.Number(), common.Big1),
130+
Time: parent.Time() + 10,
131+
UncleHash: types.EmptyUncleHash,
132+
}
133+
var receipts []*types.Receipt
134+
135+
// The post-state result doesn't need to be correct (this is a bad block), but we do need something there
136+
// Preferably something unique. So let's use a combo of blocknum + txhash
137+
hasher := sha3.NewLegacyKeccak256()
138+
hasher.Write(header.Number.Bytes())
139+
var cumulativeGas uint64
140+
for _, tx := range txs {
141+
txh := tx.Hash()
142+
hasher.Write(txh[:])
143+
receipt := types.NewReceipt(nil, false, cumulativeGas+tx.Gas())
144+
receipt.TxHash = tx.Hash()
145+
receipt.GasUsed = tx.Gas()
146+
receipts = append(receipts, receipt)
147+
cumulativeGas += tx.Gas()
148+
}
149+
header.Root = common.BytesToHash(hasher.Sum(nil))
150+
// Assemble and return the final block for sealing
151+
return types.NewBlock(header, txs, nil, receipts, new(trie.Trie))
152+
}

core/state_transition.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package core
1818

1919
import (
20+
"fmt"
2021
"math"
2122
"math/big"
2223

@@ -174,8 +175,8 @@ func (st *StateTransition) to() common.Address {
174175

175176
func (st *StateTransition) buyGas() error {
176177
mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
177-
if st.state.GetBalance(st.msg.From()).Cmp(mgval) < 0 {
178-
return ErrInsufficientFunds
178+
if have, want := st.state.GetBalance(st.msg.From()), mgval; have.Cmp(want) < 0 {
179+
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From().Hex(), have, want)
179180
}
180181
if err := st.gp.SubGas(st.msg.Gas()); err != nil {
181182
return err
@@ -190,11 +191,13 @@ func (st *StateTransition) buyGas() error {
190191
func (st *StateTransition) preCheck() error {
191192
// Make sure this transaction's nonce is correct.
192193
if st.msg.CheckNonce() {
193-
nonce := st.state.GetNonce(st.msg.From())
194-
if nonce < st.msg.Nonce() {
195-
return ErrNonceTooHigh
196-
} else if nonce > st.msg.Nonce() {
197-
return ErrNonceTooLow
194+
stNonce := st.state.GetNonce(st.msg.From())
195+
if msgNonce := st.msg.Nonce(); stNonce < msgNonce {
196+
return fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooHigh,
197+
st.msg.From().Hex(), msgNonce, stNonce)
198+
} else if stNonce > msgNonce {
199+
return fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooLow,
200+
st.msg.From().Hex(), msgNonce, stNonce)
198201
}
199202
}
200203
return st.buyGas()
@@ -240,13 +243,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
240243
return nil, err
241244
}
242245
if st.gas < gas {
243-
return nil, ErrIntrinsicGas
246+
return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas)
244247
}
245248
st.gas -= gas
246249

247250
// Check clause 6
248251
if msg.Value().Sign() > 0 && !st.evm.Context.CanTransfer(st.state, msg.From(), msg.Value()) {
249-
return nil, ErrInsufficientFundsForTransfer
252+
return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From().Hex())
250253
}
251254
var (
252255
ret []byte

core/tx_pool_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func TestInvalidTransactions(t *testing.T) {
242242
from, _ := deriveSender(tx)
243243

244244
pool.currentState.AddBalance(from, big.NewInt(1))
245-
if err := pool.AddRemote(tx); err != ErrInsufficientFunds {
245+
if err := pool.AddRemote(tx); !errors.Is(err, ErrInsufficientFunds) {
246246
t.Error("expected", ErrInsufficientFunds)
247247
}
248248

@@ -255,7 +255,7 @@ func TestInvalidTransactions(t *testing.T) {
255255
pool.currentState.SetNonce(from, 1)
256256
pool.currentState.AddBalance(from, big.NewInt(0xffffffffffffff))
257257
tx = transaction(0, 100000, key)
258-
if err := pool.AddRemote(tx); err != ErrNonceTooLow {
258+
if err := pool.AddRemote(tx); !errors.Is(err, ErrNonceTooLow) {
259259
t.Error("expected", ErrNonceTooLow)
260260
}
261261

light/lightchain_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package light
1818

1919
import (
2020
"context"
21+
"errors"
2122
"math/big"
2223
"testing"
2324

@@ -321,7 +322,7 @@ func TestBadHeaderHashes(t *testing.T) {
321322
var err error
322323
headers := makeHeaderChainWithDiff(bc.genesisBlock, []int{1, 2, 4}, 10)
323324
core.BadHashes[headers[2].Hash()] = true
324-
if _, err = bc.InsertHeaderChain(headers, 1); err != core.ErrBlacklistedHash {
325+
if _, err = bc.InsertHeaderChain(headers, 1); !errors.Is(err, core.ErrBlacklistedHash) {
325326
t.Errorf("error mismatch: have: %v, want %v", err, core.ErrBlacklistedHash)
326327
}
327328
}

miner/worker.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -793,23 +793,23 @@ func (w *worker) commitTransactions(txs *types.TransactionsByPriceAndNonce, coin
793793
w.current.state.Prepare(tx.Hash(), common.Hash{}, w.current.tcount)
794794

795795
logs, err := w.commitTransaction(tx, coinbase)
796-
switch err {
797-
case core.ErrGasLimitReached:
796+
switch {
797+
case errors.Is(err, core.ErrGasLimitReached):
798798
// Pop the current out-of-gas transaction without shifting in the next from the account
799799
log.Trace("Gas limit exceeded for current block", "sender", from)
800800
txs.Pop()
801801

802-
case core.ErrNonceTooLow:
802+
case errors.Is(err, core.ErrNonceTooLow):
803803
// New head notification data race between the transaction pool and miner, shift
804804
log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
805805
txs.Shift()
806806

807-
case core.ErrNonceTooHigh:
807+
case errors.Is(err, core.ErrNonceTooHigh):
808808
// Reorg notification data race between the transaction pool and miner, skip account =
809809
log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce())
810810
txs.Pop()
811811

812-
case nil:
812+
case errors.Is(err, nil):
813813
// Everything ok, collect the logs and shift in the next transaction from the same account
814814
coalescedLogs = append(coalescedLogs, logs...)
815815
w.current.tcount++

0 commit comments

Comments
 (0)