eth/fetcher: move check for chain tx inside fetcher

Signed-off-by: Csaba Kiraly <csaba.kiraly@gmail.com>
This commit is contained in:
Csaba Kiraly 2026-01-15 19:50:06 +01:00
parent ac65a7cd87
commit 55f573ae87
No known key found for this signature in database
GPG key ID: 0FE274EE8C95166E
4 changed files with 86 additions and 87 deletions

View file

@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -71,6 +72,11 @@ const (
// addTxsBatchSize it the max number of transactions to add in a single batch from a peer. // addTxsBatchSize it the max number of transactions to add in a single batch from a peer.
addTxsBatchSize = 128 addTxsBatchSize = 128
// txOnChainCacheLimit is number of on-chain transactions to keep in a cache to avoid
// re-fetching them soon after they are mined.
// Approx 1MB for 30 minutes of transactions at 18 tps
txOnChainCacheLimit = 32768
) )
var ( var (
@ -152,6 +158,9 @@ type TxFetcher struct {
txSeq uint64 // Unique transaction sequence number txSeq uint64 // Unique transaction sequence number
underpriced *lru.Cache[common.Hash, time.Time] // Transactions discarded as too cheap (don't re-fetch) underpriced *lru.Cache[common.Hash, time.Time] // Transactions discarded as too cheap (don't re-fetch)
chain *core.BlockChain // Blockchain interface for on-chain checks
txOnChainCache *lru.Cache[common.Hash, struct{}] // Cache to avoid fetching once the tx gets on chain
// Stage 1: Waiting lists for newly discovered transactions that might be // Stage 1: Waiting lists for newly discovered transactions that might be
// broadcast without needing explicit request/reply round trips. // broadcast without needing explicit request/reply round trips.
waitlist map[common.Hash]map[string]struct{} // Transactions waiting for an potential broadcast waitlist map[common.Hash]map[string]struct{} // Transactions waiting for an potential broadcast
@ -184,36 +193,40 @@ type TxFetcher struct {
// NewTxFetcher creates a transaction fetcher to retrieve transaction // NewTxFetcher creates a transaction fetcher to retrieve transaction
// based on hash announcements. // based on hash announcements.
func NewTxFetcher(validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string)) *TxFetcher { // Chain can be nil to disable on-chain checks.
return NewTxFetcherForTests(validateMeta, addTxs, fetchTxs, dropPeer, mclock.System{}, time.Now, nil) func NewTxFetcher(chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string)) *TxFetcher {
return NewTxFetcherForTests(chain, validateMeta, addTxs, fetchTxs, dropPeer, mclock.System{}, time.Now, nil)
} }
// NewTxFetcherForTests is a testing method to mock out the realtime clock with // NewTxFetcherForTests is a testing method to mock out the realtime clock with
// a simulated version and the internal randomness with a deterministic one. // a simulated version and the internal randomness with a deterministic one.
// Chain can be nil to disable on-chain checks.
func NewTxFetcherForTests( func NewTxFetcherForTests(
validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string), chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string),
clock mclock.Clock, realTime func() time.Time, rand *mrand.Rand) *TxFetcher { clock mclock.Clock, realTime func() time.Time, rand *mrand.Rand) *TxFetcher {
return &TxFetcher{ return &TxFetcher{
notify: make(chan *txAnnounce), notify: make(chan *txAnnounce),
cleanup: make(chan *txDelivery), cleanup: make(chan *txDelivery),
drop: make(chan *txDrop), drop: make(chan *txDrop),
quit: make(chan struct{}), quit: make(chan struct{}),
waitlist: make(map[common.Hash]map[string]struct{}), waitlist: make(map[common.Hash]map[string]struct{}),
waittime: make(map[common.Hash]mclock.AbsTime), waittime: make(map[common.Hash]mclock.AbsTime),
waitslots: make(map[string]map[common.Hash]*txMetadataWithSeq), waitslots: make(map[string]map[common.Hash]*txMetadataWithSeq),
announces: make(map[string]map[common.Hash]*txMetadataWithSeq), announces: make(map[string]map[common.Hash]*txMetadataWithSeq),
announced: make(map[common.Hash]map[string]struct{}), announced: make(map[common.Hash]map[string]struct{}),
fetching: make(map[common.Hash]string), fetching: make(map[common.Hash]string),
requests: make(map[string]*txRequest), requests: make(map[string]*txRequest),
alternates: make(map[common.Hash]map[string]struct{}), alternates: make(map[common.Hash]map[string]struct{}),
underpriced: lru.NewCache[common.Hash, time.Time](maxTxUnderpricedSetSize), underpriced: lru.NewCache[common.Hash, time.Time](maxTxUnderpricedSetSize),
validateMeta: validateMeta, txOnChainCache: lru.NewCache[common.Hash, struct{}](txOnChainCacheLimit),
addTxs: addTxs, chain: chain,
fetchTxs: fetchTxs, validateMeta: validateMeta,
dropPeer: dropPeer, addTxs: addTxs,
clock: clock, fetchTxs: fetchTxs,
realTime: realTime, dropPeer: dropPeer,
rand: rand, clock: clock,
realTime: realTime,
rand: rand,
} }
} }
@ -245,6 +258,12 @@ func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []c
continue continue
} }
// check on chain as well (no need to check limbo separately, as chain checks limbo too)
if _, exist := f.txOnChainCache.Get(hash); exist {
duplicate++ //TODO
continue
}
if f.isKnownUnderpriced(hash) { if f.isKnownUnderpriced(hash) {
underpriced++ underpriced++
continue continue
@ -412,7 +431,29 @@ func (f *TxFetcher) loop() {
waitTrigger = make(chan struct{}, 1) waitTrigger = make(chan struct{}, 1)
timeoutTrigger = make(chan struct{}, 1) timeoutTrigger = make(chan struct{}, 1)
oldHead *types.Header
) )
//Subscribe to chain events to know when transactions are added to chain
var headEventCh chan core.ChainEvent
var subErr <-chan error
if f.chain != nil {
headEventCh = make(chan core.ChainEvent, 10)
sub := f.chain.SubscribeChainEvent(headEventCh)
subErr = sub.Err()
defer func() {
sub.Unsubscribe()
for {
select {
case <-headEventCh: // drain channel
default:
return
}
}
}()
}
for { for {
select { select {
case ann := <-f.notify: case ann := <-f.notify:
@ -837,6 +878,24 @@ func (f *TxFetcher) loop() {
f.rescheduleTimeout(timeoutTimer, timeoutTrigger) f.rescheduleTimeout(timeoutTimer, timeoutTrigger)
} }
case ev := <-headEventCh:
// New head(s) added
newHead := ev.Header
if oldHead != nil && newHead.ParentHash != oldHead.Hash() {
// Reorg or setHead detected, clear the cache. We could be smarter here and
// only remove/add the diff, but this is simpler and not being exact here
// only results in a few more fetches.
f.txOnChainCache.Purge()
}
oldHead = newHead
// Add all transactions from the new block to the on-chain cache
for _, tx := range ev.Transactions {
f.txOnChainCache.Add(tx.Hash(), struct{}{})
}
case <-subErr:
return
case <-f.quit: case <-f.quit:
return return
} }

View file

@ -91,6 +91,7 @@ type txFetcherTest struct {
// and deterministic randomness. // and deterministic randomness.
func newTestTxFetcher() *TxFetcher { func newTestTxFetcher() *TxFetcher {
return NewTxFetcher( return NewTxFetcher(
nil,
func(common.Hash, byte) error { return nil }, func(common.Hash, byte) error { return nil },
func(txs []*types.Transaction) []error { func(txs []*types.Transaction) []error {
return make([]error, len(txs)) return make([]error, len(txs))
@ -2191,6 +2192,7 @@ func TestTransactionForgotten(t *testing.T) {
} }
fetcher := NewTxFetcherForTests( fetcher := NewTxFetcherForTests(
nil,
func(common.Hash, byte) error { return nil }, func(common.Hash, byte) error { return nil },
func(txs []*types.Transaction) []error { func(txs []*types.Transaction) []error {
errs := make([]error, len(txs)) errs := make([]error, len(txs))

View file

@ -29,7 +29,6 @@ import (
"github.com/dchest/siphash" "github.com/dchest/siphash"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
@ -59,11 +58,6 @@ const (
// All transactions with a higher size will be announced and need to be fetched // All transactions with a higher size will be announced and need to be fetched
// by the peer. // by the peer.
txMaxBroadcastSize = 4096 txMaxBroadcastSize = 4096
// txOnChainCacheLimit is number of on-chain transactions to keep in a cache to avoid
// re-fetching them soon after they are mined.
// Approx 1MB for 30 minutes of transactions at 18 tps
txOnChainCacheLimit = 32768
) )
var syncChallengeTimeout = 15 * time.Second // Time allowance for a node to reply to the sync progress challenge var syncChallengeTimeout = 15 * time.Second // Time allowance for a node to reply to the sync progress challenge
@ -132,9 +126,6 @@ type handler struct {
peers *peerSet peers *peerSet
txBroadcastKey [16]byte txBroadcastKey [16]byte
// cache of on-chain transactions to avoid fetching once the tx gets on chain
txOnChainCache *lru.Cache[common.Hash, struct{}]
eventMux *event.TypeMux eventMux *event.TypeMux
txsCh chan core.NewTxsEvent txsCh chan core.NewTxsEvent
txsSub event.Subscription txsSub event.Subscription
@ -192,16 +183,12 @@ func newHandler(config *handlerConfig) (*handler, error) {
if h.txpool.Has(tx) { if h.txpool.Has(tx) {
return txpool.ErrAlreadyKnown return txpool.ErrAlreadyKnown
} }
// check on chain as well (no need to check limbo separately, as chain checks limbo too)
if _, exist := h.txOnChainCache.Get(tx); exist {
return core.ErrNonceTooLow
}
if !h.txpool.FilterType(kind) { if !h.txpool.FilterType(kind) {
return types.ErrTxTypeNotSupported return types.ErrTxTypeNotSupported
} }
return nil return nil
} }
h.txFetcher = fetcher.NewTxFetcher(validateMeta, addTxs, fetchTx, h.removePeer) h.txFetcher = fetcher.NewTxFetcher(h.chain, validateMeta, addTxs, fetchTx, h.removePeer)
return h, nil return h, nil
} }
@ -436,10 +423,6 @@ func (h *handler) Start(maxPeers int) {
h.blockRange = newBlockRangeState(h.chain, h.eventMux) h.blockRange = newBlockRangeState(h.chain, h.eventMux)
go h.blockRangeLoop(h.blockRange) go h.blockRangeLoop(h.blockRange)
h.wg.Add(1)
h.txOnChainCache = lru.NewCache[common.Hash, struct{}](txOnChainCacheLimit)
go h.txOnChainCacheLoop()
// start sync handlers // start sync handlers
h.txFetcher.Start() h.txFetcher.Start()
@ -731,49 +714,3 @@ func (bc *broadcastChoice) choosePeers(peers []*ethPeer, txSender common.Address
} }
return bc.buffer return bc.buffer
} }
// txOnChainCacheLoop listens to new chain events and updates the on-chain tx cache,
// used as part of the filter on incoming tx announcements. In case of a reorg, the
// cache is reset.
func (h *handler) txOnChainCacheLoop() {
defer h.wg.Done()
//Subscribe to chain events to know when transactions are added to chain
headEventCh := make(chan core.ChainEvent, 10)
sub := h.chain.SubscribeChainEvent(headEventCh)
defer func() {
sub.Unsubscribe()
// drain channel
for {
select {
case <-headEventCh:
default:
return
}
}
}()
var oldHead *types.Header
for {
select {
case ev := <-headEventCh:
// New head(s) added
newHead := ev.Header
if oldHead != nil && newHead.ParentHash != oldHead.Hash() {
// Reorg or setHead detected, clear the cache. We could be smarter here and
// only remove/add the diff, but this is simpler and not being exact here
// only results in a few more fetches.
h.txOnChainCache.Purge()
}
oldHead = newHead
// Add all transactions from the new block to the on-chain cache
for _, tx := range ev.Transactions {
h.txOnChainCache.Add(tx.Hash(), struct{}{})
}
// exit when the subscription ends
case <-sub.Err():
return
case <-h.quitSync:
return
}
}
}

View file

@ -78,6 +78,7 @@ func fuzz(input []byte) int {
rand := rand.New(rand.NewSource(0x3a29)) // Same used in package tests!!! rand := rand.New(rand.NewSource(0x3a29)) // Same used in package tests!!!
f := fetcher.NewTxFetcherForTests( f := fetcher.NewTxFetcherForTests(
nil,
func(common.Hash, byte) error { return nil }, func(common.Hash, byte) error { return nil },
func(txs []*types.Transaction) []error { func(txs []*types.Transaction) []error {
return make([]error, len(txs)) return make([]error, len(txs))