Skip to content

Commit 7852d12

Browse files
committed
core/txpool: reject stale transaction for local tracking
1 parent fd4049d commit 7852d12

File tree

2 files changed

+227
-3
lines changed

2 files changed

+227
-3
lines changed

core/txpool/locals/tx_tracker_test.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2025 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 locals
18+
19+
import (
20+
"errors"
21+
"math/big"
22+
"testing"
23+
"time"
24+
25+
"github.com/ethereum/go-ethereum/common"
26+
"github.com/ethereum/go-ethereum/consensus/ethash"
27+
"github.com/ethereum/go-ethereum/core"
28+
"github.com/ethereum/go-ethereum/core/rawdb"
29+
"github.com/ethereum/go-ethereum/core/txpool"
30+
"github.com/ethereum/go-ethereum/core/txpool/legacypool"
31+
"github.com/ethereum/go-ethereum/core/types"
32+
"github.com/ethereum/go-ethereum/core/vm"
33+
"github.com/ethereum/go-ethereum/crypto"
34+
"github.com/ethereum/go-ethereum/ethdb"
35+
"github.com/ethereum/go-ethereum/params"
36+
)
37+
38+
var (
39+
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
40+
address = crypto.PubkeyToAddress(key.PublicKey)
41+
funds = big.NewInt(1000000000000000)
42+
gspec = &core.Genesis{
43+
Config: params.TestChainConfig,
44+
Alloc: types.GenesisAlloc{
45+
address: {Balance: funds},
46+
},
47+
BaseFee: big.NewInt(params.InitialBaseFee),
48+
}
49+
signer = types.LatestSigner(gspec.Config)
50+
)
51+
52+
type testEnv struct {
53+
chain *core.BlockChain
54+
pool *txpool.TxPool
55+
tracker *TxTracker
56+
genDb ethdb.Database
57+
}
58+
59+
func newTestEnv(t *testing.T, n int, gasTip uint64, journal string) *testEnv {
60+
genDb, blocks, _ := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), n, func(i int, gen *core.BlockGen) {
61+
tx, err := types.SignTx(types.NewTransaction(gen.TxNonce(address), common.Address{0x00}, big.NewInt(1000), params.TxGas, gen.BaseFee(), nil), signer, key)
62+
if err != nil {
63+
panic(err)
64+
}
65+
gen.AddTx(tx)
66+
})
67+
68+
db := rawdb.NewMemoryDatabase()
69+
chain, _ := core.NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil)
70+
if n, err := chain.InsertChain(blocks); err != nil {
71+
t.Fatalf("failed to process block %d: %v", n, err)
72+
}
73+
legacyPool := legacypool.New(legacypool.DefaultConfig, chain)
74+
pool, err := txpool.New(gasTip, chain, []txpool.SubPool{legacyPool})
75+
if err != nil {
76+
t.Fatalf("Failed to create tx pool: %v", err)
77+
}
78+
time.Sleep(time.Second) // hack, make sure txpool initialization is done
79+
80+
return &testEnv{
81+
chain: chain,
82+
pool: pool,
83+
tracker: New(journal, time.Minute, gspec.Config, pool),
84+
genDb: genDb,
85+
}
86+
}
87+
88+
func (env *testEnv) close() {
89+
env.chain.Stop()
90+
}
91+
92+
func (env *testEnv) setGasTip(gasTip uint64) {
93+
env.pool.SetGasTip(new(big.Int).SetUint64(gasTip))
94+
}
95+
96+
func (env *testEnv) makeTx(nonce uint64, gasPrice *big.Int) *types.Transaction {
97+
if nonce == 0 {
98+
head := env.chain.CurrentHeader()
99+
state, _ := env.chain.StateAt(head.Root)
100+
nonce = state.GetNonce(address)
101+
}
102+
if gasPrice == nil {
103+
gasPrice = big.NewInt(params.GWei)
104+
}
105+
tx, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x00}, big.NewInt(1000), params.TxGas, gasPrice, nil), signer, key)
106+
return tx
107+
}
108+
109+
func (env *testEnv) commit() {
110+
head := env.chain.CurrentBlock()
111+
block := env.chain.GetBlock(head.Hash(), head.Number.Uint64())
112+
blocks, _ := core.GenerateChain(env.chain.Config(), block, ethash.NewFaker(), env.genDb, 1, func(i int, gen *core.BlockGen) {
113+
tx, err := types.SignTx(types.NewTransaction(gen.TxNonce(address), common.Address{0x00}, big.NewInt(1000), params.TxGas, gen.BaseFee(), nil), signer, key)
114+
if err != nil {
115+
panic(err)
116+
}
117+
gen.AddTx(tx)
118+
})
119+
env.chain.InsertChain(blocks)
120+
}
121+
122+
func TestRejectInvalids(t *testing.T) {
123+
env := newTestEnv(t, 10, 0, "")
124+
defer env.close()
125+
126+
var cases = []struct {
127+
gasTip uint64
128+
tx *types.Transaction
129+
expErr error
130+
op func()
131+
}{
132+
{
133+
tx: env.makeTx(5, nil), // stale
134+
expErr: core.ErrNonceTooLow,
135+
},
136+
{
137+
tx: env.makeTx(11, nil), // future transaction
138+
expErr: nil,
139+
},
140+
{
141+
gasTip: params.GWei,
142+
tx: env.makeTx(0, new(big.Int).SetUint64(params.GWei/2)), // low price
143+
expErr: txpool.ErrUnderpriced,
144+
},
145+
{
146+
tx: types.NewTransaction(10, common.Address{0x00}, big.NewInt(1000), params.TxGas, big.NewInt(params.GWei), nil), // invalid signature
147+
expErr: types.ErrInvalidSig,
148+
},
149+
{
150+
op: func() {
151+
env.commit()
152+
},
153+
tx: env.makeTx(10, nil), // stale
154+
expErr: core.ErrNonceTooLow,
155+
},
156+
{
157+
tx: env.makeTx(11, nil),
158+
expErr: nil,
159+
},
160+
}
161+
for _, c := range cases {
162+
if c.gasTip != 0 {
163+
env.setGasTip(c.gasTip)
164+
}
165+
if c.op != nil {
166+
c.op()
167+
}
168+
gotErr := env.tracker.Track(c.tx)
169+
if c.expErr == nil && gotErr != nil {
170+
t.Fatalf("Unexpected error: %v", gotErr)
171+
}
172+
if c.expErr != nil && !errors.Is(gotErr, c.expErr) {
173+
t.Fatalf("Unexpected error, want: %v, got: %v", c.expErr, gotErr)
174+
}
175+
}
176+
}

core/txpool/txpool.go

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ import (
2424

2525
"github.com/ethereum/go-ethereum/common"
2626
"github.com/ethereum/go-ethereum/core"
27+
"github.com/ethereum/go-ethereum/core/state"
2728
"github.com/ethereum/go-ethereum/core/types"
2829
"github.com/ethereum/go-ethereum/crypto/kzg4844"
2930
"github.com/ethereum/go-ethereum/event"
3031
"github.com/ethereum/go-ethereum/log"
3132
"github.com/ethereum/go-ethereum/metrics"
33+
"github.com/ethereum/go-ethereum/params"
3234
)
3335

3436
// TxStatus is the current status of a transaction as seen by the pool.
@@ -53,11 +55,17 @@ var (
5355
// BlockChain defines the minimal set of methods needed to back a tx pool with
5456
// a chain. Exists to allow mocking the live chain out of tests.
5557
type BlockChain interface {
58+
// Config retrieves the chain's fork configuration.
59+
Config() *params.ChainConfig
60+
5661
// CurrentBlock returns the current head of the chain.
5762
CurrentBlock() *types.Header
5863

5964
// SubscribeChainHeadEvent subscribes to new blocks being added to the chain.
6065
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
66+
67+
// StateAt returns a state database for a given root hash (generally the head).
68+
StateAt(root common.Hash) (*state.StateDB, error)
6169
}
6270

6371
// TxPool is an aggregator for various transaction specific pools, collectively
@@ -67,6 +75,11 @@ type BlockChain interface {
6775
// resource constraints.
6876
type TxPool struct {
6977
subpools []SubPool // List of subpools for specialized transaction handling
78+
chain BlockChain
79+
signer types.Signer
80+
81+
stateLock sync.RWMutex // The lock for protecting state instance
82+
state *state.StateDB // Current state at the blockchain head
7083

7184
reservations map[common.Address]SubPool // Map with the account to pool reservations
7285
reserveLock sync.Mutex // Lock protecting the account reservations
@@ -86,8 +99,21 @@ func New(gasTip uint64, chain BlockChain, subpools []SubPool) (*TxPool, error) {
8699
// during initialization.
87100
head := chain.CurrentBlock()
88101

102+
// Initialize the state with head block, or fallback to empty one in
103+
// case the head state is not available (might occur when node is not
104+
// fully synced).
105+
statedb, err := chain.StateAt(head.Root)
106+
if err != nil {
107+
statedb, err = chain.StateAt(types.EmptyRootHash)
108+
}
109+
if err != nil {
110+
return nil, err
111+
}
89112
pool := &TxPool{
90113
subpools: subpools,
114+
chain: chain,
115+
signer: types.LatestSigner(chain.Config()),
116+
state: statedb,
91117
reservations: make(map[common.Address]SubPool),
92118
quit: make(chan chan error),
93119
term: make(chan struct{}),
@@ -101,7 +127,7 @@ func New(gasTip uint64, chain BlockChain, subpools []SubPool) (*TxPool, error) {
101127
return nil, err
102128
}
103129
}
104-
go pool.loop(head, chain)
130+
go pool.loop(head)
105131
return pool, nil
106132
}
107133

@@ -179,14 +205,14 @@ func (p *TxPool) Close() error {
179205
// loop is the transaction pool's main event loop, waiting for and reacting to
180206
// outside blockchain events as well as for various reporting and transaction
181207
// eviction events.
182-
func (p *TxPool) loop(head *types.Header, chain BlockChain) {
208+
func (p *TxPool) loop(head *types.Header) {
183209
// Close the termination marker when the pool stops
184210
defer close(p.term)
185211

186212
// Subscribe to chain head events to trigger subpool resets
187213
var (
188214
newHeadCh = make(chan core.ChainHeadEvent)
189-
newHeadSub = chain.SubscribeChainHeadEvent(newHeadCh)
215+
newHeadSub = p.chain.SubscribeChainHeadEvent(newHeadCh)
190216
)
191217
defer newHeadSub.Unsubscribe()
192218

@@ -219,6 +245,14 @@ func (p *TxPool) loop(head *types.Header, chain BlockChain) {
219245
// Try to inject a busy marker and start a reset if successful
220246
select {
221247
case resetBusy <- struct{}{}:
248+
statedb, err := p.chain.StateAt(newHead.Root)
249+
if err != nil {
250+
log.Crit("Failed to reset txpool state", "err", err)
251+
}
252+
p.stateLock.Lock()
253+
p.state = statedb
254+
p.stateLock.Unlock()
255+
222256
// Busy marker injected, start a new subpool reset
223257
go func(oldHead, newHead *types.Header) {
224258
for _, subpool := range p.subpools {
@@ -339,6 +373,20 @@ func (p *TxPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Pr
339373
// ValidateTxBasics checks whether a transaction is valid according to the consensus
340374
// rules, but does not check state-dependent validation such as sufficient balance.
341375
func (p *TxPool) ValidateTxBasics(tx *types.Transaction) error {
376+
addr, err := types.Sender(p.signer, tx)
377+
if err != nil {
378+
return err
379+
}
380+
// Reject transactions with stale nonce. Gapped-nonce future transactions
381+
// are considered valid and will be handled by the subpool according to its
382+
// internal policy.
383+
p.stateLock.RLock()
384+
nonce := p.state.GetNonce(addr)
385+
p.stateLock.RUnlock()
386+
387+
if nonce > tx.Nonce() {
388+
return core.ErrNonceTooLow
389+
}
342390
for _, subpool := range p.subpools {
343391
if subpool.Filter(tx) {
344392
return subpool.ValidateTxBasics(tx)

0 commit comments

Comments
 (0)