From 4e80be539b98be1d090573119270ce3116bab190 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 15:20:30 -0500 Subject: [PATCH 01/18] Provide alternative to gossip.BloomFilter --- network/p2p/gossip/bloom.go | 5 + network/p2p/gossip/gossip.go | 41 ++++++- network/p2p/gossip/gossip_test.go | 78 ++++++------- network/p2p/gossip/gossipable.go | 31 ----- network/p2p/gossip/handler.go | 13 ++- network/p2p/gossip/set.go | 159 ++++++++++++++++++++++++++ network/p2p/gossip/set_test.go | 130 +++++++++++++++++++++ vms/avm/network/gossip.go | 62 ++-------- vms/avm/network/gossip_test.go | 38 ++---- vms/avm/network/network.go | 45 +++++--- vms/platformvm/network/gossip.go | 53 ++------- vms/platformvm/network/gossip_test.go | 79 ++----------- vms/platformvm/network/network.go | 15 ++- 13 files changed, 448 insertions(+), 301 deletions(-) delete mode 100644 network/p2p/gossip/gossipable.go create mode 100644 network/p2p/gossip/set.go create mode 100644 network/p2p/gossip/set_test.go diff --git a/network/p2p/gossip/bloom.go b/network/p2p/gossip/bloom.go index 9c05c8db78e0..e6ad24fc9ab5 100644 --- a/network/p2p/gossip/bloom.go +++ b/network/p2p/gossip/bloom.go @@ -18,6 +18,8 @@ import ( // // Invariant: The returned bloom filter is not safe to reset concurrently with // other operations. However, it is otherwise safe to access concurrently. +// +// Deprecated: [SetWithBloomFilter] should be used to manage bloom filters. func NewBloomFilter( registerer prometheus.Registerer, namespace string, @@ -45,6 +47,7 @@ func NewBloomFilter( return filter, err } +// Deprecated: [SetWithBloomFilter] should be used to manage bloom filters. type BloomFilter struct { minTargetElements int targetFalsePositiveProbability float64 @@ -85,6 +88,8 @@ func (b *BloomFilter) Marshal() ([]byte, []byte) { // the same [targetFalsePositiveProbability]. // // Returns true if the bloom filter was reset. +// +// Deprecated: [SetWithBloomFilter] should be used to manage bloom filters. func ResetBloomFilterIfNeeded( bloomFilter *BloomFilter, targetElements int, diff --git a/network/p2p/gossip/gossip.go b/network/p2p/gossip/gossip.go index c03e513854df..ade88ee61684 100644 --- a/network/p2p/gossip/gossip.go +++ b/network/p2p/gossip/gossip.go @@ -18,6 +18,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils/bloom" "github.com/ava-labs/avalanchego/utils/buffer" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" @@ -40,6 +41,7 @@ const ( var ( _ Gossiper = (*ValidatorGossiper)(nil) _ Gossiper = (*PullGossiper[Gossipable])(nil) + _ Gossiper = (*PushGossiper[Gossipable])(nil) ioTypeLabels = []string{ioLabel, typeLabel} sentPushLabels = prometheus.Labels{ @@ -75,6 +77,17 @@ var ( ErrInvalidRegossipFrequency = errors.New("re-gossip frequency cannot be negative") ) +// Gossipable is an item that can be gossiped across the network +type Gossipable interface { + GossipID() ids.ID +} + +// Marshaller handles parsing logic for a concrete Gossipable type +type Marshaller[T Gossipable] interface { + MarshalGossip(T) ([]byte, error) + UnmarshalGossip([]byte) (T, error) +} + // Gossiper gossips Gossipables to other nodes type Gossiper interface { // Gossip runs a cycle of gossip. Returns an error if we failed to gossip. @@ -191,7 +204,7 @@ func (v ValidatorGossiper) Gossip(ctx context.Context) error { func NewPullGossiper[T Gossipable]( log logging.Logger, marshaller Marshaller[T], - set Set[T], + set PullGossiperSet[T], client *p2p.Client, metrics Metrics, pollSize int, @@ -206,17 +219,27 @@ func NewPullGossiper[T Gossipable]( } } +// PullGossiperSet exposes the current bloom filter and allows adding new items +// that were not included in the filter. +type PullGossiperSet[T Gossipable] interface { + // Add adds a value to the set. Returns an error if v was not added. + Add(v T) error + // BloomFilter returns the bloom filter and its corresponding salt. + BloomFilter() (bloom *bloom.Filter, salt ids.ID) +} + type PullGossiper[T Gossipable] struct { log logging.Logger marshaller Marshaller[T] - set Set[T] + set PullGossiperSet[T] client *p2p.Client metrics Metrics pollSize int } func (p *PullGossiper[_]) Gossip(ctx context.Context) error { - msgBytes, err := MarshalAppRequest(p.set.GetFilter()) + bf, salt := p.set.BloomFilter() + msgBytes, err := MarshalAppRequest(bf.Marshal(), salt[:]) if err != nil { return err } @@ -293,7 +316,7 @@ func (p *PullGossiper[_]) handleResponse( // NewPushGossiper returns an instance of PushGossiper func NewPushGossiper[T Gossipable]( marshaller Marshaller[T], - mempool Set[T], + set PushGossiperSet, validators p2p.ValidatorSubset, client *p2p.Client, metrics Metrics, @@ -320,7 +343,7 @@ func NewPushGossiper[T Gossipable]( return &PushGossiper[T]{ marshaller: marshaller, - set: mempool, + set: set, validators: validators, client: client, metrics: metrics, @@ -336,10 +359,16 @@ func NewPushGossiper[T Gossipable]( }, nil } +// PushGossiperSet exposes whether hashes are still included in a set. +type PushGossiperSet interface { + // Has returns true if the hash is in the set. + Has(h ids.ID) bool +} + // PushGossiper broadcasts gossip to peers randomly in the network type PushGossiper[T Gossipable] struct { marshaller Marshaller[T] - set Set[T] + set PushGossiperSet validators p2p.ValidatorSubset client *p2p.Client metrics Metrics diff --git a/network/p2p/gossip/gossip_test.go b/network/p2p/gossip/gossip_test.go index cb8222ceff4f..0d42098f85dc 100644 --- a/network/p2p/gossip/gossip_test.go +++ b/network/p2p/gossip/gossip_test.go @@ -20,7 +20,6 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/enginetest" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/snow/validators/validatorstest" - "github.com/ava-labs/avalanchego/utils/bloom" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" @@ -69,29 +68,26 @@ func (marshaller) UnmarshalGossip(bytes []byte) (tx, error) { type setDouble struct { txs set.Set[tx] - bloom *BloomFilter onAdd func(tx tx) } -func (s *setDouble) Add(gossipable tx) error { - if s.txs.Contains(gossipable) { - return fmt.Errorf("%s already present", ids.ID(gossipable)) +func (s *setDouble) Add(t tx) error { + if s.txs.Contains(t) { + return fmt.Errorf("%s already present", t) } - s.txs.Add(gossipable) - s.bloom.Add(gossipable) + s.txs.Add(t) if s.onAdd != nil { - s.onAdd(gossipable) + s.onAdd(t) } - return nil } -func (s *setDouble) Has(gossipID ids.ID) bool { - return s.txs.Contains(tx(gossipID)) +func (s *setDouble) Has(id ids.ID) bool { + return s.txs.Contains(tx(id)) } -func (s *setDouble) Iterate(f func(gossipable tx) bool) { +func (s *setDouble) Iterate(f func(t tx) bool) { for tx := range s.txs { if !f(tx) { return @@ -99,8 +95,8 @@ func (s *setDouble) Iterate(f func(gossipable tx) bool) { } } -func (s *setDouble) GetFilter() ([]byte, []byte) { - return s.bloom.Marshal() +func (s *setDouble) Len() int { + return s.txs.Len() } func TestGossiperGossip(t *testing.T) { @@ -175,13 +171,18 @@ func TestGossiperGossip(t *testing.T) { ) require.NoError(err) - responseBloom, err := NewBloomFilter(prometheus.NewRegistry(), "", 1000, 0.01, 0.05) + var serverSet setDouble + serverMempool, err := NewSetWithBloomFilter( + &serverSet, + prometheus.NewRegistry(), + "", + 1000, + 0.01, + 0.05, + ) require.NoError(err) - responseSet := &setDouble{ - bloom: responseBloom, - } for _, item := range tt.responder { - require.NoError(responseSet.Add(item)) + require.NoError(serverMempool.Add(item)) } metrics, err := NewMetrics(prometheus.NewRegistry(), "") @@ -196,7 +197,7 @@ func TestGossiperGossip(t *testing.T) { handler := NewHandler[tx]( logging.NoLog{}, marshaller, - responseSet, + serverMempool, metrics, tt.targetResponseSize, ) @@ -218,13 +219,18 @@ func TestGossiperGossip(t *testing.T) { require.NoError(err) require.NoError(requestNetwork.Connected(t.Context(), ids.EmptyNodeID, nil)) - bloom, err := NewBloomFilter(prometheus.NewRegistry(), "", 1000, 0.01, 0.05) + var requestSet setDouble + clientMempool, err := NewSetWithBloomFilter( + &requestSet, + prometheus.NewRegistry(), + "", + 1000, + 0.01, + 0.05, + ) require.NoError(err) - requestSet := &setDouble{ - bloom: bloom, - } for _, item := range tt.requester { - require.NoError(requestSet.Add(item)) + require.NoError(clientMempool.Add(item)) } requestClient := requestNetwork.NewClient( @@ -236,7 +242,7 @@ func TestGossiperGossip(t *testing.T) { gossiper := NewPullGossiper[tx]( logging.NoLog{}, marshaller, - requestSet, + clientMempool, requestClient, metrics, 1, @@ -457,20 +463,10 @@ func TestPushGossiperNew(t *testing.T) { } } -type fullSet[T Gossipable] struct{} - -func (fullSet[T]) Add(T) error { - return nil -} - -func (fullSet[T]) Has(ids.ID) bool { - return true -} - -func (fullSet[T]) Iterate(func(gossipable T) bool) {} +type hasFunc func(h ids.ID) bool -func (fullSet[_]) GetFilter() ([]byte, []byte) { - return bloom.FullFilter.Marshal(), ids.Empty[:] +func (f hasFunc) Has(h ids.ID) bool { + return f(h) } // Tests that the outgoing gossip is equivalent to what was accumulated @@ -593,7 +589,9 @@ func TestPushGossiper(t *testing.T) { gossiper, err := NewPushGossiper[tx]( marshaller, - fullSet[tx]{}, + hasFunc(func(ids.ID) bool { + return true // Never remove the items from the set + }), validators, client, metrics, diff --git a/network/p2p/gossip/gossipable.go b/network/p2p/gossip/gossipable.go deleted file mode 100644 index b535609a7a48..000000000000 --- a/network/p2p/gossip/gossipable.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package gossip - -import "github.com/ava-labs/avalanchego/ids" - -// Gossipable is an item that can be gossiped across the network -type Gossipable interface { - GossipID() ids.ID -} - -// Marshaller handles parsing logic for a concrete Gossipable type -type Marshaller[T Gossipable] interface { - MarshalGossip(T) ([]byte, error) - UnmarshalGossip([]byte) (T, error) -} - -// Set holds a set of known Gossipable items -type Set[T Gossipable] interface { - // Add adds a Gossipable to the set. Returns an error if gossipable was not - // added. - Add(gossipable T) error - // Has returns true if the gossipable is in the set. - Has(gossipID ids.ID) bool - // Iterate iterates over elements until [f] returns false - Iterate(f func(gossipable T) bool) - // GetFilter returns the byte representation of bloom filter and its - // corresponding salt. - GetFilter() (bloom []byte, salt []byte) -} diff --git a/network/p2p/gossip/handler.go b/network/p2p/gossip/handler.go index 7c016030c913..20cc31520bf3 100644 --- a/network/p2p/gossip/handler.go +++ b/network/p2p/gossip/handler.go @@ -18,10 +18,19 @@ import ( var _ p2p.Handler = (*Handler[Gossipable])(nil) +// HandlerSet exposes the ability to add new values to the set in response to +// pushed information and for responding to pull requests. +type HandlerSet[T Gossipable] interface { + // Add adds a value to the set. Returns an error if v was not added. + Add(v T) error + // Iterate iterates over elements until f returns false. + Iterate(f func(v T) bool) +} + func NewHandler[T Gossipable]( log logging.Logger, marshaller Marshaller[T], - set Set[T], + set HandlerSet[T], metrics Metrics, targetResponseSize int, ) *Handler[T] { @@ -39,7 +48,7 @@ type Handler[T Gossipable] struct { p2p.Handler marshaller Marshaller[T] log logging.Logger - set Set[T] + set HandlerSet[T] metrics Metrics targetResponseSize int } diff --git a/network/p2p/gossip/set.go b/network/p2p/gossip/set.go new file mode 100644 index 000000000000..187b13454fcf --- /dev/null +++ b/network/p2p/gossip/set.go @@ -0,0 +1,159 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import ( + "crypto/rand" + "fmt" + "sync" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/bloom" + "github.com/prometheus/client_golang/prometheus" +) + +var ( + _ Set[Gossipable] = (*SetWithBloomFilter[Gossipable])(nil) + _ PullGossiperSet[Gossipable] = (*SetWithBloomFilter[Gossipable])(nil) +) + +type Set[T Gossipable] interface { + HandlerSet[T] + PushGossiperSet + // Len returns the number of items in the set. + Len() int +} + +// NewSetWithBloomFilter wraps set with a bloom filter. It is expected for all +// future additions to the provided set to go through the returned set, or be +// followed by a call to AddToBloom. +func NewSetWithBloomFilter[T Gossipable]( + set Set[T], + registerer prometheus.Registerer, + namespace string, + minTargetElements int, + targetFalsePositiveProbability, + resetFalsePositiveProbability float64, +) (*SetWithBloomFilter[T], error) { + metrics, err := bloom.NewMetrics(namespace, registerer) + if err != nil { + return nil, err + } + m := &SetWithBloomFilter[T]{ + set: set, + + minTargetElements: minTargetElements, + targetFalsePositiveProbability: targetFalsePositiveProbability, + resetFalsePositiveProbability: resetFalsePositiveProbability, + + metrics: metrics, + } + return m, m.resetBloomFilter() +} + +type SetWithBloomFilter[T Gossipable] struct { + set Set[T] + + minTargetElements int + targetFalsePositiveProbability float64 + resetFalsePositiveProbability float64 + + metrics *bloom.Metrics + + l sync.RWMutex + maxCount int + bloom *bloom.Filter + // salt is provided to eventually unblock collisions in Bloom. It's possible + // that conflicting Gossipable items collide in the bloom filter, so a salt + // is generated to eventually resolve collisions. + salt ids.ID +} + +func (s *SetWithBloomFilter[T]) Add(v T) error { + if err := s.set.Add(v); err != nil { + return err + } + return s.AddToBloom(v.GossipID()) +} + +// AddToBloom adds the provided ID to the bloom filter. This function is exposed +// to allow modifications to the inner set without going through Add. +// +// Even if an error is returned, the ID has still been added to the bloom +// filter. +func (s *SetWithBloomFilter[T]) AddToBloom(h ids.ID) error { + s.l.RLock() + bloom.Add(s.bloom, h[:], s.salt[:]) + shouldReset := s.shouldReset() + s.l.RUnlock() + s.metrics.Count.Inc() + + if !shouldReset { + return nil + } + + s.l.Lock() + defer s.l.Unlock() + + // Bloom filter was already reset by another thread + if !s.shouldReset() { + return nil + } + return s.resetBloomFilter() +} + +func (s *SetWithBloomFilter[T]) shouldReset() bool { + return s.bloom.Count() > s.maxCount +} + +// resetBloomFilter attempts to generate a new bloom filter and fill it with the +// current entries in the set. +// +// If an error is returned, the bloom filter and salt are unchanged. +func (s *SetWithBloomFilter[T]) resetBloomFilter() error { + targetElements := max(2*s.set.Len(), s.minTargetElements) + numHashes, numEntries := bloom.OptimalParameters( + targetElements, + s.targetFalsePositiveProbability, + ) + newBloom, err := bloom.New(numHashes, numEntries) + if err != nil { + return fmt.Errorf("creating new bloom: %w", err) + } + var newSalt ids.ID + if _, err := rand.Read(newSalt[:]); err != nil { + return fmt.Errorf("generating new salt: %w", err) + } + s.set.Iterate(func(v T) bool { + h := v.GossipID() + bloom.Add(newBloom, h[:], newSalt[:]) + return true + }) + + s.maxCount = bloom.EstimateCount(numHashes, numEntries, s.resetFalsePositiveProbability) + s.bloom = newBloom + s.salt = newSalt + + s.metrics.Reset(newBloom, s.maxCount) + return nil +} + +func (s *SetWithBloomFilter[T]) Has(h ids.ID) bool { + return s.set.Has(h) +} + +func (s *SetWithBloomFilter[T]) Iterate(f func(T) bool) { + s.set.Iterate(f) +} + +func (s *SetWithBloomFilter[T]) Len() int { + return s.set.Len() +} + +func (s *SetWithBloomFilter[_]) BloomFilter() (*bloom.Filter, ids.ID) { + s.l.RLock() + defer s.l.RUnlock() + + return s.bloom, s.salt +} diff --git a/network/p2p/gossip/set_test.go b/network/p2p/gossip/set_test.go new file mode 100644 index 000000000000..add3436ccfc2 --- /dev/null +++ b/network/p2p/gossip/set_test.go @@ -0,0 +1,130 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import ( + "testing" + + "github.com/ava-labs/avalanchego/utils/bloom" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/require" +) + +func TestSetWithBloomFilter_Refresh(t *testing.T) { + type ( + op struct { + add *tx + remove *tx + } + test struct { + name string + resetFalsePositiveProbability float64 + ops []op + expected []tx + expectedResetCount float64 + } + ) + tests := []test{ + { + name: "no refresh", + resetFalsePositiveProbability: 1, // maxCount = 9223372036854775807 + ops: []op{ + {add: &tx{0}}, + {add: &tx{1}}, + {add: &tx{2}}, + }, + expected: []tx{ + {0}, + {1}, + {2}, + }, + expectedResetCount: 0, + }, + { + name: "no refresh - with removals", + resetFalsePositiveProbability: 1, // maxCount = 9223372036854775807 + ops: []op{ + {add: &tx{0}}, + {add: &tx{1}}, + {add: &tx{2}}, + {remove: &tx{0}}, + {remove: &tx{1}}, + {remove: &tx{2}}, + }, + expected: []tx{ + {0}, + {1}, + {2}, + }, + expectedResetCount: 0, + }, + { + name: "refresh", + resetFalsePositiveProbability: 0.0000000000000001, // maxCount = 1 + ops: []op{ + {add: &tx{0}}, // no reset + {remove: &tx{0}}, + {add: &tx{1}}, // reset + }, + expected: []tx{ + {1}, + }, + expectedResetCount: 1, + }, + { + name: "multiple refresh", + resetFalsePositiveProbability: 0.0000000000000001, // maxCount = 1 + ops: []op{ + {add: &tx{0}}, // no reset + {remove: &tx{0}}, + {add: &tx{1}}, // reset + {remove: &tx{1}}, + {add: &tx{2}}, // reset + }, + expected: []tx{ + {2}, + }, + expectedResetCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + const ( + minTargetElements = 1 + targetFalsePositiveProbability = 0.0001 + ) + var s setDouble + bs, err := NewSetWithBloomFilter( + &s, + prometheus.NewRegistry(), + "", + minTargetElements, + targetFalsePositiveProbability, + tt.resetFalsePositiveProbability, + ) + require.NoError(err) + + for _, op := range tt.ops { + if op.add != nil { + require.NoError(bs.Add(*op.add)) + } + if op.remove != nil { + s.txs.Remove(*op.remove) + } + } + + // Add one to expectedResetCount to account for the initial creation + // of the bloom filter. + require.Equal(tt.expectedResetCount+1, testutil.ToFloat64(bs.metrics.ResetCount)) + b, h := bs.BloomFilter() + for _, expected := range tt.expected { + require.True(bloom.Contains(b, expected[:], h[:])) + } + }) + } +} diff --git a/vms/avm/network/gossip.go b/vms/avm/network/gossip.go index fb2077449dae..e9d92736dc15 100644 --- a/vms/avm/network/gossip.go +++ b/vms/avm/network/gossip.go @@ -6,23 +6,19 @@ package network import ( "context" "fmt" - "sync" "time" - "github.com/prometheus/client_golang/prometheus" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/gossip" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/txs/mempool" ) var ( _ p2p.Handler = (*txGossipHandler)(nil) - _ gossip.Set[*txs.Tx] = (*gossipMempool)(nil) + _ gossip.Set[*txs.Tx] = (*mempoolWithVerification)(nil) _ gossip.Marshaller[*txs.Tx] = (*txParser)(nil) ) @@ -66,38 +62,26 @@ func (g *txParser) UnmarshalGossip(bytes []byte) (*txs.Tx, error) { return g.parser.ParseTx(bytes) } -func newGossipMempool( +func newMempoolWithVerification( mempool mempool.Mempool[*txs.Tx], - registerer prometheus.Registerer, - log logging.Logger, txVerifier TxVerifier, - minTargetElements int, - targetFalsePositiveProbability, - resetFalsePositiveProbability float64, -) (*gossipMempool, error) { - bloom, err := gossip.NewBloomFilter(registerer, "mempool_bloom_filter", minTargetElements, targetFalsePositiveProbability, resetFalsePositiveProbability) - return &gossipMempool{ +) *mempoolWithVerification { + return &mempoolWithVerification{ Mempool: mempool, - log: log, txVerifier: txVerifier, - bloom: bloom, - }, err + } } -type gossipMempool struct { +type mempoolWithVerification struct { mempool.Mempool[*txs.Tx] - log logging.Logger txVerifier TxVerifier - - lock sync.RWMutex - bloom *gossip.BloomFilter } // Add is called by the p2p SDK when handling transactions that were pushed to // us and when handling transactions that were pulled from a peer. If this // returns a nil error while handling push gossip, the p2p SDK will queue the // transaction to push gossip as well. -func (g *gossipMempool) Add(tx *txs.Tx) error { +func (g *mempoolWithVerification) Add(tx *txs.Tx) error { txID := tx.ID() if _, ok := g.Mempool.Get(txID); ok { return fmt.Errorf("attempted to issue %w: %s ", mempool.ErrDuplicateTx, txID) @@ -120,43 +104,15 @@ func (g *gossipMempool) Add(tx *txs.Tx) error { return g.AddWithoutVerification(tx) } -func (g *gossipMempool) Has(txID ids.ID) bool { +func (g *mempoolWithVerification) Has(txID ids.ID) bool { _, ok := g.Mempool.Get(txID) return ok } -func (g *gossipMempool) AddWithoutVerification(tx *txs.Tx) error { +func (g *mempoolWithVerification) AddWithoutVerification(tx *txs.Tx) error { if err := g.Mempool.Add(tx); err != nil { g.Mempool.MarkDropped(tx.ID(), err) return err } - - g.lock.Lock() - defer g.lock.Unlock() - - g.bloom.Add(tx) - reset, err := gossip.ResetBloomFilterIfNeeded(g.bloom, g.Mempool.Len()*bloomChurnMultiplier) - if err != nil { - return err - } - - if reset { - g.log.Debug("resetting bloom filter") - g.Mempool.Iterate(func(tx *txs.Tx) bool { - g.bloom.Add(tx) - return true - }) - } return nil } - -func (g *gossipMempool) Iterate(f func(*txs.Tx) bool) { - g.Mempool.Iterate(f) -} - -func (g *gossipMempool) GetFilter() (bloom []byte, salt []byte) { - g.lock.RLock() - defer g.lock.RUnlock() - - return g.bloom.Marshal() -} diff --git a/vms/avm/network/gossip_test.go b/vms/avm/network/gossip_test.go index 3059e22304e3..2c782f5fccf7 100644 --- a/vms/avm/network/gossip_test.go +++ b/vms/avm/network/gossip_test.go @@ -4,13 +4,13 @@ package network import ( + "errors" "testing" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/avm/fxs" "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/avm/txs/mempool" @@ -53,7 +53,7 @@ func TestMarshaller(t *testing.T) { require.Equal(want.GossipID(), got.GossipID()) } -func TestGossipMempoolAdd(t *testing.T) { +func TestMempoolWithVerificationAdd(t *testing.T) { require := require.New(t) metrics := prometheus.NewRegistry() @@ -61,17 +61,7 @@ func TestGossipMempoolAdd(t *testing.T) { baseMempool, err := mempool.New("", metrics) require.NoError(err) - mempool, err := newGossipMempool( - baseMempool, - metrics, - logging.NoLog{}, - testVerifier{}, - DefaultConfig.ExpectedBloomFilterElements, - DefaultConfig.ExpectedBloomFilterFalsePositiveProbability, - DefaultConfig.MaxBloomFilterFalsePositiveProbability, - ) - require.NoError(err) - + mempool := newMempoolWithVerification(baseMempool, testVerifier{}) tx := &txs.Tx{ Unsigned: &txs.BaseTx{ BaseTx: avax.BaseTx{ @@ -82,10 +72,10 @@ func TestGossipMempoolAdd(t *testing.T) { } require.NoError(mempool.Add(tx)) - require.True(mempool.bloom.Has(tx)) + require.True(mempool.Has(tx.ID())) } -func TestGossipMempoolAddVerified(t *testing.T) { +func TestMempoolWithVerificationAddWithoutVerification(t *testing.T) { require := require.New(t) metrics := prometheus.NewRegistry() @@ -93,19 +83,9 @@ func TestGossipMempoolAddVerified(t *testing.T) { baseMempool, err := mempool.New("", metrics) require.NoError(err) - mempool, err := newGossipMempool( - baseMempool, - metrics, - logging.NoLog{}, - testVerifier{ - err: errTest, // We shouldn't be attempting to verify the tx in this flow - }, - DefaultConfig.ExpectedBloomFilterElements, - DefaultConfig.ExpectedBloomFilterFalsePositiveProbability, - DefaultConfig.MaxBloomFilterFalsePositiveProbability, - ) - require.NoError(err) - + mempool := newMempoolWithVerification(baseMempool, testVerifier{ + err: errors.New("verification failed"), + }) tx := &txs.Tx{ Unsigned: &txs.BaseTx{ BaseTx: avax.BaseTx{ @@ -116,5 +96,5 @@ func TestGossipMempoolAddVerified(t *testing.T) { } require.NoError(mempool.AddWithoutVerification(tx)) - require.True(mempool.bloom.Has(tx)) + require.True(mempool.Has(tx.ID())) } diff --git a/vms/avm/network/network.go b/vms/avm/network/network.go index 45a873acb60e..90ada1db58cc 100644 --- a/vms/avm/network/network.go +++ b/vms/avm/network/network.go @@ -9,6 +9,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" @@ -28,8 +29,10 @@ var ( type Network struct { *p2p.Network - log logging.Logger - mempool *gossipMempool + log logging.Logger + + mempoolWithVerification *mempoolWithVerification + set *gossip.SetWithBloomFilter[*txs.Tx] txPushGossiper *gossip.PushGossiper[*txs.Tx] txPushGossipFrequency time.Duration @@ -76,11 +79,11 @@ func New( return nil, err } - gossipMempool, err := newGossipMempool( - mempool, + mempoolWithVerification := newMempoolWithVerification(mempool, txVerifier) + set, err := gossip.NewSetWithBloomFilter( + mempoolWithVerification, registerer, - log, - txVerifier, + "mempool_bloom_filter", config.ExpectedBloomFilterElements, config.ExpectedBloomFilterFalsePositiveProbability, config.MaxBloomFilterFalsePositiveProbability, @@ -91,7 +94,7 @@ func New( txPushGossiper, err := gossip.NewPushGossiper[*txs.Tx]( marshaller, - gossipMempool, + set, validators, txGossipClient, txGossipMetrics, @@ -115,7 +118,7 @@ func New( var txPullGossiper gossip.Gossiper = gossip.NewPullGossiper[*txs.Tx]( log, marshaller, - gossipMempool, + set, txGossipClient, txGossipMetrics, config.PullGossipPollSize, @@ -131,7 +134,7 @@ func New( handler := gossip.NewHandler[*txs.Tx]( log, marshaller, - gossipMempool, + set, txGossipMetrics, config.TargetGossipSize, ) @@ -167,13 +170,14 @@ func New( } return &Network{ - Network: p2pNetwork, - log: log, - mempool: gossipMempool, - txPushGossiper: txPushGossiper, - txPushGossipFrequency: config.PushGossipFrequency, - txPullGossiper: txPullGossiper, - txPullGossipFrequency: config.PullGossipFrequency, + Network: p2pNetwork, + log: log, + mempoolWithVerification: mempoolWithVerification, + set: set, + txPushGossiper: txPushGossiper, + txPushGossipFrequency: config.PushGossipFrequency, + txPullGossiper: txPullGossiper, + txPullGossipFrequency: config.PullGossipFrequency, }, nil } @@ -193,7 +197,7 @@ func (n *Network) PullGossip(ctx context.Context) { // returned. // If the tx is not added to the mempool, an error will be returned. func (n *Network) IssueTxFromRPC(tx *txs.Tx) error { - if err := n.mempool.Add(tx); err != nil { + if err := n.set.Add(tx); err != nil { return err } n.txPushGossiper.Add(tx) @@ -208,9 +212,14 @@ func (n *Network) IssueTxFromRPC(tx *txs.Tx) error { // returned. // If the tx is not added to the mempool, an error will be returned. func (n *Network) IssueTxFromRPCWithoutVerification(tx *txs.Tx) error { - if err := n.mempool.AddWithoutVerification(tx); err != nil { + if err := n.mempoolWithVerification.AddWithoutVerification(tx); err != nil { return err } + if err := n.set.AddToBloom(tx.ID()); err != nil { + n.log.Warn("adding tx to bloom filter", + zap.Error(err), + ) + } n.txPushGossiper.Add(tx) return nil } diff --git a/vms/platformvm/network/gossip.go b/vms/platformvm/network/gossip.go index 006c1740c57f..9e37fa9a657a 100644 --- a/vms/platformvm/network/gossip.go +++ b/vms/platformvm/network/gossip.go @@ -6,10 +6,8 @@ package network import ( "context" "fmt" - "sync" "time" - "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "github.com/ava-labs/avalanchego/ids" @@ -27,6 +25,7 @@ var ( _ p2p.Handler = (*txGossipHandler)(nil) _ gossip.Marshaller[*txs.Tx] = (*txMarshaller)(nil) _ gossip.Gossipable = (*txs.Tx)(nil) + _ gossip.Set[*txs.Tx] = (*mempoolWithVerification)(nil) ) // bloomChurnMultiplier is the number used to multiply the size of the mempool @@ -67,34 +66,25 @@ func (txMarshaller) UnmarshalGossip(bytes []byte) (*txs.Tx, error) { return txs.Parse(txs.Codec, bytes) } -func newGossipMempool( +func newMempoolWithVerification( mempool *mempool.Mempool, - registerer prometheus.Registerer, log logging.Logger, txVerifier TxVerifier, - minTargetElements int, - targetFalsePositiveProbability, - resetFalsePositiveProbability float64, -) (*gossipMempool, error) { - bloom, err := gossip.NewBloomFilter(registerer, "mempool_bloom_filter", minTargetElements, targetFalsePositiveProbability, resetFalsePositiveProbability) - return &gossipMempool{ +) *mempoolWithVerification { + return &mempoolWithVerification{ Mempool: mempool, log: log, txVerifier: txVerifier, - bloom: bloom, - }, err + } } -type gossipMempool struct { +type mempoolWithVerification struct { *mempool.Mempool log logging.Logger txVerifier TxVerifier - - lock sync.RWMutex - bloom *gossip.BloomFilter } -func (g *gossipMempool) Add(tx *txs.Tx) error { +func (g *mempoolWithVerification) Add(tx *txs.Tx) error { txID := tx.ID() if _, ok := g.Mempool.Get(txID); ok { return fmt.Errorf("tx %s dropped: %w", txID, txmempool.ErrDuplicateTx) @@ -122,37 +112,10 @@ func (g *gossipMempool) Add(tx *txs.Tx) error { g.Mempool.MarkDropped(txID, err) return err } - - g.lock.Lock() - defer g.lock.Unlock() - - g.bloom.Add(tx) - reset, err := gossip.ResetBloomFilterIfNeeded( - g.bloom, - g.Mempool.Len()*bloomChurnMultiplier, - ) - if err != nil { - return err - } - - if reset { - g.log.Debug("resetting bloom filter") - g.Mempool.Iterate(func(tx *txs.Tx) bool { - g.bloom.Add(tx) - return true - }) - } return nil } -func (g *gossipMempool) Has(txID ids.ID) bool { +func (g *mempoolWithVerification) Has(txID ids.ID) bool { _, ok := g.Mempool.Get(txID) return ok } - -func (g *gossipMempool) GetFilter() (bloom []byte, salt []byte) { - g.lock.RLock() - defer g.lock.RUnlock() - - return g.bloom.Marshal() -} diff --git a/vms/platformvm/network/gossip_test.go b/vms/platformvm/network/gossip_test.go index 87121f26961b..28abf6d7f5b8 100644 --- a/vms/platformvm/network/gossip_test.go +++ b/vms/platformvm/network/gossip_test.go @@ -25,7 +25,7 @@ import ( var errFoo = errors.New("foo") // Add should error if verification errors -func TestGossipMempoolAddVerificationError(t *testing.T) { +func TestMempoolWithVerificationAddVerificationError(t *testing.T) { require := require.New(t) txID := ids.GenerateTestID() @@ -43,24 +43,18 @@ func TestGossipMempoolAddVerificationError(t *testing.T) { require.NoError(err) txVerifier := testTxVerifier{err: errFoo} - gossipMempool, err := newGossipMempool( + mempoolWithVerification := newMempoolWithVerification( mempool, - prometheus.NewRegistry(), logging.NoLog{}, txVerifier, - testConfig.ExpectedBloomFilterElements, - testConfig.ExpectedBloomFilterFalsePositiveProbability, - testConfig.MaxBloomFilterFalsePositiveProbability, ) - require.NoError(err) - err = gossipMempool.Add(tx) - require.ErrorIs(err, errFoo) - require.False(gossipMempool.bloom.Has(tx)) + require.ErrorIs(mempoolWithVerification.Add(tx), errFoo) + require.ErrorIs(mempoolWithVerification.GetDropReason(txID), errFoo) } // Adding a duplicate to the mempool should return an error -func TestMempoolDuplicate(t *testing.T) { +func TestMempoolWithVerificationAddDuplicate(t *testing.T) { require := require.New(t) testMempool, err := pmempool.New( @@ -96,69 +90,10 @@ func TestMempoolDuplicate(t *testing.T) { } require.NoError(testMempool.Add(tx)) - gossipMempool, err := newGossipMempool( + mempoolWithVerification := newMempoolWithVerification( testMempool, - prometheus.NewRegistry(), - logging.NoLog{}, - txVerifier, - testConfig.ExpectedBloomFilterElements, - testConfig.ExpectedBloomFilterFalsePositiveProbability, - testConfig.MaxBloomFilterFalsePositiveProbability, - ) - require.NoError(err) - - err = gossipMempool.Add(tx) - require.ErrorIs(err, mempool.ErrDuplicateTx) - require.False(gossipMempool.bloom.Has(tx)) -} - -// Adding a tx to the mempool should add it to the bloom filter -func TestGossipAddBloomFilter(t *testing.T) { - require := require.New(t) - - txID := ids.GenerateTestID() - tx := &txs.Tx{ - Unsigned: &txs.BaseTx{ - BaseTx: avax.BaseTx{ - Ins: []*avax.TransferableInput{ - { - UTXOID: avax.UTXOID{ - TxID: ids.GenerateTestID(), - }, - Asset: avax.Asset{ - ID: snowtest.AVAXAssetID, - }, - In: &secp256k1fx.TransferInput{ - Amt: 1, - }, - }, - }, - }, - }, - TxID: txID, - } - - txVerifier := testTxVerifier{} - mempool, err := pmempool.New( - "", - gas.Dimensions{1, 1, 1, 1}, - 1_000_000, - snowtest.AVAXAssetID, - prometheus.NewRegistry(), - ) - require.NoError(err) - - gossipMempool, err := newGossipMempool( - mempool, - prometheus.NewRegistry(), logging.NoLog{}, txVerifier, - testConfig.ExpectedBloomFilterElements, - testConfig.ExpectedBloomFilterFalsePositiveProbability, - testConfig.MaxBloomFilterFalsePositiveProbability, ) - require.NoError(err) - - require.NoError(gossipMempool.Add(tx)) - require.True(gossipMempool.bloom.Has(tx)) + require.ErrorIs(mempoolWithVerification.Add(tx), mempool.ErrDuplicateTx) } diff --git a/vms/platformvm/network/network.go b/vms/platformvm/network/network.go index c93c8f009aed..3f03e99b6cb4 100644 --- a/vms/platformvm/network/network.go +++ b/vms/platformvm/network/network.go @@ -30,7 +30,7 @@ type Network struct { *p2p.Network log logging.Logger - mempool *gossipMempool + gossipMempool *gossip.SetWithBloomFilter[*txs.Tx] partialSyncPrimaryNetwork bool txPushGossiper *gossip.PushGossiper[*txs.Tx] @@ -81,11 +81,16 @@ func New( return nil, err } - gossipMempool, err := newGossipMempool( + mempoolWithVerification := newMempoolWithVerification( mempool, - registerer, log, txVerifier, + ) + + gossipMempool, err := gossip.NewSetWithBloomFilter( + mempoolWithVerification, + registerer, + "mempool_bloom_filter", config.ExpectedBloomFilterElements, config.ExpectedBloomFilterFalsePositiveProbability, config.MaxBloomFilterFalsePositiveProbability, @@ -185,7 +190,7 @@ func New( return &Network{ Network: p2pNetwork, log: log, - mempool: gossipMempool, + gossipMempool: gossipMempool, partialSyncPrimaryNetwork: partialSyncPrimaryNetwork, txPushGossiper: txPushGossiper, txPushGossipFrequency: config.PushGossipFrequency, @@ -221,7 +226,7 @@ func (n *Network) AppGossip(ctx context.Context, nodeID ids.NodeID, msgBytes []b } func (n *Network) IssueTxFromRPC(tx *txs.Tx) error { - if err := n.mempool.Add(tx); err != nil { + if err := n.gossipMempool.Add(tx); err != nil { return err } n.txPushGossiper.Add(tx) From e402f5568686223ee3e670d22f4fa3a0e0e6a0e7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 16:03:03 -0500 Subject: [PATCH 02/18] nit --- network/p2p/gossip/gossip_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/network/p2p/gossip/gossip_test.go b/network/p2p/gossip/gossip_test.go index 0d42098f85dc..cdabf8793554 100644 --- a/network/p2p/gossip/gossip_test.go +++ b/network/p2p/gossip/gossip_test.go @@ -83,8 +83,8 @@ func (s *setDouble) Add(t tx) error { return nil } -func (s *setDouble) Has(id ids.ID) bool { - return s.txs.Contains(tx(id)) +func (s *setDouble) Has(h ids.ID) bool { + return s.txs.Contains(tx(h)) } func (s *setDouble) Iterate(f func(t tx) bool) { @@ -171,9 +171,9 @@ func TestGossiperGossip(t *testing.T) { ) require.NoError(err) - var serverSet setDouble - serverMempool, err := NewSetWithBloomFilter( - &serverSet, + var responseSet setDouble + responseSetWithBloom, err := NewSetWithBloomFilter( + &responseSet, prometheus.NewRegistry(), "", 1000, @@ -182,7 +182,7 @@ func TestGossiperGossip(t *testing.T) { ) require.NoError(err) for _, item := range tt.responder { - require.NoError(serverMempool.Add(item)) + require.NoError(responseSetWithBloom.Add(item)) } metrics, err := NewMetrics(prometheus.NewRegistry(), "") @@ -197,7 +197,7 @@ func TestGossiperGossip(t *testing.T) { handler := NewHandler[tx]( logging.NoLog{}, marshaller, - serverMempool, + responseSetWithBloom, metrics, tt.targetResponseSize, ) @@ -220,7 +220,7 @@ func TestGossiperGossip(t *testing.T) { require.NoError(requestNetwork.Connected(t.Context(), ids.EmptyNodeID, nil)) var requestSet setDouble - clientMempool, err := NewSetWithBloomFilter( + requestSetWithBloom, err := NewSetWithBloomFilter( &requestSet, prometheus.NewRegistry(), "", @@ -230,7 +230,7 @@ func TestGossiperGossip(t *testing.T) { ) require.NoError(err) for _, item := range tt.requester { - require.NoError(clientMempool.Add(item)) + require.NoError(requestSetWithBloom.Add(item)) } requestClient := requestNetwork.NewClient( @@ -242,7 +242,7 @@ func TestGossiperGossip(t *testing.T) { gossiper := NewPullGossiper[tx]( logging.NoLog{}, marshaller, - clientMempool, + requestSetWithBloom, requestClient, metrics, 1, From 7ad6b7b5c077ead31d02aae3da9c8d50793c6222 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 16:04:26 -0500 Subject: [PATCH 03/18] nit --- network/p2p/gossip/gossip_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/network/p2p/gossip/gossip_test.go b/network/p2p/gossip/gossip_test.go index cdabf8793554..966e86ba3095 100644 --- a/network/p2p/gossip/gossip_test.go +++ b/network/p2p/gossip/gossip_test.go @@ -171,9 +171,8 @@ func TestGossiperGossip(t *testing.T) { ) require.NoError(err) - var responseSet setDouble responseSetWithBloom, err := NewSetWithBloomFilter( - &responseSet, + &setDouble{}, prometheus.NewRegistry(), "", 1000, From 57dbd0b277fec56cd6d07797fa3ececd29481fb8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 16:06:14 -0500 Subject: [PATCH 04/18] nit --- network/p2p/gossip/gossip_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/p2p/gossip/gossip_test.go b/network/p2p/gossip/gossip_test.go index 966e86ba3095..5dbcc8e70958 100644 --- a/network/p2p/gossip/gossip_test.go +++ b/network/p2p/gossip/gossip_test.go @@ -462,10 +462,10 @@ func TestPushGossiperNew(t *testing.T) { } } -type hasFunc func(h ids.ID) bool +type hasFunc func(id ids.ID) bool -func (f hasFunc) Has(h ids.ID) bool { - return f(h) +func (h hasFunc) Has(id ids.ID) bool { + return h(id) } // Tests that the outgoing gossip is equivalent to what was accumulated From df55d7d34e0c0ba9a1cfaced384118a1f1a08008 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 16:28:51 -0500 Subject: [PATCH 05/18] wip update atomic mempool --- .../plugin/evm/atomic/txpool/mempool.go | 46 +--------- .../plugin/evm/atomic/txpool/mempool_test.go | 88 +++---------------- graft/coreth/plugin/evm/atomic/txpool/txs.go | 19 +++- 3 files changed, 29 insertions(+), 124 deletions(-) diff --git a/graft/coreth/plugin/evm/atomic/txpool/mempool.go b/graft/coreth/plugin/evm/atomic/txpool/mempool.go index a28cbdd6f8bd..ca00f07b0c03 100644 --- a/graft/coreth/plugin/evm/atomic/txpool/mempool.go +++ b/graft/coreth/plugin/evm/atomic/txpool/mempool.go @@ -9,10 +9,8 @@ import ( "github.com/ava-labs/libevm/log" "github.com/holiman/uint256" - "github.com/prometheus/client_golang/prometheus" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" - "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/config" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/gossip" ) @@ -39,30 +37,17 @@ var ( // Mempool is a simple mempool for atomic transactions type Mempool struct { *Txs - // bloom is a bloom filter containing the txs in the mempool - bloom *gossip.BloomFilter verify func(tx *atomic.Tx) error } func NewMempool( txs *Txs, - registerer prometheus.Registerer, verify func(tx *atomic.Tx) error, -) (*Mempool, error) { - bloom, err := gossip.NewBloomFilter(registerer, "atomic_mempool_bloom_filter", - config.TxGossipBloomMinTargetElements, - config.TxGossipBloomTargetFalsePositiveRate, - config.TxGossipBloomResetFalsePositiveRate, - ) - if err != nil { - return nil, fmt.Errorf("failed to initialize bloom filter: %w", err) - } - +) *Mempool { return &Mempool{ Txs: txs, - bloom: bloom, verify: verify, - }, nil + } } // Add attempts to add tx to the mempool as a Remote transaction. It is assumed @@ -269,26 +254,6 @@ func (m *Mempool) addTx(tx *atomic.Tx, local bool, force bool) error { m.utxoSpenders[utxoID] = tx } - m.bloom.Add(tx) - reset, err := gossip.ResetBloomFilterIfNeeded(m.bloom, m.length()*config.TxGossipBloomChurnMultiplier) - if err != nil { - return err - } - - if reset { - log.Debug("resetting bloom filter", "reason", "reached max filled ratio") - - for _, pendingTx := range m.pendingTxs.minHeap.items { - m.bloom.Add(pendingTx.tx) - } - // Current transactions must be added to the bloom filter as well - // because they could be added back into the pending set without going - // through addTx again. - for _, currentTx := range m.currentTxs { - m.bloom.Add(currentTx) - } - } - // When adding a transaction to the mempool, we make sure that there is an // item in Pending to signal the VM to produce a block. select { @@ -297,10 +262,3 @@ func (m *Mempool) addTx(tx *atomic.Tx, local bool, force bool) error { } return nil } - -func (m *Mempool) GetFilter() ([]byte, []byte) { - m.lock.RLock() - defer m.lock.RUnlock() - - return m.bloom.Marshal() -} diff --git a/graft/coreth/plugin/evm/atomic/txpool/mempool_test.go b/graft/coreth/plugin/evm/atomic/txpool/mempool_test.go index 65f4872ba8d8..b6408b68ba35 100644 --- a/graft/coreth/plugin/evm/atomic/txpool/mempool_test.go +++ b/graft/coreth/plugin/evm/atomic/txpool/mempool_test.go @@ -4,29 +4,23 @@ package txpool import ( - "math" "testing" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/atomictest" - "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/config" "github.com/ava-labs/avalanchego/snow/snowtest" - "github.com/ava-labs/avalanchego/utils/bloom" ) func TestMempoolAddTx(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - m, err := NewMempool( + m := NewMempool( NewTxs(ctx, 5_000), - prometheus.NewRegistry(), nil, ) - require.NoError(err) txs := make([]*atomic.Tx, 0) for i := 0; i < 3_000; i++ { @@ -34,10 +28,6 @@ func TestMempoolAddTx(t *testing.T) { txs = append(txs, tx) require.NoError(m.Add(tx)) } - - for _, tx := range txs { - require.True(m.bloom.Has(tx)) - } } // Add should return an error if a tx is already known @@ -45,16 +35,14 @@ func TestMempoolAdd(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - m, err := NewMempool( + m := NewMempool( NewTxs(ctx, 5_000), - prometheus.NewRegistry(), nil, ) - require.NoError(err) tx := atomictest.GenerateTestImportTxWithGas(1, 1) require.NoError(m.Add(tx)) - err = m.Add(tx) + err := m.Add(tx) require.ErrorIs(err, ErrAlreadyKnown) } @@ -63,62 +51,16 @@ func TestMempoolAddNoGas(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - m, err := NewMempool( + m := NewMempool( NewTxs(ctx, 5_000), - prometheus.NewRegistry(), nil, ) - require.NoError(err) tx := atomictest.GenerateTestImportTxWithGas(0, 1) - err = m.Add(tx) + err := m.Add(tx) require.ErrorIs(err, atomic.ErrNoGasUsed) } -// Add should return an error if a tx doesn't consume any gas -func TestMempoolAddBloomReset(t *testing.T) { - require := require.New(t) - - ctx := snowtest.Context(t, snowtest.CChainID) - m, err := NewMempool( - NewTxs(ctx, 2), - prometheus.NewRegistry(), - nil, - ) - require.NoError(err) - - maxFeeTx := atomictest.GenerateTestImportTxWithGas(1, math.MaxUint64) - require.NoError(m.Add(maxFeeTx)) - - // Mark maxFeeTx as Current - tx, ok := m.NextTx() - require.True(ok) - require.Equal(maxFeeTx, tx) - - numHashes, numEntries := bloom.OptimalParameters( - config.TxGossipBloomMinTargetElements, - config.TxGossipBloomTargetFalsePositiveRate, - ) - txsToAdd := bloom.EstimateCount( - numHashes, - numEntries, - config.TxGossipBloomResetFalsePositiveRate, - ) - for fee := range txsToAdd { - // Keep increasing the fee to evict older transactions - tx := atomictest.GenerateTestImportTxWithGas(1, uint64(fee)) - require.NoError(m.Add(tx)) - } - - // Mark maxFeeTx as Pending - m.CancelCurrentTxs() - - m.Iterate(func(tx *atomic.Tx) bool { - require.True(m.bloom.Has(tx)) - return true // Iterate over the whole mempool - }) -} - func TestAtomicMempoolIterate(t *testing.T) { txs := []*atomic.Tx{ atomictest.GenerateTestImportTxWithGas(1, 1), @@ -162,12 +104,10 @@ func TestAtomicMempoolIterate(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - m, err := NewMempool( + m := NewMempool( NewTxs(ctx, 10), - prometheus.NewRegistry(), nil, ) - require.NoError(err) for _, add := range tt.add { require.NoError(m.Add(add)) @@ -197,12 +137,10 @@ func TestMempoolMaxSizeHandling(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - mempool, err := NewMempool( + mempool := NewMempool( NewTxs(ctx, 1), - prometheus.NewRegistry(), nil, ) - require.NoError(err) lowFeeTx := atomictest.GenerateTestImportTxWithGas(1, 1) highFeeTx := atomictest.GenerateTestImportTxWithGas(1, 2) @@ -216,7 +154,7 @@ func TestMempoolMaxSizeHandling(t *testing.T) { // Because Current transactions can not be evicted, the mempool should // report full. - err = mempool.Add(highFeeTx) + err := mempool.Add(highFeeTx) require.ErrorIs(err, ErrMempoolFull) // Mark the lowFeeTx as Issued @@ -236,19 +174,17 @@ func TestMempoolPriorityDrop(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - mempool, err := NewMempool( + mempool := NewMempool( NewTxs(ctx, 1), - prometheus.NewRegistry(), nil, ) - require.NoError(err) tx1 := atomictest.GenerateTestImportTxWithGas(1, 2) // lower fee require.NoError(mempool.AddRemoteTx(tx1)) require.True(mempool.Has(tx1.ID())) tx2 := atomictest.GenerateTestImportTxWithGas(1, 2) // lower fee - err = mempool.AddRemoteTx(tx2) + err := mempool.AddRemoteTx(tx2) require.ErrorIs(err, ErrInsufficientFee) require.True(mempool.Has(tx1.ID())) require.False(mempool.Has(tx2.ID())) @@ -266,12 +202,10 @@ func TestMempoolPendingLen(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - mempool, err := NewMempool( + mempool := NewMempool( NewTxs(ctx, 2), - prometheus.NewRegistry(), nil, ) - require.NoError(err) tx1 := atomictest.GenerateTestImportTxWithGas(1, 1) tx2 := atomictest.GenerateTestImportTxWithGas(1, 2) diff --git a/graft/coreth/plugin/evm/atomic/txpool/txs.go b/graft/coreth/plugin/evm/atomic/txpool/txs.go index 7e18dd5f3cb5..4c9150a4370c 100644 --- a/graft/coreth/plugin/evm/atomic/txpool/txs.go +++ b/graft/coreth/plugin/evm/atomic/txpool/txs.go @@ -75,7 +75,7 @@ func NewTxs(ctx *snow.Context, maxSize int) *Txs { } } -// PendingLen returns the number of pending transactions. +// PendingLen returns the number of Pending transactions. func (t *Txs) PendingLen() int { t.lock.RLock() defer t.lock.RUnlock() @@ -83,8 +83,16 @@ func (t *Txs) PendingLen() int { return t.pendingTxs.Len() } -// Iterate applies f to all Pending transactions. If f returns false, the -// iteration stops early. +// Len returns the number of Current and Pending transactions. +func (t *Txs) Len() int { + t.lock.RLock() + defer t.lock.RUnlock() + + return len(t.currentTxs) + t.pendingTxs.Len() +} + +// Iterate applies f to all Current and Pending transactions. If f returns +// false, the iteration stops early. func (t *Txs) Iterate(f func(tx *atomic.Tx) bool) { t.lock.RLock() defer t.lock.RUnlock() @@ -94,6 +102,11 @@ func (t *Txs) Iterate(f func(tx *atomic.Tx) bool) { return } } + for _, tx := range t.currentTxs { + if !f(tx) { + return + } + } } // NextTx returns the highest paying Pending transaction from the mempool and From d24a666b6ff9780a1abe01151365f0bbdad0a035 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 16:29:34 -0500 Subject: [PATCH 06/18] unexport fields --- graft/coreth/plugin/evm/atomic/vm/api.go | 4 +- .../plugin/evm/atomic/vm/block_extension.go | 6 +- .../plugin/evm/atomic/vm/export_tx_test.go | 8 +- .../plugin/evm/atomic/vm/import_tx_test.go | 2 +- .../plugin/evm/atomic/vm/syncervm_test.go | 4 +- .../plugin/evm/atomic/vm/tx_gossip_test.go | 22 ++--- graft/coreth/plugin/evm/atomic/vm/vm.go | 51 +++++----- graft/coreth/plugin/evm/atomic/vm/vm_test.go | 96 +++++++++---------- 8 files changed, 97 insertions(+), 96 deletions(-) diff --git a/graft/coreth/plugin/evm/atomic/vm/api.go b/graft/coreth/plugin/evm/atomic/vm/api.go index 5337f5f987da..526c0a7eacb8 100644 --- a/graft/coreth/plugin/evm/atomic/vm/api.go +++ b/graft/coreth/plugin/evm/atomic/vm/api.go @@ -146,7 +146,7 @@ func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response service.vm.Ctx.Lock.Lock() defer service.vm.Ctx.Lock.Unlock() - err = service.vm.AtomicMempool.AddLocalTx(tx) + err = service.vm.atomicMempool.AddLocalTx(tx) if err != nil && !errors.Is(err, txpool.ErrAlreadyKnown) { return err } @@ -155,7 +155,7 @@ func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response // we push it to the network for inclusion. If the tx was previously added // to the mempool through p2p gossip, this will ensure this node also pushes // it to the network. - service.vm.AtomicTxPushGossiper.Add(tx) + service.vm.atomicTxPushGossiper.Add(tx) return nil } diff --git a/graft/coreth/plugin/evm/atomic/vm/block_extension.go b/graft/coreth/plugin/evm/atomic/vm/block_extension.go index 74a30a11522f..f7a1a5274064 100644 --- a/graft/coreth/plugin/evm/atomic/vm/block_extension.go +++ b/graft/coreth/plugin/evm/atomic/vm/block_extension.go @@ -193,7 +193,7 @@ func (be *blockExtension) Accept(acceptedBatch database.Batch) error { vm := be.blockExtender.vm for _, tx := range be.atomicTxs { // Remove the accepted transaction from the mempool - vm.AtomicMempool.RemoveTx(tx) + vm.atomicMempool.RemoveTx(tx) } // Update VM state for atomic txs in this block. This includes updating the @@ -213,8 +213,8 @@ func (be *blockExtension) Reject() error { vm := be.blockExtender.vm for _, tx := range be.atomicTxs { // Re-issue the transaction in the mempool, continue even if it fails - vm.AtomicMempool.RemoveTx(tx) - if err := vm.AtomicMempool.AddRemoteTx(tx); err != nil { + vm.atomicMempool.RemoveTx(tx) + if err := vm.atomicMempool.AddRemoteTx(tx); err != nil { log.Debug("Failed to re-issue transaction in rejected block", "txID", tx.ID(), "err", err) } } diff --git a/graft/coreth/plugin/evm/atomic/vm/export_tx_test.go b/graft/coreth/plugin/evm/atomic/vm/export_tx_test.go index 4a990a80ec2a..3f101db8afc0 100644 --- a/graft/coreth/plugin/evm/atomic/vm/export_tx_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/export_tx_test.go @@ -66,7 +66,7 @@ func createExportTxOptions(t *testing.T, vm *VM, sharedMemory *avalancheatomic.M importTx, err := vm.newImportTx(vm.Ctx.XChainID, ethAddr, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(t, err) - require.NoError(t, vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(t, vm.atomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(t, err) @@ -363,7 +363,7 @@ func TestExportTxEVMStateTransfer(t *testing.T) { tx, err := vm.newImportTx(vm.Ctx.XChainID, ethAddr, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(t, err) - require.NoError(t, vm.AtomicMempool.AddLocalTx(tx)) + require.NoError(t, vm.atomicMempool.AddLocalTx(tx)) msg, err := tvm.VM.WaitForEvent(t.Context()) require.NoError(t, err) @@ -1609,7 +1609,7 @@ func TestNewExportTx(t *testing.T) { tx, err := vm.newImportTx(vm.Ctx.XChainID, ethAddress, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(t, err) - require.NoError(t, vm.AtomicMempool.AddLocalTx(tx)) + require.NoError(t, vm.atomicMempool.AddLocalTx(tx)) msg, err := tvm.VM.WaitForEvent(t.Context()) require.NoError(t, err) @@ -1760,7 +1760,7 @@ func TestNewExportTxMulticoin(t *testing.T) { tx, err := vm.newImportTx(vm.Ctx.XChainID, ethAddress, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(t, err) - require.NoError(t, vm.AtomicMempool.AddRemoteTx(tx)) + require.NoError(t, vm.atomicMempool.AddRemoteTx(tx)) msg, err := tvm.VM.WaitForEvent(t.Context()) require.NoError(t, err) diff --git a/graft/coreth/plugin/evm/atomic/vm/import_tx_test.go b/graft/coreth/plugin/evm/atomic/vm/import_tx_test.go index 60624f8b5330..ba6b44a0f12b 100644 --- a/graft/coreth/plugin/evm/atomic/vm/import_tx_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/import_tx_test.go @@ -1233,7 +1233,7 @@ func executeTxTest(t *testing.T, test atomicTxTest) { return } - require.NoError(t, vm.AtomicMempool.AddLocalTx(tx)) + require.NoError(t, vm.atomicMempool.AddLocalTx(tx)) if test.bootstrapping { return diff --git a/graft/coreth/plugin/evm/atomic/vm/syncervm_test.go b/graft/coreth/plugin/evm/atomic/vm/syncervm_test.go index 6ba4f315ce21..fffc610ce64c 100644 --- a/graft/coreth/plugin/evm/atomic/vm/syncervm_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/syncervm_test.go @@ -43,7 +43,7 @@ func TestAtomicSyncerVM(t *testing.T) { // spend the UTXOs from shared memory importTx, err := atomicVM.newImportTx(atomicVM.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, vmtest.TestKeys[0:1]) require.NoError(t, err) - require.NoError(t, atomicVM.AtomicMempool.AddLocalTx(importTx)) + require.NoError(t, atomicVM.atomicMempool.AddLocalTx(importTx)) includedAtomicTxs = append(includedAtomicTxs, importTx) case 1: // export some of the imported UTXOs to test exportTx is properly synced @@ -62,7 +62,7 @@ func TestAtomicSyncerVM(t *testing.T) { vmtest.TestKeys[0:1], ) require.NoError(t, err) - require.NoError(t, atomicVM.AtomicMempool.AddLocalTx(exportTx)) + require.NoError(t, atomicVM.atomicMempool.AddLocalTx(exportTx)) includedAtomicTxs = append(includedAtomicTxs, exportTx) default: // Generate simple transfer transactions. pk := vmtest.TestKeys[0].ToECDSA() diff --git a/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go b/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go index 05569bb87738..27f6d6798343 100644 --- a/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go @@ -158,7 +158,7 @@ func TestAtomicTxGossip(t *testing.T) { require.NoError(err) tx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, address, vmtest.InitialBaseFee, secp256k1fx.NewKeychain(pk), []*avax.UTXO{utxo}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(tx)) + require.NoError(vm.atomicMempool.AddLocalTx(tx)) // wait so we aren't throttled by the vm utilstest.SleepWithContext(ctx, 5*time.Second) @@ -240,8 +240,8 @@ func TestAtomicTxPushGossipOutbound(t *testing.T) { require.NoError(err) tx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, address, vmtest.InitialBaseFee, secp256k1fx.NewKeychain(pk), []*avax.UTXO{utxo}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(tx)) - vm.AtomicTxPushGossiper.Add(tx) + require.NoError(vm.atomicMempool.AddLocalTx(tx)) + vm.atomicTxPushGossiper.Add(tx) gossipedBytes := <-sender.SentAppGossip require.Equal(byte(p2p.AtomicTxGossipHandlerID), gossipedBytes[0]) @@ -308,11 +308,11 @@ func TestAtomicTxPushGossipInboundValid(t *testing.T) { require.NoError(err) tx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, address, vmtest.InitialBaseFee, secp256k1fx.NewKeychain(pk), []*avax.UTXO{utxo}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(tx)) + require.NoError(vm.atomicMempool.AddLocalTx(tx)) inboundGossipMsg := buildAtomicPushGossip(t, tx) require.NoError(vm.AppGossip(ctx, ids.EmptyNodeID, inboundGossipMsg)) - require.True(vm.AtomicMempool.Has(tx.ID())) + require.True(vm.atomicMempool.Has(tx.ID())) } // Tests that conflicting txs are handled, based on whether the original tx is @@ -337,22 +337,22 @@ func TestAtomicTxPushGossipInboundConflicting(t *testing.T) { // show that no txID is requested require.NoError(vm.AppGossip(t.Context(), nodeID, msgBytes)) - require.True(vm.AtomicMempool.Has(tx.ID())) + require.True(vm.atomicMempool.Has(tx.ID())) // Try to add a conflicting tx msgBytes = buildAtomicPushGossip(t, conflictingTx) require.NoError(vm.AppGossip(t.Context(), nodeID, msgBytes)) - require.False(vm.AtomicMempool.Has(conflictingTx.ID()), "conflicting tx should not be in the atomic mempool") + require.False(vm.atomicMempool.Has(conflictingTx.ID()), "conflicting tx should not be in the atomic mempool") // Now, show tx as discarded. - vm.AtomicMempool.NextTx() - vm.AtomicMempool.DiscardCurrentTx(tx.ID()) - require.False(vm.AtomicMempool.Has(tx.ID())) + vm.atomicMempool.NextTx() + vm.atomicMempool.DiscardCurrentTx(tx.ID()) + require.False(vm.atomicMempool.Has(tx.ID())) // A conflicting tx can now be added to the mempool msgBytes = buildAtomicPushGossip(t, overridingTx) require.NoError(vm.AppGossip(t.Context(), nodeID, msgBytes)) - require.True(vm.AtomicMempool.Has(overridingTx.ID()), "overriding tx should be in the atomic mempool") + require.True(vm.atomicMempool.Has(overridingTx.ID()), "overriding tx should be in the atomic mempool") } func buildAtomicPushGossip(t *testing.T, tx *atomic.Tx) []byte { diff --git a/graft/coreth/plugin/evm/atomic/vm/vm.go b/graft/coreth/plugin/evm/atomic/vm/vm.go index 53e53918634c..9c6e5abc6e06 100644 --- a/graft/coreth/plugin/evm/atomic/vm/vm.go +++ b/graft/coreth/plugin/evm/atomic/vm/vm.go @@ -82,10 +82,13 @@ type VM struct { Ctx *snow.Context // TODO: unexport these fields - SecpCache *secp256k1.RecoverCache - Fx secp256k1fx.Fx - baseCodec codec.Registry - AtomicMempool *txpool.Mempool + SecpCache *secp256k1.RecoverCache + Fx secp256k1fx.Fx + baseCodec codec.Registry + + atomicMempool *txpool.Mempool + atomicGossipSet avalanchegossip.SetWithBloomFilter[*atomic.Tx] + atomicTxPushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] // AtomicTxRepository maintains two indexes on accepted atomic txs. // - txID to accepted atomic tx @@ -95,8 +98,6 @@ type VM struct { // AtomicBackend abstracts verification and processing of atomic transactions AtomicBackend *atomicstate.AtomicBackend - AtomicTxPushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] - // cancel may be nil until [snow.NormalOp] starts cancel context.CancelFunc shutdownWg sync.WaitGroup @@ -181,7 +182,7 @@ func (vm *VM) Initialize( if err != nil { return fmt.Errorf("failed to initialize mempool: %w", err) } - vm.AtomicMempool = atomicMempool + vm.atomicMempool = atomicMempool // initialize bonus blocks on mainnet var ( @@ -278,9 +279,9 @@ func (vm *VM) onNormalOperationsStarted() error { Peers: vm.InnerVM.Config().PushRegossipNumPeers, } - vm.AtomicTxPushGossiper, err = avalanchegossip.NewPushGossiper[*atomic.Tx]( + vm.atomicTxPushGossiper, err = avalanchegossip.NewPushGossiper[*atomic.Tx]( &atomicTxGossipMarshaller, - vm.AtomicMempool, + vm.atomicMempool, vm.InnerVM.P2PValidators(), atomicTxGossipClient, atomicTxGossipMetrics, @@ -297,7 +298,7 @@ func (vm *VM) onNormalOperationsStarted() error { atomicTxGossipHandler, err := gossip.NewTxGossipHandler[*atomic.Tx]( vm.Ctx.Log, &atomicTxGossipMarshaller, - vm.AtomicMempool, + vm.atomicMempool, atomicTxGossipMetrics, config.TxGossipTargetMessageSize, config.TxGossipThrottlingPeriod, @@ -317,7 +318,7 @@ func (vm *VM) onNormalOperationsStarted() error { atomicTxPullGossiper := avalanchegossip.NewPullGossiper[*atomic.Tx]( vm.Ctx.Log, &atomicTxGossipMarshaller, - vm.AtomicMempool, + vm.atomicMempool, atomicTxGossipClient, atomicTxGossipMetrics, config.TxGossipPollSize, @@ -331,7 +332,7 @@ func (vm *VM) onNormalOperationsStarted() error { vm.shutdownWg.Add(1) go func() { - avalanchegossip.Every(ctx, vm.Ctx.Log, vm.AtomicTxPushGossiper, vm.InnerVM.Config().PushGossipFrequency.Duration) + avalanchegossip.Every(ctx, vm.Ctx.Log, vm.atomicTxPushGossiper, vm.InnerVM.Config().PushGossipFrequency.Duration) vm.shutdownWg.Done() }() @@ -486,7 +487,7 @@ func (vm *VM) createConsensusCallbacks() dummy.ConsensusCallbacks { func (vm *VM) preBatchOnFinalizeAndAssemble(header *types.Header, state *state.StateDB, txs []*types.Transaction) ([]byte, *big.Int, *big.Int, error) { for { - tx, exists := vm.AtomicMempool.NextTx() + tx, exists := vm.atomicMempool.NextTx() if !exists { break } @@ -499,7 +500,7 @@ func (vm *VM) preBatchOnFinalizeAndAssemble(header *types.Header, state *state.S if err := vm.verifyTx(tx, header.ParentHash, header.BaseFee, state, rules); err != nil { // Discard the transaction from the mempool on failed verification. log.Debug("discarding tx from mempool on failed verification", "txID", tx.ID(), "err", err) - vm.AtomicMempool.DiscardCurrentTx(tx.ID()) + vm.atomicMempool.DiscardCurrentTx(tx.ID()) state.RevertToSnapshot(snapshot) continue } @@ -509,7 +510,7 @@ func (vm *VM) preBatchOnFinalizeAndAssemble(header *types.Header, state *state.S // Discard the transaction from the mempool and error if the transaction // cannot be marshalled. This should never happen. log.Debug("discarding tx due to unmarshal err", "txID", tx.ID(), "err", err) - vm.AtomicMempool.DiscardCurrentTx(tx.ID()) + vm.atomicMempool.DiscardCurrentTx(tx.ID()) return nil, nil, nil, fmt.Errorf("failed to marshal atomic transaction %s due to %w", tx.ID(), err) } var contribution, gasUsed *big.Int @@ -552,7 +553,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( } for { - tx, exists := vm.AtomicMempool.NextTx() + tx, exists := vm.atomicMempool.NextTx() if !exists { break } @@ -560,7 +561,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // Ensure that adding [tx] to the block will not exceed the block size soft limit. txSize := len(tx.SignedBytes()) if size+txSize > targetAtomicTxsSize { - vm.AtomicMempool.CancelCurrentTx(tx.ID()) + vm.atomicMempool.CancelCurrentTx(tx.ID()) break } @@ -579,7 +580,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // ensure `gasUsed + batchGasUsed` doesn't exceed `atomicGasLimit` if totalGasUsed := new(big.Int).Add(batchGasUsed, txGasUsed); !utils.BigLessOrEqualUint64(totalGasUsed, atomicGasLimit) { // Send [tx] back to the mempool's tx heap. - vm.AtomicMempool.CancelCurrentTx(tx.ID()) + vm.atomicMempool.CancelCurrentTx(tx.ID()) break } @@ -591,7 +592,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // block will most likely be accepted. // Discard the transaction from the mempool on failed verification. log.Debug("discarding tx due to overlapping input utxos", "txID", tx.ID()) - vm.AtomicMempool.DiscardCurrentTx(tx.ID()) + vm.atomicMempool.DiscardCurrentTx(tx.ID()) continue } @@ -602,7 +603,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // Note: prior to this point, we have not modified [state] so there is no need to // revert to a snapshot if we discard the transaction prior to this point. log.Debug("discarding tx from mempool due to failed verification", "txID", tx.ID(), "err", err) - vm.AtomicMempool.DiscardCurrentTx(tx.ID()) + vm.atomicMempool.DiscardCurrentTx(tx.ID()) state.RevertToSnapshot(snapshot) continue } @@ -623,7 +624,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // If we fail to marshal the batch of atomic transactions for any reason, // discard the entire set of current transactions. log.Debug("discarding txs due to error marshaling atomic transactions", "err", err) - vm.AtomicMempool.DiscardCurrentTxs() + vm.atomicMempool.DiscardCurrentTxs() return nil, nil, nil, fmt.Errorf("failed to marshal batch of atomic transactions due to %w", err) } return atomicTxBytes, batchContribution, batchGasUsed, nil @@ -739,13 +740,13 @@ func (vm *VM) BuildBlockWithContext(ctx context.Context, proposerVMBlockCtx *blo case err == nil: // Marks the current transactions from the mempool as being successfully issued // into a block. - vm.AtomicMempool.IssueCurrentTxs() + vm.atomicMempool.IssueCurrentTxs() case errors.Is(err, vmerrors.ErrGenerateBlockFailed), errors.Is(err, vmerrors.ErrBlockVerificationFailed): log.Debug("cancelling txs due to error generating block", "err", err) - vm.AtomicMempool.CancelCurrentTxs() + vm.atomicMempool.CancelCurrentTxs() case errors.Is(err, vmerrors.ErrWrapBlockFailed): log.Debug("discarding txs due to error making new block", "err", err) - vm.AtomicMempool.DiscardCurrentTxs() + vm.atomicMempool.DiscardCurrentTxs() } return blk, err } @@ -775,7 +776,7 @@ func (vm *VM) GetAtomicTx(txID ids.ID) (*atomic.Tx, atomic.Status, uint64, error } else if err != avalanchedatabase.ErrNotFound { return nil, atomic.Unknown, 0, err } - tx, dropped, found := vm.AtomicMempool.GetTx(txID) + tx, dropped, found := vm.atomicMempool.GetTx(txID) switch { case found && dropped: return tx, atomic.Dropped, 0, nil diff --git a/graft/coreth/plugin/evm/atomic/vm/vm_test.go b/graft/coreth/plugin/evm/atomic/vm/vm_test.go index d7cbc79404bb..0e4aeaad6903 100644 --- a/graft/coreth/plugin/evm/atomic/vm/vm_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/vm_test.go @@ -145,7 +145,7 @@ func testImportMissingUTXOs(t *testing.T, scheme string) { importTx, err := vm1.newImportTx(vm1.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, vmtest.TestKeys[0:1]) require.NoError(err) - require.NoError(vm1.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm1.atomicMempool.AddLocalTx(importTx)) msg, err := vm1.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -202,7 +202,7 @@ func testIssueAtomicTxs(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, vmtest.TestKeys[0:1]) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -225,7 +225,7 @@ func testIssueAtomicTxs(t *testing.T, scheme string) { wrappedState := extstate.New(state) exportTx, err := atomic.NewExportTx(vm.Ctx, vm.CurrentRules(), wrappedState, vm.Ctx.AVAXAssetID, importAmount-(2*ap0.AtomicTxFee), vm.Ctx.XChainID, vmtest.TestShortIDAddrs[0], vmtest.InitialBaseFee, vmtest.TestKeys[0:1]) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(exportTx)) + require.NoError(vm.atomicMempool.AddLocalTx(exportTx)) msg, err = vm.WaitForEvent(t.Context()) require.NoError(err) @@ -288,7 +288,7 @@ func testConflictingImportTxs(t *testing.T, fork upgradetest.Fork, scheme string expectedParentBlkID, err := vm.LastAccepted(t.Context()) require.NoError(err) for _, tx := range importTxs[:2] { - require.NoError(vm.AtomicMempool.AddLocalTx(tx)) + require.NoError(vm.atomicMempool.AddLocalTx(tx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -309,10 +309,10 @@ func testConflictingImportTxs(t *testing.T, fork upgradetest.Fork, scheme string // the VM returns an error when it attempts to issue the conflict into the mempool // and when it attempts to build a block with the conflict force added to the mempool. for i, tx := range conflictTxs[:2] { - err = vm.AtomicMempool.AddLocalTx(tx) + err = vm.atomicMempool.AddLocalTx(tx) require.ErrorIsf(err, ErrConflictingAtomicInputs, "tx index %d", i) // Force issue transaction directly to the mempool - require.NoErrorf(vm.AtomicMempool.ForceAddTx(tx), "force issue failed for tx index %d", i) + require.NoErrorf(vm.atomicMempool.ForceAddTx(tx), "force issue failed for tx index %d", i) msg, err := vm.WaitForEvent(t.Context()) require.NoErrorf(err, "wait for event failed for tx index %d", i) require.Equal(commonEng.PendingTxs, msg) @@ -328,7 +328,7 @@ func testConflictingImportTxs(t *testing.T, fork upgradetest.Fork, scheme string // Generate one more valid block so that we can copy the header to create an invalid block // with modified extra data. This new block will be invalid for more than one reason (invalid merkle root) // so we check to make sure that the expected error is returned from block verification. - require.NoError(vm.AtomicMempool.AddLocalTx(importTxs[2])) + require.NoError(vm.atomicMempool.AddLocalTx(importTxs[2])) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -413,8 +413,8 @@ func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) { require.NoError(t, err) tx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(common.Big2, vmtest.InitialBaseFee), kc, []*avax.UTXO{utxo}) require.NoError(t, err) - require.NoError(t, vm.AtomicMempool.AddLocalTx(tx1)) - require.NoError(t, vm.AtomicMempool.AddLocalTx(tx2)) + require.NoError(t, vm.atomicMempool.AddLocalTx(tx1)) + require.NoError(t, vm.atomicMempool.AddLocalTx(tx2)) return []*atomic.Tx{tx2}, []*atomic.Tx{tx1} }, @@ -427,8 +427,8 @@ func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) { require.NoError(t, err) tx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(common.Big2, vmtest.InitialBaseFee), kc, []*avax.UTXO{utxo1}) require.NoError(t, err) - require.NoError(t, vm.AtomicMempool.AddLocalTx(tx1)) - require.NoError(t, vm.AtomicMempool.AddLocalTx(tx2)) + require.NoError(t, vm.atomicMempool.AddLocalTx(tx1)) + require.NoError(t, vm.atomicMempool.AddLocalTx(tx2)) return []*atomic.Tx{tx2}, []*atomic.Tx{tx1} }, @@ -443,19 +443,19 @@ func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) { require.NoError(t, err) reissuanceTx1, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(big.NewInt(2), vmtest.InitialBaseFee), kc, []*avax.UTXO{utxo1, utxo2}) require.NoError(t, err) - require.NoError(t, vm.AtomicMempool.AddLocalTx(importTx1)) - require.NoError(t, vm.AtomicMempool.AddLocalTx(importTx2)) + require.NoError(t, vm.atomicMempool.AddLocalTx(importTx1)) + require.NoError(t, vm.atomicMempool.AddLocalTx(importTx2)) - err = vm.AtomicMempool.AddLocalTx(reissuanceTx1) + err = vm.atomicMempool.AddLocalTx(reissuanceTx1) require.ErrorIs(t, err, txpool.ErrConflict) - require.True(t, vm.AtomicMempool.Has(importTx1.ID())) - require.True(t, vm.AtomicMempool.Has(importTx2.ID())) - require.False(t, vm.AtomicMempool.Has(reissuanceTx1.ID())) + require.True(t, vm.atomicMempool.Has(importTx1.ID())) + require.True(t, vm.atomicMempool.Has(importTx2.ID())) + require.False(t, vm.atomicMempool.Has(reissuanceTx1.ID())) reissuanceTx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(big.NewInt(4), vmtest.InitialBaseFee), kc, []*avax.UTXO{utxo1, utxo2}) require.NoError(t, err) - require.NoError(t, vm.AtomicMempool.AddLocalTx(reissuanceTx2)) + require.NoError(t, vm.atomicMempool.AddLocalTx(reissuanceTx2)) return []*atomic.Tx{reissuanceTx2}, []*atomic.Tx{importTx1, importTx2} }, @@ -472,12 +472,12 @@ func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) { issuedTxs, evictedTxs := issueTxs(t, vm, tvm.AtomicMemory) for i, tx := range issuedTxs { - _, issued := vm.AtomicMempool.GetPendingTx(tx.ID()) + _, issued := vm.atomicMempool.GetPendingTx(tx.ID()) require.True(t, issued, "expected issued tx at index %d to be issued", i) } for i, tx := range evictedTxs { - _, discarded, _ := vm.AtomicMempool.GetTx(tx.ID()) + _, discarded, _ := vm.atomicMempool.GetTx(tx.ID()) require.True(t, discarded, "expected discarded tx at index %d to be discarded", i) } }) @@ -544,7 +544,7 @@ func testConflictingTransitiveAncestryWithGap(t *testing.T, scheme string) { // Create a conflicting transaction importTx0B, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[2], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key0}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx0A)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx0A)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -569,7 +569,7 @@ func testConflictingTransitiveAncestryWithGap(t *testing.T, scheme string) { importTx1, err := vm.newImportTx(vm.Ctx.XChainID, key.Address, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key1}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx1)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx1)) msg, err = vm.WaitForEvent(t.Context()) require.NoError(err) @@ -580,11 +580,11 @@ func testConflictingTransitiveAncestryWithGap(t *testing.T, scheme string) { require.NoError(blk2.Verify(t.Context())) require.NoError(vm.SetPreference(t.Context(), blk2.ID())) - err = vm.AtomicMempool.AddLocalTx(importTx0B) + err = vm.atomicMempool.AddLocalTx(importTx0B) require.ErrorIs(err, ErrConflictingAtomicInputs) // Force issue transaction directly into the mempool - require.NoError(vm.AtomicMempool.ForceAddTx(importTx0B)) + require.NoError(vm.atomicMempool.ForceAddTx(importTx0B)) msg, err = vm.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -641,7 +641,7 @@ func testBonusBlocksTxs(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -698,7 +698,7 @@ func testReissueAtomicTx(t *testing.T, scheme string) { require.NoError(err) importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -772,7 +772,7 @@ func testAtomicTxFailsEVMStateTransferBuildBlock(t *testing.T, scheme string) { exportTxs := createExportTxOptions(t, vm, tvm.AtomicMemory) exportTx1, exportTx2 := exportTxs[0], exportTxs[1] - require.NoError(vm.AtomicMempool.AddLocalTx(exportTx1)) + require.NoError(vm.atomicMempool.AddLocalTx(exportTx1)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -782,14 +782,14 @@ func testAtomicTxFailsEVMStateTransferBuildBlock(t *testing.T, scheme string) { require.NoError(vm.SetPreference(t.Context(), exportBlk1.ID())) - err = vm.AtomicMempool.AddLocalTx(exportTx2) + err = vm.atomicMempool.AddLocalTx(exportTx2) require.ErrorIs(err, atomic.ErrInvalidNonce) - err = vm.AtomicMempool.AddRemoteTx(exportTx2) + err = vm.atomicMempool.AddRemoteTx(exportTx2) require.ErrorIs(err, atomic.ErrInvalidNonce) // Manually add transaction to mempool to bypass validation - require.NoError(vm.AtomicMempool.ForceAddTx(exportTx2)) + require.NoError(vm.atomicMempool.ForceAddTx(exportTx2)) msg, err = vm.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -824,7 +824,7 @@ func TestConsecutiveAtomicTransactionsRevertSnapshot(t *testing.T) { importTxs := createImportTxOptions(t, vm, tvm.AtomicMemory) // Issue the first import transaction, build, and accept the block. - require.NoError(vm.AtomicMempool.AddLocalTx(importTxs[0])) + require.NoError(vm.atomicMempool.AddLocalTx(importTxs[0])) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -841,9 +841,9 @@ func TestConsecutiveAtomicTransactionsRevertSnapshot(t *testing.T) { // Add the two conflicting transactions directly to the mempool, so that two consecutive transactions // will fail verification when build block is called. - require.NoError(vm.AtomicMempool.ForceAddTx(importTxs[1])) - require.NoError(vm.AtomicMempool.ForceAddTx(importTxs[2])) - require.Equal(2, vm.AtomicMempool.Txs.PendingLen()) + require.NoError(vm.atomicMempool.ForceAddTx(importTxs[1])) + require.NoError(vm.atomicMempool.ForceAddTx(importTxs[2])) + require.Equal(2, vm.atomicMempool.Txs.PendingLen()) _, err = vm.BuildBlock(t.Context()) require.ErrorIs(err, ErrEmptyBlock) @@ -873,14 +873,14 @@ func TestAtomicTxBuildBlockDropsConflicts(t *testing.T) { for index, key := range vmtest.TestKeys { importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[index], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx)) conflictSets[index].Add(importTx.ID()) conflictTx, err := vm.newImportTx(vm.Ctx.XChainID, conflictKey.Address, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(err) - err = vm.AtomicMempool.AddLocalTx(conflictTx) + err = vm.atomicMempool.AddLocalTx(conflictTx) require.ErrorIs(err, txpool.ErrConflict) // force add the tx - require.NoError(vm.AtomicMempool.ForceAddTx(conflictTx)) + require.NoError(vm.atomicMempool.ForceAddTx(conflictTx)) conflictSets[index].Add(conflictTx.ID()) } msg, err := vm.WaitForEvent(t.Context()) @@ -937,7 +937,7 @@ func TestBuildBlockDoesNotExceedAtomicGasLimit(t *testing.T) { importTx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, kc, []*avax.UTXO{utxo}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx)) } msg, err := vm.WaitForEvent(t.Context()) @@ -994,7 +994,7 @@ func TestExtraStateChangeAtomicGasLimitExceeded(t *testing.T) { // used in ApricotPhase5 it still pays a sufficient fee with the fixed fee per atomic transaction. importTx, err := vm1.newImportTx(vm1.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(common.Big2, vmtest.InitialBaseFee), []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm1.AtomicMempool.ForceAddTx(importTx)) + require.NoError(vm1.atomicMempool.ForceAddTx(importTx)) msg, err := vm1.WaitForEvent(t.Context()) require.NoError(err) @@ -1056,7 +1056,7 @@ func testEmptyBlock(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -1147,7 +1147,7 @@ func testBuildApricotPhase5Block(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, address, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -1275,7 +1275,7 @@ func testBuildApricotPhase4Block(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, address, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(vm.atomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -1395,10 +1395,10 @@ func testBuildInvalidBlockHead(t *testing.T, scheme string) { // Verify that the transaction fails verification when attempting to issue // it into the atomic mempool. - err := vm.AtomicMempool.AddLocalTx(tx) + err := vm.atomicMempool.AddLocalTx(tx) require.ErrorIs(err, errFailedToFetchImportUTXOs) // Force issue the transaction directly to the mempool - require.NoError(vm.AtomicMempool.ForceAddTx(tx)) + require.NoError(vm.atomicMempool.ForceAddTx(tx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -1428,7 +1428,7 @@ func TestMempoolAddLocallyCreateAtomicTx(t *testing.T) { defer func() { require.NoError(vm.Shutdown(t.Context())) }() - mempool := vm.AtomicMempool + mempool := vm.atomicMempool // generate a valid and conflicting tx var ( @@ -1445,12 +1445,12 @@ func TestMempoolAddLocallyCreateAtomicTx(t *testing.T) { conflictingTxID := conflictingTx.ID() // add a tx to the mempool - require.NoError(vm.AtomicMempool.AddLocalTx(tx)) + require.NoError(vm.atomicMempool.AddLocalTx(tx)) has := mempool.Has(txID) require.True(has, "valid tx not recorded into mempool") // try to add a conflicting tx - err := vm.AtomicMempool.AddLocalTx(conflictingTx) + err := vm.atomicMempool.AddLocalTx(conflictingTx) require.ErrorIs(err, txpool.ErrConflict) has = mempool.Has(conflictingTxID) require.False(has, "conflicting tx in mempool") @@ -1523,7 +1523,7 @@ func TestWaitForEvent(t *testing.T) { } }() - require.NoError(t, vm.AtomicMempool.AddLocalTx(importTx)) + require.NoError(t, vm.atomicMempool.AddLocalTx(importTx)) r := <-results require.NoError(t, r.err) From ca287e23e53522e1a185baf3048e36b74c7c6782 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 16:41:55 -0500 Subject: [PATCH 07/18] wip --- graft/coreth/plugin/evm/atomic/vm/api.go | 11 +++++++++-- .../plugin/evm/atomic/vm/block_extension.go | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/graft/coreth/plugin/evm/atomic/vm/api.go b/graft/coreth/plugin/evm/atomic/vm/api.go index 526c0a7eacb8..8e9a2db5bba2 100644 --- a/graft/coreth/plugin/evm/atomic/vm/api.go +++ b/graft/coreth/plugin/evm/atomic/vm/api.go @@ -141,7 +141,8 @@ func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response return fmt.Errorf("problem initializing transaction: %w", err) } - response.TxID = tx.ID() + txID := tx.ID() + response.TxID = txID service.vm.Ctx.Lock.Lock() defer service.vm.Ctx.Lock.Unlock() @@ -156,7 +157,13 @@ func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response // to the mempool through p2p gossip, this will ensure this node also pushes // it to the network. service.vm.atomicTxPushGossiper.Add(tx) - return nil + if err != nil { + return nil + } + + // If we just added the tx to the mempool, add it to the gossip bloom + // filter. + return service.vm.atomicGossipSet.AddToBloom(txID) } // GetAtomicTxStatus returns the status of the specified transaction diff --git a/graft/coreth/plugin/evm/atomic/vm/block_extension.go b/graft/coreth/plugin/evm/atomic/vm/block_extension.go index f7a1a5274064..ea47933a2af9 100644 --- a/graft/coreth/plugin/evm/atomic/vm/block_extension.go +++ b/graft/coreth/plugin/evm/atomic/vm/block_extension.go @@ -214,8 +214,21 @@ func (be *blockExtension) Reject() error { for _, tx := range be.atomicTxs { // Re-issue the transaction in the mempool, continue even if it fails vm.atomicMempool.RemoveTx(tx) + + txID := tx.ID() if err := vm.atomicMempool.AddRemoteTx(tx); err != nil { - log.Debug("Failed to re-issue transaction in rejected block", "txID", tx.ID(), "err", err) + log.Debug("Failed to re-issue transaction in rejected block", + "txID", txID, + "err", err, + ) + continue + } + + if err := vm.atomicGossipSet.AddToBloom(txID); err != nil { + log.Debug("Failed to add transaction to gossip bloom in rejected block", + "txID", txID, + "err", err, + ) } } atomicState, err := vm.AtomicBackend.GetVerifiedAtomicState(common.Hash(be.block.ID())) From 07d02012b870984e0517eaf7bc4bff5635c88535 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 16:47:50 -0500 Subject: [PATCH 08/18] fix atomic gossip --- graft/coreth/plugin/evm/atomic/vm/vm.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/graft/coreth/plugin/evm/atomic/vm/vm.go b/graft/coreth/plugin/evm/atomic/vm/vm.go index 9c6e5abc6e06..26fe0870f354 100644 --- a/graft/coreth/plugin/evm/atomic/vm/vm.go +++ b/graft/coreth/plugin/evm/atomic/vm/vm.go @@ -87,7 +87,7 @@ type VM struct { baseCodec codec.Registry atomicMempool *txpool.Mempool - atomicGossipSet avalanchegossip.SetWithBloomFilter[*atomic.Tx] + atomicGossipSet *avalanchegossip.SetWithBloomFilter[*atomic.Tx] atomicTxPushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] // AtomicTxRepository maintains two indexes on accepted atomic txs. @@ -178,11 +178,19 @@ func (vm *VM) Initialize( return fmt.Errorf("failed to initialize inner VM: %w", err) } - atomicMempool, err := txpool.NewMempool(atomicTxs, vm.InnerVM.MetricRegistry(), vm.verifyTxAtTip) + vm.atomicMempool = txpool.NewMempool(atomicTxs, vm.verifyTxAtTip) + atomicGossipSet, err := avalanchegossip.NewSetWithBloomFilter[*atomic.Tx]( + vm.atomicMempool, + vm.InnerVM.MetricRegistry(), + "atomic_mempool_bloom_filter", + config.TxGossipBloomMinTargetElements, + config.TxGossipBloomTargetFalsePositiveRate, + config.TxGossipBloomResetFalsePositiveRate, + ) if err != nil { - return fmt.Errorf("failed to initialize mempool: %w", err) + return fmt.Errorf("failed to initialize atomic gossip set: %w", err) } - vm.atomicMempool = atomicMempool + vm.atomicGossipSet = atomicGossipSet // initialize bonus blocks on mainnet var ( @@ -281,7 +289,7 @@ func (vm *VM) onNormalOperationsStarted() error { vm.atomicTxPushGossiper, err = avalanchegossip.NewPushGossiper[*atomic.Tx]( &atomicTxGossipMarshaller, - vm.atomicMempool, + vm.atomicGossipSet, vm.InnerVM.P2PValidators(), atomicTxGossipClient, atomicTxGossipMetrics, @@ -298,7 +306,7 @@ func (vm *VM) onNormalOperationsStarted() error { atomicTxGossipHandler, err := gossip.NewTxGossipHandler[*atomic.Tx]( vm.Ctx.Log, &atomicTxGossipMarshaller, - vm.atomicMempool, + vm.atomicGossipSet, atomicTxGossipMetrics, config.TxGossipTargetMessageSize, config.TxGossipThrottlingPeriod, @@ -318,7 +326,7 @@ func (vm *VM) onNormalOperationsStarted() error { atomicTxPullGossiper := avalanchegossip.NewPullGossiper[*atomic.Tx]( vm.Ctx.Log, &atomicTxGossipMarshaller, - vm.atomicMempool, + vm.atomicGossipSet, atomicTxGossipClient, atomicTxGossipMetrics, config.TxGossipPollSize, From 1ef2bac7b528e00dc55a2852816df6fc7583c605 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 17:21:57 -0500 Subject: [PATCH 09/18] wip --- graft/coreth/plugin/evm/eth_gossiper.go | 5 ----- graft/coreth/plugin/evm/gossip_test.go | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/graft/coreth/plugin/evm/eth_gossiper.go b/graft/coreth/plugin/evm/eth_gossiper.go index b6129670e012..6bf9b0276bd5 100644 --- a/graft/coreth/plugin/evm/eth_gossiper.go +++ b/graft/coreth/plugin/evm/eth_gossiper.go @@ -66,11 +66,6 @@ type GossipEthTxPool struct { subscribed atomic.Bool } -// IsSubscribed returns whether or not the gossip subscription is active. -func (g *GossipEthTxPool) IsSubscribed() bool { - return g.subscribed.Load() -} - func (g *GossipEthTxPool) Subscribe(ctx context.Context) { sub := g.mempool.SubscribeTransactions(g.pendingTxs, false) if sub == nil { diff --git a/graft/coreth/plugin/evm/gossip_test.go b/graft/coreth/plugin/evm/gossip_test.go index 15523adc24b6..1a58e027292d 100644 --- a/graft/coreth/plugin/evm/gossip_test.go +++ b/graft/coreth/plugin/evm/gossip_test.go @@ -66,7 +66,7 @@ func TestGossipSubscribe(t *testing.T) { go gossipTxPool.Subscribe(ctx) require.Eventually(func() bool { - return gossipTxPool.IsSubscribed() + return gossipTxPool.subscribed.Load() }, 10*time.Second, 500*time.Millisecond, "expected gossipTxPool to be subscribed") // create eth txs From 5fcf9acdcee8aed15a62c6aa5beb3a6cc1394e78 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 19:10:38 -0500 Subject: [PATCH 10/18] wip --- graft/coreth/eth/api_backend.go | 9 +- graft/coreth/eth/backend.go | 2 +- graft/coreth/ethclient/simulated/backend.go | 18 +- graft/coreth/plugin/evm/config/constants.go | 1 - graft/coreth/plugin/evm/eth_gossiper.go | 173 -------------------- graft/coreth/plugin/evm/gossip.go | 104 ++++++++++++ graft/coreth/plugin/evm/gossip_test.go | 57 +------ graft/coreth/plugin/evm/tx_gossip_test.go | 14 +- graft/coreth/plugin/evm/vm.go | 120 +++++++------- 9 files changed, 195 insertions(+), 303 deletions(-) delete mode 100644 graft/coreth/plugin/evm/eth_gossiper.go create mode 100644 graft/coreth/plugin/evm/gossip.go diff --git a/graft/coreth/eth/api_backend.go b/graft/coreth/eth/api_backend.go index 27763269b3bf..0eecd3b3978b 100644 --- a/graft/coreth/eth/api_backend.go +++ b/graft/coreth/eth/api_backend.go @@ -353,14 +353,7 @@ func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) if err := ctx.Err(); err != nil { return err } - if err := b.eth.txPool.Add([]*types.Transaction{signedTx}, true, false)[0]; err != nil { - return err - } - - // We only enqueue transactions for push gossip if they were submitted over the RPC and - // added to the mempool. - b.eth.gossiper.Add(signedTx) - return nil + return b.eth.gossiper.Add(signedTx) } func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) { diff --git a/graft/coreth/eth/backend.go b/graft/coreth/eth/backend.go index 5052055b431f..32cce8bd47a9 100644 --- a/graft/coreth/eth/backend.go +++ b/graft/coreth/eth/backend.go @@ -76,7 +76,7 @@ type Settings struct { // PushGossiper sends pushes pending transactions to peers until they are // removed from the mempool. type PushGossiper interface { - Add(*types.Transaction) + Add(*types.Transaction) error } // Ethereum implements the Ethereum full node service. diff --git a/graft/coreth/ethclient/simulated/backend.go b/graft/coreth/ethclient/simulated/backend.go index 08c4aa63f740..cb52be7b138f 100644 --- a/graft/coreth/ethclient/simulated/backend.go +++ b/graft/coreth/ethclient/simulated/backend.go @@ -35,6 +35,7 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/consensus/dummy" "github.com/ava-labs/avalanchego/graft/coreth/constants" "github.com/ava-labs/avalanchego/graft/coreth/core" + "github.com/ava-labs/avalanchego/graft/coreth/core/txpool" "github.com/ava-labs/avalanchego/graft/coreth/eth" "github.com/ava-labs/avalanchego/graft/coreth/eth/ethconfig" "github.com/ava-labs/avalanchego/graft/coreth/ethclient" @@ -42,6 +43,7 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/node" "github.com/ava-labs/avalanchego/graft/coreth/params" "github.com/ava-labs/avalanchego/graft/coreth/rpc" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/timer/mockable" ethereum "github.com/ava-labs/libevm" "github.com/ava-labs/libevm/common" @@ -51,9 +53,16 @@ import ( var _ eth.PushGossiper = (*fakePushGossiper)(nil) -type fakePushGossiper struct{} +type fakePushGossiper struct { + txpool utils.Atomic[*txpool.TxPool] +} -func (*fakePushGossiper) Add(*types.Transaction) {} +func (f *fakePushGossiper) Add(tx *types.Transaction) error { + if pool := f.txpool.Get(); pool != nil { + return pool.Add([]*types.Transaction{tx}, true, false)[0] + } + return errors.New("tx pool not set") +} // Client exposes the methods provided by the Ethereum RPC client. type Client interface { @@ -134,13 +143,16 @@ func newWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backe engine := dummy.NewCoinbaseFaker() + gossiper := &fakePushGossiper{} backend, err := eth.New( - stack, conf, &fakePushGossiper{}, chaindb, eth.Settings{}, common.Hash{}, + stack, conf, gossiper, chaindb, eth.Settings{}, common.Hash{}, engine, clock, ) if err != nil { return nil, err } + gossiper.txpool.Set(backend.TxPool()) + server := rpc.NewServer(0) for _, api := range backend.APIs() { if err := server.RegisterName(api.Namespace, api.Service); err != nil { diff --git a/graft/coreth/plugin/evm/config/constants.go b/graft/coreth/plugin/evm/config/constants.go index 4fe953def797..e7bedeaa6a7a 100644 --- a/graft/coreth/plugin/evm/config/constants.go +++ b/graft/coreth/plugin/evm/config/constants.go @@ -13,7 +13,6 @@ const ( TxGossipBloomMinTargetElements = 8 * 1024 TxGossipBloomTargetFalsePositiveRate = 0.01 TxGossipBloomResetFalsePositiveRate = 0.05 - TxGossipBloomChurnMultiplier = 3 PushGossipDiscardedElements = 16_384 TxGossipTargetMessageSize = 20 * units.KiB TxGossipThrottlingPeriod = time.Hour diff --git a/graft/coreth/plugin/evm/eth_gossiper.go b/graft/coreth/plugin/evm/eth_gossiper.go deleted file mode 100644 index 6bf9b0276bd5..000000000000 --- a/graft/coreth/plugin/evm/eth_gossiper.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// TODO: move to network - -package evm - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - - "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/log" - "github.com/prometheus/client_golang/prometheus" - - "github.com/ava-labs/avalanchego/graft/coreth/core" - "github.com/ava-labs/avalanchego/graft/coreth/core/txpool" - "github.com/ava-labs/avalanchego/graft/coreth/eth" - "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/config" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/gossip" - - ethcommon "github.com/ava-labs/libevm/common" -) - -const pendingTxsBuffer = 10 - -var ( - _ gossip.Gossipable = (*GossipEthTx)(nil) - _ gossip.Marshaller[*GossipEthTx] = (*GossipEthTxMarshaller)(nil) - _ gossip.Set[*GossipEthTx] = (*GossipEthTxPool)(nil) - - _ eth.PushGossiper = (*EthPushGossiper)(nil) -) - -func NewGossipEthTxPool(mempool *txpool.TxPool, registerer prometheus.Registerer) (*GossipEthTxPool, error) { - bloom, err := gossip.NewBloomFilter( - registerer, - "eth_tx_bloom_filter", - config.TxGossipBloomMinTargetElements, - config.TxGossipBloomTargetFalsePositiveRate, - config.TxGossipBloomResetFalsePositiveRate, - ) - if err != nil { - return nil, fmt.Errorf("failed to initialize bloom filter: %w", err) - } - - return &GossipEthTxPool{ - mempool: mempool, - pendingTxs: make(chan core.NewTxsEvent, pendingTxsBuffer), - bloom: bloom, - }, nil -} - -type GossipEthTxPool struct { - mempool *txpool.TxPool - pendingTxs chan core.NewTxsEvent - - bloom *gossip.BloomFilter - lock sync.RWMutex - - // subscribed is set to true when the gossip subscription is active - // mostly used for testing - subscribed atomic.Bool -} - -func (g *GossipEthTxPool) Subscribe(ctx context.Context) { - sub := g.mempool.SubscribeTransactions(g.pendingTxs, false) - if sub == nil { - log.Warn("failed to subscribe to new txs event") - return - } - g.subscribed.CompareAndSwap(false, true) - defer func() { - sub.Unsubscribe() - g.subscribed.CompareAndSwap(true, false) - }() - - for { - select { - case <-ctx.Done(): - log.Debug("shutting down subscription") - return - case pendingTxs := <-g.pendingTxs: - g.lock.Lock() - optimalElements := (g.mempool.PendingSize(txpool.PendingFilter{}) + len(pendingTxs.Txs)) * config.TxGossipBloomChurnMultiplier - for _, pendingTx := range pendingTxs.Txs { - tx := &GossipEthTx{Tx: pendingTx} - g.bloom.Add(tx) - reset, err := gossip.ResetBloomFilterIfNeeded(g.bloom, optimalElements) - if err != nil { - log.Error("failed to reset bloom filter", "err", err) - continue - } - - if reset { - log.Debug("resetting bloom filter", "reason", "reached max filled ratio") - - g.mempool.IteratePending(func(tx *types.Transaction) bool { - g.bloom.Add(&GossipEthTx{Tx: tx}) - return true - }) - } - } - g.lock.Unlock() - } - } -} - -// Add enqueues the transaction to the mempool. Subscribe should be called -// to receive an event if tx is actually added to the mempool or not. -func (g *GossipEthTxPool) Add(tx *GossipEthTx) error { - return g.mempool.Add([]*types.Transaction{tx.Tx}, false, false)[0] -} - -// Has should just return whether or not the [txID] is still in the mempool, -// not whether it is in the mempool AND pending. -func (g *GossipEthTxPool) Has(txID ids.ID) bool { - return g.mempool.Has(ethcommon.Hash(txID)) -} - -func (g *GossipEthTxPool) Iterate(f func(tx *GossipEthTx) bool) { - g.mempool.IteratePending(func(tx *types.Transaction) bool { - return f(&GossipEthTx{Tx: tx}) - }) -} - -func (g *GossipEthTxPool) GetFilter() ([]byte, []byte) { - g.lock.RLock() - defer g.lock.RUnlock() - - return g.bloom.Marshal() -} - -type GossipEthTxMarshaller struct{} - -func (GossipEthTxMarshaller) MarshalGossip(tx *GossipEthTx) ([]byte, error) { - return tx.Tx.MarshalBinary() -} - -func (GossipEthTxMarshaller) UnmarshalGossip(bytes []byte) (*GossipEthTx, error) { - tx := &GossipEthTx{ - Tx: &types.Transaction{}, - } - - return tx, tx.Tx.UnmarshalBinary(bytes) -} - -type GossipEthTx struct { - Tx *types.Transaction -} - -func (tx *GossipEthTx) GossipID() ids.ID { - return ids.ID(tx.Tx.Hash()) -} - -// EthPushGossiper is used by the ETH backend to push transactions issued over -// the RPC and added to the mempool to peers. -type EthPushGossiper struct { - vm *VM -} - -func (e *EthPushGossiper) Add(tx *types.Transaction) { - // eth.Backend is initialized before the [ethTxPushGossiper] is created, so - // we just ignore any gossip requests until it is set. - ethTxPushGossiper := e.vm.ethTxPushGossiper.Get() - if ethTxPushGossiper == nil { - return - } - ethTxPushGossiper.Add(&GossipEthTx{tx}) -} diff --git a/graft/coreth/plugin/evm/gossip.go b/graft/coreth/plugin/evm/gossip.go new file mode 100644 index 000000000000..ee8685b85d73 --- /dev/null +++ b/graft/coreth/plugin/evm/gossip.go @@ -0,0 +1,104 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// TODO: move to network + +package evm + +import ( + "errors" + + "github.com/ava-labs/libevm/core/types" + + "github.com/ava-labs/avalanchego/graft/coreth/core/txpool" + "github.com/ava-labs/avalanchego/graft/coreth/eth" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p/gossip" + "github.com/ava-labs/avalanchego/utils" + + ethcommon "github.com/ava-labs/libevm/common" +) + +const pendingTxsBuffer = 10 + +var ( + _ gossip.Gossipable = (*gossipTx)(nil) + _ gossip.Marshaller[*gossipTx] = (*gossipTxMarshaller)(nil) + _ gossip.Set[*gossipTx] = (*gossipTxPool)(nil) + + _ eth.PushGossiper = (*atomicPushGossiper)(nil) +) + +func newGossipTxPool(mempool *txpool.TxPool) *gossipTxPool { + return &gossipTxPool{ + mempool: mempool, + } +} + +type gossipTxPool struct { + mempool *txpool.TxPool +} + +// Add enqueues the transaction to the mempool. +func (g *gossipTxPool) Add(tx *gossipTx) error { + return g.mempool.Add([]*types.Transaction{tx.tx}, false, false)[0] +} + +// Has should just return whether or not the [txID] is still in the mempool, +// not whether it is in the mempool AND pending. +func (g *gossipTxPool) Has(txID ids.ID) bool { + return g.mempool.Has(ethcommon.Hash(txID)) +} + +func (g *gossipTxPool) Iterate(f func(tx *gossipTx) bool) { + g.mempool.IteratePending(func(tx *types.Transaction) bool { + return f(&gossipTx{tx: tx}) + }) +} + +func (g *gossipTxPool) Len() int { + return g.mempool.PendingSize(txpool.PendingFilter{}) +} + +type gossipTxMarshaller struct{} + +func (gossipTxMarshaller) MarshalGossip(tx *gossipTx) ([]byte, error) { + return tx.tx.MarshalBinary() +} + +func (gossipTxMarshaller) UnmarshalGossip(bytes []byte) (*gossipTx, error) { + tx := &gossipTx{ + tx: &types.Transaction{}, + } + + return tx, tx.tx.UnmarshalBinary(bytes) +} + +type gossipTx struct { + tx *types.Transaction +} + +func (tx *gossipTx) GossipID() ids.ID { + return ids.ID(tx.tx.Hash()) +} + +// atomicPushGossiper is used by the ETH backend to push transactions issued +// over the RPC and added to the mempool to peers. +type atomicPushGossiper struct { + pusher *utils.Atomic[*gossip.PushGossiper[*gossipTx]] + txPool **txpool.TxPool +} + +func (e *atomicPushGossiper) Add(tx *types.Transaction) error { + // eth.Backend is initialized before the pusher is created, so we just + // ignore any gossip requests until it is set. + pusher := e.pusher.Get() + if pusher == nil { + return errors.New("tx pool not set") + } + if err := (*e.txPool).Add([]*types.Transaction{tx}, true, false)[0]; err != nil { + return err + } + pusher.Add(&gossipTx{tx}) + return nil +} diff --git a/graft/coreth/plugin/evm/gossip_test.go b/graft/coreth/plugin/evm/gossip_test.go index 1a58e027292d..4a0c7f711d2f 100644 --- a/graft/coreth/plugin/evm/gossip_test.go +++ b/graft/coreth/plugin/evm/gossip_test.go @@ -4,7 +4,6 @@ package evm import ( - "context" "crypto/ecdsa" "math/big" "strings" @@ -15,8 +14,6 @@ import ( "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" - "github.com/ava-labs/libevm/crypto" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/graft/coreth/consensus/dummy" @@ -24,16 +21,14 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/core/txpool" "github.com/ava-labs/avalanchego/graft/coreth/core/txpool/legacypool" "github.com/ava-labs/avalanchego/graft/coreth/params" - "github.com/ava-labs/avalanchego/graft/coreth/utils" - "github.com/ava-labs/avalanchego/network/p2p/gossip" ) func TestGossipEthTxMarshaller(t *testing.T) { require := require.New(t) blobTx := &types.BlobTx{} - want := &GossipEthTx{Tx: types.NewTx(blobTx)} - marshaller := GossipEthTxMarshaller{} + want := &gossipTx{tx: types.NewTx(blobTx)} + marshaller := gossipTxMarshaller{} bytes, err := marshaller.MarshalGossip(want) require.NoError(err) @@ -43,54 +38,6 @@ func TestGossipEthTxMarshaller(t *testing.T) { require.Equal(want.GossipID(), got.GossipID()) } -func TestGossipSubscribe(t *testing.T) { - require := require.New(t) - key, err := crypto.GenerateKey() - require.NoError(err) - addr := crypto.PubkeyToAddress(key.PublicKey) - - require.NoError(err) - txPool := setupPoolWithConfig(t, params.TestChainConfig, addr) - defer txPool.Close() - txPool.SetGasTip(common.Big1) - txPool.SetMinFee(common.Big0) - - gossipTxPool, err := NewGossipEthTxPool(txPool, prometheus.NewRegistry()) - require.NoError(err) - - // use a custom bloom filter to test the bloom filter reset - gossipTxPool.bloom, err = gossip.NewBloomFilter(prometheus.NewRegistry(), "", 1, 0.01, 0.0000000000000001) // maxCount =1 - require.NoError(err) - ctx, cancel := context.WithCancel(t.Context()) - defer cancel() - go gossipTxPool.Subscribe(ctx) - - require.Eventually(func() bool { - return gossipTxPool.subscribed.Load() - }, 10*time.Second, 500*time.Millisecond, "expected gossipTxPool to be subscribed") - - // create eth txs - ethTxs := getValidEthTxs(key, 10, big.NewInt(226*utils.GWei)) - - // Notify mempool about txs - errs := txPool.AddRemotesSync(ethTxs) - for _, err := range errs { - require.NoError(err, "failed adding tx to remote mempool") - } - - require.Eventually(func() bool { - gossipTxPool.lock.RLock() - defer gossipTxPool.lock.RUnlock() - - for _, tx := range ethTxs { - if !gossipTxPool.bloom.Has(&GossipEthTx{Tx: tx}) { - return false - } - } - return true - }, 30*time.Second, 500*time.Millisecond, "expected all transactions to eventually be in the bloom filter") -} - func setupPoolWithConfig(t *testing.T, config *params.ChainConfig, fundedAddress common.Address) *txpool.TxPool { diskdb := rawdb.NewMemoryDatabase() engine := dummy.NewETHFaker() diff --git a/graft/coreth/plugin/evm/tx_gossip_test.go b/graft/coreth/plugin/evm/tx_gossip_test.go index 7f08d501969f..cd5bbe7bd000 100644 --- a/graft/coreth/plugin/evm/tx_gossip_test.go +++ b/graft/coreth/plugin/evm/tx_gossip_test.go @@ -151,7 +151,7 @@ func TestEthTxGossip(t *testing.T) { // wait so we aren't throttled by the vm time.Sleep(5 * time.Second) - marshaller := GossipEthTxMarshaller{} + marshaller := gossipTxMarshaller{} // Ask the VM for new transactions. We should get the newly issued tx. wg.Add(1) onResponse = func(_ context.Context, _ ids.NodeID, responseBytes []byte, err error) { @@ -163,7 +163,7 @@ func TestEthTxGossip(t *testing.T) { gotTx, err := marshaller.UnmarshalGossip(response.Gossip[0]) require.NoError(err) - require.Equal(signedTx.Hash(), gotTx.Tx.Hash()) + require.Equal(signedTx.Hash(), gotTx.tx.Hash()) wg.Done() } @@ -213,7 +213,7 @@ func TestEthTxPushGossipOutbound(t *testing.T) { // issue a tx require.NoError(vm.txPool.Add([]*types.Transaction{signedTx}, true, true)[0]) - vm.ethTxPushGossiper.Get().Add(&GossipEthTx{signedTx}) + vm.gossipPusher.Get().Add(&gossipTx{signedTx}) sent := <-sender.SentAppGossip got := &sdk.PushGossip{} @@ -223,7 +223,7 @@ func TestEthTxPushGossipOutbound(t *testing.T) { require.Equal(byte(p2p.TxGossipHandlerID), sent[0]) require.NoError(proto.Unmarshal(sent[1:], got)) - marshaller := GossipEthTxMarshaller{} + marshaller := gossipTxMarshaller{} require.Len(got.Gossip, 1) gossipedTx, err := marshaller.UnmarshalGossip(got.Gossip[0]) require.NoError(err) @@ -266,9 +266,9 @@ func TestEthTxPushGossipInbound(t *testing.T) { signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainID), pk.ToECDSA()) require.NoError(err) - marshaller := GossipEthTxMarshaller{} - gossipedTx := &GossipEthTx{ - Tx: signedTx, + marshaller := gossipTxMarshaller{} + gossipedTx := &gossipTx{ + tx: signedTx, } gossipedTxBytes, err := marshaller.MarshalGossip(gossipedTx) require.NoError(err) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index 44fac1fa0d31..6630f712613f 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -200,6 +200,11 @@ type VM struct { blockChain *core.BlockChain miner *miner.Miner + gossipClient *p2p.Client + gossipMetrics avalanchegossip.Metrics + gossipSet *avalanchegossip.SetWithBloomFilter[*gossipTx] + gossipPusher avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*gossipTx]] + // [versiondb] is the VM's current versioned database versiondb *versiondb.Database @@ -250,8 +255,6 @@ type VM struct { // Used to serve BLS signatures of warp messages over RPC warpBackend warp.Backend - ethTxPushGossiper avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*GossipEthTx]] - chainAlias string // RPC handlers (should be stopped before closing chaindb) rpcHandlers []interface{ Stop() } @@ -462,6 +465,53 @@ func (vm *VM) Initialize( return err } + { + vm.gossipClient = vm.Network.NewClient(p2p.TxGossipHandlerID) + vm.gossipMetrics, err = avalanchegossip.NewMetrics(vm.sdkMetrics, ethTxGossipNamespace) + if err != nil { + return fmt.Errorf("failed to initialize eth tx gossip metrics: %w", err) + } + + vm.gossipSet, err = avalanchegossip.NewSetWithBloomFilter[*gossipTx]( + newGossipTxPool(vm.txPool), + vm.sdkMetrics, + "eth_tx_bloom_filter", + config.TxGossipBloomMinTargetElements, + config.TxGossipBloomTargetFalsePositiveRate, + config.TxGossipBloomResetFalsePositiveRate, + ) + if err != nil { + return fmt.Errorf("failed to create eth tx gossip set: %w", err) + } + + pushGossipParams := avalanchegossip.BranchingFactor{ + StakePercentage: vm.config.PushGossipPercentStake, + Validators: vm.config.PushGossipNumValidators, + Peers: vm.config.PushGossipNumPeers, + } + pushRegossipParams := avalanchegossip.BranchingFactor{ + Validators: vm.config.PushRegossipNumValidators, + Peers: vm.config.PushRegossipNumPeers, + } + + gossipPusher, err := avalanchegossip.NewPushGossiper[*gossipTx]( + gossipTxMarshaller{}, + vm.gossipSet, + vm.P2PValidators(), + vm.gossipClient, + vm.gossipMetrics, + pushGossipParams, + pushRegossipParams, + config.PushGossipDiscardedElements, + config.TxGossipTargetMessageSize, + vm.config.RegossipFrequency.Duration, + ) + if err != nil { + return fmt.Errorf("failed to initialize eth tx push gossiper: %w", err) + } + vm.gossipPusher.Set(gossipPusher) + } + go vm.ctx.Log.RecoverAndPanic(vm.startContinuousProfiler) // Add p2p warp message warpHandler @@ -553,7 +603,9 @@ func (vm *VM) initializeChain(lastAcceptedHash common.Hash) error { vm.eth, err = eth.New( node, &vm.ethConfig, - &EthPushGossiper{vm: vm}, + &atomicPushGossiper{ + pusher: &vm.gossipPusher, + }, vm.chaindb, eth.Settings{MaxBlocksPerRequest: vm.config.MaxBlocksPerRequest}, lastAcceptedHash, @@ -763,48 +815,6 @@ func (vm *VM) initBlockBuilding() error { ctx, cancel := context.WithCancel(context.TODO()) vm.cancel = cancel - ethTxGossipMarshaller := GossipEthTxMarshaller{} - ethTxGossipClient := vm.Network.NewClient(p2p.TxGossipHandlerID) - ethTxGossipMetrics, err := avalanchegossip.NewMetrics(vm.sdkMetrics, ethTxGossipNamespace) - if err != nil { - return fmt.Errorf("failed to initialize eth tx gossip metrics: %w", err) - } - ethTxPool, err := NewGossipEthTxPool(vm.txPool, vm.sdkMetrics) - if err != nil { - return fmt.Errorf("failed to initialize gossip eth tx pool: %w", err) - } - vm.shutdownWg.Add(1) - go func() { - ethTxPool.Subscribe(ctx) - vm.shutdownWg.Done() - }() - pushGossipParams := avalanchegossip.BranchingFactor{ - StakePercentage: vm.config.PushGossipPercentStake, - Validators: vm.config.PushGossipNumValidators, - Peers: vm.config.PushGossipNumPeers, - } - pushRegossipParams := avalanchegossip.BranchingFactor{ - Validators: vm.config.PushRegossipNumValidators, - Peers: vm.config.PushRegossipNumPeers, - } - - ethTxPushGossiper, err := avalanchegossip.NewPushGossiper[*GossipEthTx]( - ethTxGossipMarshaller, - ethTxPool, - vm.P2PValidators(), - ethTxGossipClient, - ethTxGossipMetrics, - pushGossipParams, - pushRegossipParams, - config.PushGossipDiscardedElements, - config.TxGossipTargetMessageSize, - vm.config.RegossipFrequency.Duration, - ) - if err != nil { - return fmt.Errorf("failed to initialize eth tx push gossiper: %w", err) - } - vm.ethTxPushGossiper.Set(ethTxPushGossiper) - // NOTE: gossip network must be initialized first otherwise ETH tx gossip will not work. vm.builderLock.Lock() vm.builder = vm.NewBlockBuilder(vm.extensionConfig.ExtraMempool) @@ -813,11 +823,11 @@ func (vm *VM) initBlockBuilding() error { vm.builder.awaitSubmittedTxs() vm.builderLock.Unlock() - ethTxGossipHandler, err := gossip.NewTxGossipHandler[*GossipEthTx]( + ethTxGossipHandler, err := gossip.NewTxGossipHandler[*gossipTx]( vm.ctx.Log, - ethTxGossipMarshaller, - ethTxPool, - ethTxGossipMetrics, + gossipTxMarshaller{}, + vm.gossipSet, + vm.gossipMetrics, config.TxGossipTargetMessageSize, config.TxGossipThrottlingPeriod, config.TxGossipRequestsPerPeer, @@ -833,12 +843,12 @@ func (vm *VM) initBlockBuilding() error { return fmt.Errorf("failed to add eth tx gossip handler: %w", err) } - ethTxPullGossiper := avalanchegossip.NewPullGossiper[*GossipEthTx]( + ethTxPullGossiper := avalanchegossip.NewPullGossiper[*gossipTx]( vm.ctx.Log, - ethTxGossipMarshaller, - ethTxPool, - ethTxGossipClient, - ethTxGossipMetrics, + gossipTxMarshaller{}, + vm.gossipSet, + vm.gossipClient, + vm.gossipMetrics, config.TxGossipPollSize, ) @@ -850,7 +860,7 @@ func (vm *VM) initBlockBuilding() error { vm.shutdownWg.Add(1) go func() { - avalanchegossip.Every(ctx, vm.ctx.Log, ethTxPushGossiper, vm.config.PushGossipFrequency.Duration) + avalanchegossip.Every(ctx, vm.ctx.Log, vm.gossipPusher.Get(), vm.config.PushGossipFrequency.Duration) vm.shutdownWg.Done() }() vm.shutdownWg.Add(1) From 66a44461369e7d633c33080eda80914c0d8ef53b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 19:17:53 -0500 Subject: [PATCH 11/18] reduce diff --- graft/coreth/eth/api_backend.go | 9 ++++++++- graft/coreth/eth/backend.go | 2 +- graft/coreth/ethclient/simulated/backend.go | 17 +++-------------- graft/coreth/plugin/evm/gossip.go | 13 ++++--------- graft/coreth/plugin/evm/vm.go | 1 + 5 files changed, 17 insertions(+), 25 deletions(-) diff --git a/graft/coreth/eth/api_backend.go b/graft/coreth/eth/api_backend.go index 0eecd3b3978b..27763269b3bf 100644 --- a/graft/coreth/eth/api_backend.go +++ b/graft/coreth/eth/api_backend.go @@ -353,7 +353,14 @@ func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) if err := ctx.Err(); err != nil { return err } - return b.eth.gossiper.Add(signedTx) + if err := b.eth.txPool.Add([]*types.Transaction{signedTx}, true, false)[0]; err != nil { + return err + } + + // We only enqueue transactions for push gossip if they were submitted over the RPC and + // added to the mempool. + b.eth.gossiper.Add(signedTx) + return nil } func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) { diff --git a/graft/coreth/eth/backend.go b/graft/coreth/eth/backend.go index 32cce8bd47a9..5052055b431f 100644 --- a/graft/coreth/eth/backend.go +++ b/graft/coreth/eth/backend.go @@ -76,7 +76,7 @@ type Settings struct { // PushGossiper sends pushes pending transactions to peers until they are // removed from the mempool. type PushGossiper interface { - Add(*types.Transaction) error + Add(*types.Transaction) } // Ethereum implements the Ethereum full node service. diff --git a/graft/coreth/ethclient/simulated/backend.go b/graft/coreth/ethclient/simulated/backend.go index cb52be7b138f..69dc1b268328 100644 --- a/graft/coreth/ethclient/simulated/backend.go +++ b/graft/coreth/ethclient/simulated/backend.go @@ -35,7 +35,6 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/consensus/dummy" "github.com/ava-labs/avalanchego/graft/coreth/constants" "github.com/ava-labs/avalanchego/graft/coreth/core" - "github.com/ava-labs/avalanchego/graft/coreth/core/txpool" "github.com/ava-labs/avalanchego/graft/coreth/eth" "github.com/ava-labs/avalanchego/graft/coreth/eth/ethconfig" "github.com/ava-labs/avalanchego/graft/coreth/ethclient" @@ -43,7 +42,6 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/node" "github.com/ava-labs/avalanchego/graft/coreth/params" "github.com/ava-labs/avalanchego/graft/coreth/rpc" - "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/timer/mockable" ethereum "github.com/ava-labs/libevm" "github.com/ava-labs/libevm/common" @@ -53,16 +51,9 @@ import ( var _ eth.PushGossiper = (*fakePushGossiper)(nil) -type fakePushGossiper struct { - txpool utils.Atomic[*txpool.TxPool] -} +type fakePushGossiper struct{} -func (f *fakePushGossiper) Add(tx *types.Transaction) error { - if pool := f.txpool.Get(); pool != nil { - return pool.Add([]*types.Transaction{tx}, true, false)[0] - } - return errors.New("tx pool not set") -} +func (fakePushGossiper) Add(*types.Transaction) {} // Client exposes the methods provided by the Ethereum RPC client. type Client interface { @@ -143,15 +134,13 @@ func newWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backe engine := dummy.NewCoinbaseFaker() - gossiper := &fakePushGossiper{} backend, err := eth.New( - stack, conf, gossiper, chaindb, eth.Settings{}, common.Hash{}, + stack, conf, fakePushGossiper{}, chaindb, eth.Settings{}, common.Hash{}, engine, clock, ) if err != nil { return nil, err } - gossiper.txpool.Set(backend.TxPool()) server := rpc.NewServer(0) for _, api := range backend.APIs() { diff --git a/graft/coreth/plugin/evm/gossip.go b/graft/coreth/plugin/evm/gossip.go index ee8685b85d73..b26c2ba68e89 100644 --- a/graft/coreth/plugin/evm/gossip.go +++ b/graft/coreth/plugin/evm/gossip.go @@ -6,8 +6,6 @@ package evm import ( - "errors" - "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/avalanchego/graft/coreth/core/txpool" @@ -86,19 +84,16 @@ func (tx *gossipTx) GossipID() ids.ID { // over the RPC and added to the mempool to peers. type atomicPushGossiper struct { pusher *utils.Atomic[*gossip.PushGossiper[*gossipTx]] - txPool **txpool.TxPool + set **gossip.SetWithBloomFilter[*gossipTx] } -func (e *atomicPushGossiper) Add(tx *types.Transaction) error { +func (e *atomicPushGossiper) Add(tx *types.Transaction) { // eth.Backend is initialized before the pusher is created, so we just // ignore any gossip requests until it is set. pusher := e.pusher.Get() if pusher == nil { - return errors.New("tx pool not set") - } - if err := (*e.txPool).Add([]*types.Transaction{tx}, true, false)[0]; err != nil { - return err + return } + _ = (*e.set).AddToBloom(ids.ID(tx.Hash())) pusher.Add(&gossipTx{tx}) - return nil } diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index 6630f712613f..2ac200f7a046 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -605,6 +605,7 @@ func (vm *VM) initializeChain(lastAcceptedHash common.Hash) error { &vm.ethConfig, &atomicPushGossiper{ pusher: &vm.gossipPusher, + set: &vm.gossipSet, }, vm.chaindb, eth.Settings{MaxBlocksPerRequest: vm.config.MaxBlocksPerRequest}, From 23f194ebfb506b98a37a3671856c0cf0bebee0a9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 19:18:50 -0500 Subject: [PATCH 12/18] nit --- graft/coreth/plugin/evm/vm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index 2ac200f7a046..4a65cde9bb35 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -202,7 +202,7 @@ type VM struct { gossipClient *p2p.Client gossipMetrics avalanchegossip.Metrics - gossipSet *avalanchegossip.SetWithBloomFilter[*gossipTx] + gossipSet *avalanchegossip.SetWithBloomFilter[*gossipTx] // gossipSet must be initialized before gossipPusher gossipPusher avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*gossipTx]] // [versiondb] is the VM's current versioned database From fbc0cb1e52ba956bd8180470c2953ade86cd89ae Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 19:19:26 -0500 Subject: [PATCH 13/18] reduce diff --- graft/coreth/ethclient/simulated/backend.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/graft/coreth/ethclient/simulated/backend.go b/graft/coreth/ethclient/simulated/backend.go index 69dc1b268328..08c4aa63f740 100644 --- a/graft/coreth/ethclient/simulated/backend.go +++ b/graft/coreth/ethclient/simulated/backend.go @@ -53,7 +53,7 @@ var _ eth.PushGossiper = (*fakePushGossiper)(nil) type fakePushGossiper struct{} -func (fakePushGossiper) Add(*types.Transaction) {} +func (*fakePushGossiper) Add(*types.Transaction) {} // Client exposes the methods provided by the Ethereum RPC client. type Client interface { @@ -135,13 +135,12 @@ func newWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backe engine := dummy.NewCoinbaseFaker() backend, err := eth.New( - stack, conf, fakePushGossiper{}, chaindb, eth.Settings{}, common.Hash{}, + stack, conf, &fakePushGossiper{}, chaindb, eth.Settings{}, common.Hash{}, engine, clock, ) if err != nil { return nil, err } - server := rpc.NewServer(0) for _, api := range backend.APIs() { if err := server.RegisterName(api.Namespace, api.Service); err != nil { From 3e93da9701feb0ce5002fc347a33f51e2088870a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 19:21:46 -0500 Subject: [PATCH 14/18] reduce diff --- graft/coreth/plugin/evm/atomic/vm/api.go | 2 +- .../plugin/evm/atomic/vm/block_extension.go | 6 +- .../plugin/evm/atomic/vm/export_tx_test.go | 8 +- .../plugin/evm/atomic/vm/import_tx_test.go | 2 +- .../plugin/evm/atomic/vm/syncervm_test.go | 4 +- .../plugin/evm/atomic/vm/tx_gossip_test.go | 20 ++-- graft/coreth/plugin/evm/atomic/vm/vm.go | 32 +++---- graft/coreth/plugin/evm/atomic/vm/vm_test.go | 96 +++++++++---------- 8 files changed, 85 insertions(+), 85 deletions(-) diff --git a/graft/coreth/plugin/evm/atomic/vm/api.go b/graft/coreth/plugin/evm/atomic/vm/api.go index 8e9a2db5bba2..2eaa45131ddb 100644 --- a/graft/coreth/plugin/evm/atomic/vm/api.go +++ b/graft/coreth/plugin/evm/atomic/vm/api.go @@ -147,7 +147,7 @@ func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response service.vm.Ctx.Lock.Lock() defer service.vm.Ctx.Lock.Unlock() - err = service.vm.atomicMempool.AddLocalTx(tx) + err = service.vm.AtomicMempool.AddLocalTx(tx) if err != nil && !errors.Is(err, txpool.ErrAlreadyKnown) { return err } diff --git a/graft/coreth/plugin/evm/atomic/vm/block_extension.go b/graft/coreth/plugin/evm/atomic/vm/block_extension.go index ea47933a2af9..e6ef0f7cfcbe 100644 --- a/graft/coreth/plugin/evm/atomic/vm/block_extension.go +++ b/graft/coreth/plugin/evm/atomic/vm/block_extension.go @@ -193,7 +193,7 @@ func (be *blockExtension) Accept(acceptedBatch database.Batch) error { vm := be.blockExtender.vm for _, tx := range be.atomicTxs { // Remove the accepted transaction from the mempool - vm.atomicMempool.RemoveTx(tx) + vm.AtomicMempool.RemoveTx(tx) } // Update VM state for atomic txs in this block. This includes updating the @@ -213,10 +213,10 @@ func (be *blockExtension) Reject() error { vm := be.blockExtender.vm for _, tx := range be.atomicTxs { // Re-issue the transaction in the mempool, continue even if it fails - vm.atomicMempool.RemoveTx(tx) + vm.AtomicMempool.RemoveTx(tx) txID := tx.ID() - if err := vm.atomicMempool.AddRemoteTx(tx); err != nil { + if err := vm.AtomicMempool.AddRemoteTx(tx); err != nil { log.Debug("Failed to re-issue transaction in rejected block", "txID", txID, "err", err, diff --git a/graft/coreth/plugin/evm/atomic/vm/export_tx_test.go b/graft/coreth/plugin/evm/atomic/vm/export_tx_test.go index 3f101db8afc0..4a990a80ec2a 100644 --- a/graft/coreth/plugin/evm/atomic/vm/export_tx_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/export_tx_test.go @@ -66,7 +66,7 @@ func createExportTxOptions(t *testing.T, vm *VM, sharedMemory *avalancheatomic.M importTx, err := vm.newImportTx(vm.Ctx.XChainID, ethAddr, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(t, err) - require.NoError(t, vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(t, err) @@ -363,7 +363,7 @@ func TestExportTxEVMStateTransfer(t *testing.T) { tx, err := vm.newImportTx(vm.Ctx.XChainID, ethAddr, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(t, err) - require.NoError(t, vm.atomicMempool.AddLocalTx(tx)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(tx)) msg, err := tvm.VM.WaitForEvent(t.Context()) require.NoError(t, err) @@ -1609,7 +1609,7 @@ func TestNewExportTx(t *testing.T) { tx, err := vm.newImportTx(vm.Ctx.XChainID, ethAddress, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(t, err) - require.NoError(t, vm.atomicMempool.AddLocalTx(tx)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(tx)) msg, err := tvm.VM.WaitForEvent(t.Context()) require.NoError(t, err) @@ -1760,7 +1760,7 @@ func TestNewExportTxMulticoin(t *testing.T) { tx, err := vm.newImportTx(vm.Ctx.XChainID, ethAddress, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(t, err) - require.NoError(t, vm.atomicMempool.AddRemoteTx(tx)) + require.NoError(t, vm.AtomicMempool.AddRemoteTx(tx)) msg, err := tvm.VM.WaitForEvent(t.Context()) require.NoError(t, err) diff --git a/graft/coreth/plugin/evm/atomic/vm/import_tx_test.go b/graft/coreth/plugin/evm/atomic/vm/import_tx_test.go index ba6b44a0f12b..60624f8b5330 100644 --- a/graft/coreth/plugin/evm/atomic/vm/import_tx_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/import_tx_test.go @@ -1233,7 +1233,7 @@ func executeTxTest(t *testing.T, test atomicTxTest) { return } - require.NoError(t, vm.atomicMempool.AddLocalTx(tx)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(tx)) if test.bootstrapping { return diff --git a/graft/coreth/plugin/evm/atomic/vm/syncervm_test.go b/graft/coreth/plugin/evm/atomic/vm/syncervm_test.go index fffc610ce64c..6ba4f315ce21 100644 --- a/graft/coreth/plugin/evm/atomic/vm/syncervm_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/syncervm_test.go @@ -43,7 +43,7 @@ func TestAtomicSyncerVM(t *testing.T) { // spend the UTXOs from shared memory importTx, err := atomicVM.newImportTx(atomicVM.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, vmtest.TestKeys[0:1]) require.NoError(t, err) - require.NoError(t, atomicVM.atomicMempool.AddLocalTx(importTx)) + require.NoError(t, atomicVM.AtomicMempool.AddLocalTx(importTx)) includedAtomicTxs = append(includedAtomicTxs, importTx) case 1: // export some of the imported UTXOs to test exportTx is properly synced @@ -62,7 +62,7 @@ func TestAtomicSyncerVM(t *testing.T) { vmtest.TestKeys[0:1], ) require.NoError(t, err) - require.NoError(t, atomicVM.atomicMempool.AddLocalTx(exportTx)) + require.NoError(t, atomicVM.AtomicMempool.AddLocalTx(exportTx)) includedAtomicTxs = append(includedAtomicTxs, exportTx) default: // Generate simple transfer transactions. pk := vmtest.TestKeys[0].ToECDSA() diff --git a/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go b/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go index 27f6d6798343..ad6122aaeb3e 100644 --- a/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go @@ -158,7 +158,7 @@ func TestAtomicTxGossip(t *testing.T) { require.NoError(err) tx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, address, vmtest.InitialBaseFee, secp256k1fx.NewKeychain(pk), []*avax.UTXO{utxo}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(tx)) + require.NoError(vm.AtomicMempool.AddLocalTx(tx)) // wait so we aren't throttled by the vm utilstest.SleepWithContext(ctx, 5*time.Second) @@ -240,7 +240,7 @@ func TestAtomicTxPushGossipOutbound(t *testing.T) { require.NoError(err) tx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, address, vmtest.InitialBaseFee, secp256k1fx.NewKeychain(pk), []*avax.UTXO{utxo}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(tx)) + require.NoError(vm.AtomicMempool.AddLocalTx(tx)) vm.atomicTxPushGossiper.Add(tx) gossipedBytes := <-sender.SentAppGossip @@ -308,11 +308,11 @@ func TestAtomicTxPushGossipInboundValid(t *testing.T) { require.NoError(err) tx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, address, vmtest.InitialBaseFee, secp256k1fx.NewKeychain(pk), []*avax.UTXO{utxo}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(tx)) + require.NoError(vm.AtomicMempool.AddLocalTx(tx)) inboundGossipMsg := buildAtomicPushGossip(t, tx) require.NoError(vm.AppGossip(ctx, ids.EmptyNodeID, inboundGossipMsg)) - require.True(vm.atomicMempool.Has(tx.ID())) + require.True(vm.AtomicMempool.Has(tx.ID())) } // Tests that conflicting txs are handled, based on whether the original tx is @@ -337,22 +337,22 @@ func TestAtomicTxPushGossipInboundConflicting(t *testing.T) { // show that no txID is requested require.NoError(vm.AppGossip(t.Context(), nodeID, msgBytes)) - require.True(vm.atomicMempool.Has(tx.ID())) + require.True(vm.AtomicMempool.Has(tx.ID())) // Try to add a conflicting tx msgBytes = buildAtomicPushGossip(t, conflictingTx) require.NoError(vm.AppGossip(t.Context(), nodeID, msgBytes)) - require.False(vm.atomicMempool.Has(conflictingTx.ID()), "conflicting tx should not be in the atomic mempool") + require.False(vm.AtomicMempool.Has(conflictingTx.ID()), "conflicting tx should not be in the atomic mempool") // Now, show tx as discarded. - vm.atomicMempool.NextTx() - vm.atomicMempool.DiscardCurrentTx(tx.ID()) - require.False(vm.atomicMempool.Has(tx.ID())) + vm.AtomicMempool.NextTx() + vm.AtomicMempool.DiscardCurrentTx(tx.ID()) + require.False(vm.AtomicMempool.Has(tx.ID())) // A conflicting tx can now be added to the mempool msgBytes = buildAtomicPushGossip(t, overridingTx) require.NoError(vm.AppGossip(t.Context(), nodeID, msgBytes)) - require.True(vm.atomicMempool.Has(overridingTx.ID()), "overriding tx should be in the atomic mempool") + require.True(vm.AtomicMempool.Has(overridingTx.ID()), "overriding tx should be in the atomic mempool") } func buildAtomicPushGossip(t *testing.T, tx *atomic.Tx) []byte { diff --git a/graft/coreth/plugin/evm/atomic/vm/vm.go b/graft/coreth/plugin/evm/atomic/vm/vm.go index 26fe0870f354..31c1276f9a4d 100644 --- a/graft/coreth/plugin/evm/atomic/vm/vm.go +++ b/graft/coreth/plugin/evm/atomic/vm/vm.go @@ -86,7 +86,7 @@ type VM struct { Fx secp256k1fx.Fx baseCodec codec.Registry - atomicMempool *txpool.Mempool + AtomicMempool *txpool.Mempool atomicGossipSet *avalanchegossip.SetWithBloomFilter[*atomic.Tx] atomicTxPushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] @@ -178,9 +178,9 @@ func (vm *VM) Initialize( return fmt.Errorf("failed to initialize inner VM: %w", err) } - vm.atomicMempool = txpool.NewMempool(atomicTxs, vm.verifyTxAtTip) + vm.AtomicMempool = txpool.NewMempool(atomicTxs, vm.verifyTxAtTip) atomicGossipSet, err := avalanchegossip.NewSetWithBloomFilter[*atomic.Tx]( - vm.atomicMempool, + vm.AtomicMempool, vm.InnerVM.MetricRegistry(), "atomic_mempool_bloom_filter", config.TxGossipBloomMinTargetElements, @@ -495,7 +495,7 @@ func (vm *VM) createConsensusCallbacks() dummy.ConsensusCallbacks { func (vm *VM) preBatchOnFinalizeAndAssemble(header *types.Header, state *state.StateDB, txs []*types.Transaction) ([]byte, *big.Int, *big.Int, error) { for { - tx, exists := vm.atomicMempool.NextTx() + tx, exists := vm.AtomicMempool.NextTx() if !exists { break } @@ -508,7 +508,7 @@ func (vm *VM) preBatchOnFinalizeAndAssemble(header *types.Header, state *state.S if err := vm.verifyTx(tx, header.ParentHash, header.BaseFee, state, rules); err != nil { // Discard the transaction from the mempool on failed verification. log.Debug("discarding tx from mempool on failed verification", "txID", tx.ID(), "err", err) - vm.atomicMempool.DiscardCurrentTx(tx.ID()) + vm.AtomicMempool.DiscardCurrentTx(tx.ID()) state.RevertToSnapshot(snapshot) continue } @@ -518,7 +518,7 @@ func (vm *VM) preBatchOnFinalizeAndAssemble(header *types.Header, state *state.S // Discard the transaction from the mempool and error if the transaction // cannot be marshalled. This should never happen. log.Debug("discarding tx due to unmarshal err", "txID", tx.ID(), "err", err) - vm.atomicMempool.DiscardCurrentTx(tx.ID()) + vm.AtomicMempool.DiscardCurrentTx(tx.ID()) return nil, nil, nil, fmt.Errorf("failed to marshal atomic transaction %s due to %w", tx.ID(), err) } var contribution, gasUsed *big.Int @@ -561,7 +561,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( } for { - tx, exists := vm.atomicMempool.NextTx() + tx, exists := vm.AtomicMempool.NextTx() if !exists { break } @@ -569,7 +569,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // Ensure that adding [tx] to the block will not exceed the block size soft limit. txSize := len(tx.SignedBytes()) if size+txSize > targetAtomicTxsSize { - vm.atomicMempool.CancelCurrentTx(tx.ID()) + vm.AtomicMempool.CancelCurrentTx(tx.ID()) break } @@ -588,7 +588,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // ensure `gasUsed + batchGasUsed` doesn't exceed `atomicGasLimit` if totalGasUsed := new(big.Int).Add(batchGasUsed, txGasUsed); !utils.BigLessOrEqualUint64(totalGasUsed, atomicGasLimit) { // Send [tx] back to the mempool's tx heap. - vm.atomicMempool.CancelCurrentTx(tx.ID()) + vm.AtomicMempool.CancelCurrentTx(tx.ID()) break } @@ -600,7 +600,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // block will most likely be accepted. // Discard the transaction from the mempool on failed verification. log.Debug("discarding tx due to overlapping input utxos", "txID", tx.ID()) - vm.atomicMempool.DiscardCurrentTx(tx.ID()) + vm.AtomicMempool.DiscardCurrentTx(tx.ID()) continue } @@ -611,7 +611,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // Note: prior to this point, we have not modified [state] so there is no need to // revert to a snapshot if we discard the transaction prior to this point. log.Debug("discarding tx from mempool due to failed verification", "txID", tx.ID(), "err", err) - vm.atomicMempool.DiscardCurrentTx(tx.ID()) + vm.AtomicMempool.DiscardCurrentTx(tx.ID()) state.RevertToSnapshot(snapshot) continue } @@ -632,7 +632,7 @@ func (vm *VM) postBatchOnFinalizeAndAssemble( // If we fail to marshal the batch of atomic transactions for any reason, // discard the entire set of current transactions. log.Debug("discarding txs due to error marshaling atomic transactions", "err", err) - vm.atomicMempool.DiscardCurrentTxs() + vm.AtomicMempool.DiscardCurrentTxs() return nil, nil, nil, fmt.Errorf("failed to marshal batch of atomic transactions due to %w", err) } return atomicTxBytes, batchContribution, batchGasUsed, nil @@ -748,13 +748,13 @@ func (vm *VM) BuildBlockWithContext(ctx context.Context, proposerVMBlockCtx *blo case err == nil: // Marks the current transactions from the mempool as being successfully issued // into a block. - vm.atomicMempool.IssueCurrentTxs() + vm.AtomicMempool.IssueCurrentTxs() case errors.Is(err, vmerrors.ErrGenerateBlockFailed), errors.Is(err, vmerrors.ErrBlockVerificationFailed): log.Debug("cancelling txs due to error generating block", "err", err) - vm.atomicMempool.CancelCurrentTxs() + vm.AtomicMempool.CancelCurrentTxs() case errors.Is(err, vmerrors.ErrWrapBlockFailed): log.Debug("discarding txs due to error making new block", "err", err) - vm.atomicMempool.DiscardCurrentTxs() + vm.AtomicMempool.DiscardCurrentTxs() } return blk, err } @@ -784,7 +784,7 @@ func (vm *VM) GetAtomicTx(txID ids.ID) (*atomic.Tx, atomic.Status, uint64, error } else if err != avalanchedatabase.ErrNotFound { return nil, atomic.Unknown, 0, err } - tx, dropped, found := vm.atomicMempool.GetTx(txID) + tx, dropped, found := vm.AtomicMempool.GetTx(txID) switch { case found && dropped: return tx, atomic.Dropped, 0, nil diff --git a/graft/coreth/plugin/evm/atomic/vm/vm_test.go b/graft/coreth/plugin/evm/atomic/vm/vm_test.go index 0e4aeaad6903..d7cbc79404bb 100644 --- a/graft/coreth/plugin/evm/atomic/vm/vm_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/vm_test.go @@ -145,7 +145,7 @@ func testImportMissingUTXOs(t *testing.T, scheme string) { importTx, err := vm1.newImportTx(vm1.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, vmtest.TestKeys[0:1]) require.NoError(err) - require.NoError(vm1.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm1.AtomicMempool.AddLocalTx(importTx)) msg, err := vm1.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -202,7 +202,7 @@ func testIssueAtomicTxs(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, vmtest.TestKeys[0:1]) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -225,7 +225,7 @@ func testIssueAtomicTxs(t *testing.T, scheme string) { wrappedState := extstate.New(state) exportTx, err := atomic.NewExportTx(vm.Ctx, vm.CurrentRules(), wrappedState, vm.Ctx.AVAXAssetID, importAmount-(2*ap0.AtomicTxFee), vm.Ctx.XChainID, vmtest.TestShortIDAddrs[0], vmtest.InitialBaseFee, vmtest.TestKeys[0:1]) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(exportTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(exportTx)) msg, err = vm.WaitForEvent(t.Context()) require.NoError(err) @@ -288,7 +288,7 @@ func testConflictingImportTxs(t *testing.T, fork upgradetest.Fork, scheme string expectedParentBlkID, err := vm.LastAccepted(t.Context()) require.NoError(err) for _, tx := range importTxs[:2] { - require.NoError(vm.atomicMempool.AddLocalTx(tx)) + require.NoError(vm.AtomicMempool.AddLocalTx(tx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -309,10 +309,10 @@ func testConflictingImportTxs(t *testing.T, fork upgradetest.Fork, scheme string // the VM returns an error when it attempts to issue the conflict into the mempool // and when it attempts to build a block with the conflict force added to the mempool. for i, tx := range conflictTxs[:2] { - err = vm.atomicMempool.AddLocalTx(tx) + err = vm.AtomicMempool.AddLocalTx(tx) require.ErrorIsf(err, ErrConflictingAtomicInputs, "tx index %d", i) // Force issue transaction directly to the mempool - require.NoErrorf(vm.atomicMempool.ForceAddTx(tx), "force issue failed for tx index %d", i) + require.NoErrorf(vm.AtomicMempool.ForceAddTx(tx), "force issue failed for tx index %d", i) msg, err := vm.WaitForEvent(t.Context()) require.NoErrorf(err, "wait for event failed for tx index %d", i) require.Equal(commonEng.PendingTxs, msg) @@ -328,7 +328,7 @@ func testConflictingImportTxs(t *testing.T, fork upgradetest.Fork, scheme string // Generate one more valid block so that we can copy the header to create an invalid block // with modified extra data. This new block will be invalid for more than one reason (invalid merkle root) // so we check to make sure that the expected error is returned from block verification. - require.NoError(vm.atomicMempool.AddLocalTx(importTxs[2])) + require.NoError(vm.AtomicMempool.AddLocalTx(importTxs[2])) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -413,8 +413,8 @@ func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) { require.NoError(t, err) tx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(common.Big2, vmtest.InitialBaseFee), kc, []*avax.UTXO{utxo}) require.NoError(t, err) - require.NoError(t, vm.atomicMempool.AddLocalTx(tx1)) - require.NoError(t, vm.atomicMempool.AddLocalTx(tx2)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(tx1)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(tx2)) return []*atomic.Tx{tx2}, []*atomic.Tx{tx1} }, @@ -427,8 +427,8 @@ func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) { require.NoError(t, err) tx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(common.Big2, vmtest.InitialBaseFee), kc, []*avax.UTXO{utxo1}) require.NoError(t, err) - require.NoError(t, vm.atomicMempool.AddLocalTx(tx1)) - require.NoError(t, vm.atomicMempool.AddLocalTx(tx2)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(tx1)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(tx2)) return []*atomic.Tx{tx2}, []*atomic.Tx{tx1} }, @@ -443,19 +443,19 @@ func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) { require.NoError(t, err) reissuanceTx1, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(big.NewInt(2), vmtest.InitialBaseFee), kc, []*avax.UTXO{utxo1, utxo2}) require.NoError(t, err) - require.NoError(t, vm.atomicMempool.AddLocalTx(importTx1)) - require.NoError(t, vm.atomicMempool.AddLocalTx(importTx2)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(importTx1)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(importTx2)) - err = vm.atomicMempool.AddLocalTx(reissuanceTx1) + err = vm.AtomicMempool.AddLocalTx(reissuanceTx1) require.ErrorIs(t, err, txpool.ErrConflict) - require.True(t, vm.atomicMempool.Has(importTx1.ID())) - require.True(t, vm.atomicMempool.Has(importTx2.ID())) - require.False(t, vm.atomicMempool.Has(reissuanceTx1.ID())) + require.True(t, vm.AtomicMempool.Has(importTx1.ID())) + require.True(t, vm.AtomicMempool.Has(importTx2.ID())) + require.False(t, vm.AtomicMempool.Has(reissuanceTx1.ID())) reissuanceTx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(big.NewInt(4), vmtest.InitialBaseFee), kc, []*avax.UTXO{utxo1, utxo2}) require.NoError(t, err) - require.NoError(t, vm.atomicMempool.AddLocalTx(reissuanceTx2)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(reissuanceTx2)) return []*atomic.Tx{reissuanceTx2}, []*atomic.Tx{importTx1, importTx2} }, @@ -472,12 +472,12 @@ func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) { issuedTxs, evictedTxs := issueTxs(t, vm, tvm.AtomicMemory) for i, tx := range issuedTxs { - _, issued := vm.atomicMempool.GetPendingTx(tx.ID()) + _, issued := vm.AtomicMempool.GetPendingTx(tx.ID()) require.True(t, issued, "expected issued tx at index %d to be issued", i) } for i, tx := range evictedTxs { - _, discarded, _ := vm.atomicMempool.GetTx(tx.ID()) + _, discarded, _ := vm.AtomicMempool.GetTx(tx.ID()) require.True(t, discarded, "expected discarded tx at index %d to be discarded", i) } }) @@ -544,7 +544,7 @@ func testConflictingTransitiveAncestryWithGap(t *testing.T, scheme string) { // Create a conflicting transaction importTx0B, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[2], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key0}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx0A)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx0A)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -569,7 +569,7 @@ func testConflictingTransitiveAncestryWithGap(t *testing.T, scheme string) { importTx1, err := vm.newImportTx(vm.Ctx.XChainID, key.Address, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key1}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx1)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx1)) msg, err = vm.WaitForEvent(t.Context()) require.NoError(err) @@ -580,11 +580,11 @@ func testConflictingTransitiveAncestryWithGap(t *testing.T, scheme string) { require.NoError(blk2.Verify(t.Context())) require.NoError(vm.SetPreference(t.Context(), blk2.ID())) - err = vm.atomicMempool.AddLocalTx(importTx0B) + err = vm.AtomicMempool.AddLocalTx(importTx0B) require.ErrorIs(err, ErrConflictingAtomicInputs) // Force issue transaction directly into the mempool - require.NoError(vm.atomicMempool.ForceAddTx(importTx0B)) + require.NoError(vm.AtomicMempool.ForceAddTx(importTx0B)) msg, err = vm.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -641,7 +641,7 @@ func testBonusBlocksTxs(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -698,7 +698,7 @@ func testReissueAtomicTx(t *testing.T, scheme string) { require.NoError(err) importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -772,7 +772,7 @@ func testAtomicTxFailsEVMStateTransferBuildBlock(t *testing.T, scheme string) { exportTxs := createExportTxOptions(t, vm, tvm.AtomicMemory) exportTx1, exportTx2 := exportTxs[0], exportTxs[1] - require.NoError(vm.atomicMempool.AddLocalTx(exportTx1)) + require.NoError(vm.AtomicMempool.AddLocalTx(exportTx1)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -782,14 +782,14 @@ func testAtomicTxFailsEVMStateTransferBuildBlock(t *testing.T, scheme string) { require.NoError(vm.SetPreference(t.Context(), exportBlk1.ID())) - err = vm.atomicMempool.AddLocalTx(exportTx2) + err = vm.AtomicMempool.AddLocalTx(exportTx2) require.ErrorIs(err, atomic.ErrInvalidNonce) - err = vm.atomicMempool.AddRemoteTx(exportTx2) + err = vm.AtomicMempool.AddRemoteTx(exportTx2) require.ErrorIs(err, atomic.ErrInvalidNonce) // Manually add transaction to mempool to bypass validation - require.NoError(vm.atomicMempool.ForceAddTx(exportTx2)) + require.NoError(vm.AtomicMempool.ForceAddTx(exportTx2)) msg, err = vm.WaitForEvent(t.Context()) require.NoError(err) require.Equal(commonEng.PendingTxs, msg) @@ -824,7 +824,7 @@ func TestConsecutiveAtomicTransactionsRevertSnapshot(t *testing.T) { importTxs := createImportTxOptions(t, vm, tvm.AtomicMemory) // Issue the first import transaction, build, and accept the block. - require.NoError(vm.atomicMempool.AddLocalTx(importTxs[0])) + require.NoError(vm.AtomicMempool.AddLocalTx(importTxs[0])) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -841,9 +841,9 @@ func TestConsecutiveAtomicTransactionsRevertSnapshot(t *testing.T) { // Add the two conflicting transactions directly to the mempool, so that two consecutive transactions // will fail verification when build block is called. - require.NoError(vm.atomicMempool.ForceAddTx(importTxs[1])) - require.NoError(vm.atomicMempool.ForceAddTx(importTxs[2])) - require.Equal(2, vm.atomicMempool.Txs.PendingLen()) + require.NoError(vm.AtomicMempool.ForceAddTx(importTxs[1])) + require.NoError(vm.AtomicMempool.ForceAddTx(importTxs[2])) + require.Equal(2, vm.AtomicMempool.Txs.PendingLen()) _, err = vm.BuildBlock(t.Context()) require.ErrorIs(err, ErrEmptyBlock) @@ -873,14 +873,14 @@ func TestAtomicTxBuildBlockDropsConflicts(t *testing.T) { for index, key := range vmtest.TestKeys { importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[index], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) conflictSets[index].Add(importTx.ID()) conflictTx, err := vm.newImportTx(vm.Ctx.XChainID, conflictKey.Address, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{key}) require.NoError(err) - err = vm.atomicMempool.AddLocalTx(conflictTx) + err = vm.AtomicMempool.AddLocalTx(conflictTx) require.ErrorIs(err, txpool.ErrConflict) // force add the tx - require.NoError(vm.atomicMempool.ForceAddTx(conflictTx)) + require.NoError(vm.AtomicMempool.ForceAddTx(conflictTx)) conflictSets[index].Add(conflictTx.ID()) } msg, err := vm.WaitForEvent(t.Context()) @@ -937,7 +937,7 @@ func TestBuildBlockDoesNotExceedAtomicGasLimit(t *testing.T) { importTx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, kc, []*avax.UTXO{utxo}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) } msg, err := vm.WaitForEvent(t.Context()) @@ -994,7 +994,7 @@ func TestExtraStateChangeAtomicGasLimitExceeded(t *testing.T) { // used in ApricotPhase5 it still pays a sufficient fee with the fixed fee per atomic transaction. importTx, err := vm1.newImportTx(vm1.Ctx.XChainID, vmtest.TestEthAddrs[0], new(big.Int).Mul(common.Big2, vmtest.InitialBaseFee), []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm1.atomicMempool.ForceAddTx(importTx)) + require.NoError(vm1.AtomicMempool.ForceAddTx(importTx)) msg, err := vm1.WaitForEvent(t.Context()) require.NoError(err) @@ -1056,7 +1056,7 @@ func testEmptyBlock(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, vmtest.TestEthAddrs[0], vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -1147,7 +1147,7 @@ func testBuildApricotPhase5Block(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, address, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -1275,7 +1275,7 @@ func testBuildApricotPhase4Block(t *testing.T, scheme string) { importTx, err := vm.newImportTx(vm.Ctx.XChainID, address, vmtest.InitialBaseFee, []*secp256k1.PrivateKey{vmtest.TestKeys[0]}) require.NoError(err) - require.NoError(vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(vm.AtomicMempool.AddLocalTx(importTx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -1395,10 +1395,10 @@ func testBuildInvalidBlockHead(t *testing.T, scheme string) { // Verify that the transaction fails verification when attempting to issue // it into the atomic mempool. - err := vm.atomicMempool.AddLocalTx(tx) + err := vm.AtomicMempool.AddLocalTx(tx) require.ErrorIs(err, errFailedToFetchImportUTXOs) // Force issue the transaction directly to the mempool - require.NoError(vm.atomicMempool.ForceAddTx(tx)) + require.NoError(vm.AtomicMempool.ForceAddTx(tx)) msg, err := vm.WaitForEvent(t.Context()) require.NoError(err) @@ -1428,7 +1428,7 @@ func TestMempoolAddLocallyCreateAtomicTx(t *testing.T) { defer func() { require.NoError(vm.Shutdown(t.Context())) }() - mempool := vm.atomicMempool + mempool := vm.AtomicMempool // generate a valid and conflicting tx var ( @@ -1445,12 +1445,12 @@ func TestMempoolAddLocallyCreateAtomicTx(t *testing.T) { conflictingTxID := conflictingTx.ID() // add a tx to the mempool - require.NoError(vm.atomicMempool.AddLocalTx(tx)) + require.NoError(vm.AtomicMempool.AddLocalTx(tx)) has := mempool.Has(txID) require.True(has, "valid tx not recorded into mempool") // try to add a conflicting tx - err := vm.atomicMempool.AddLocalTx(conflictingTx) + err := vm.AtomicMempool.AddLocalTx(conflictingTx) require.ErrorIs(err, txpool.ErrConflict) has = mempool.Has(conflictingTxID) require.False(has, "conflicting tx in mempool") @@ -1523,7 +1523,7 @@ func TestWaitForEvent(t *testing.T) { } }() - require.NoError(t, vm.atomicMempool.AddLocalTx(importTx)) + require.NoError(t, vm.AtomicMempool.AddLocalTx(importTx)) r := <-results require.NoError(t, r.err) From c488a3a32ae5fe830641caf15bea2082cabf681e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 19:22:21 -0500 Subject: [PATCH 15/18] reduce diff --- graft/coreth/plugin/evm/atomic/vm/api.go | 2 +- graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go | 2 +- graft/coreth/plugin/evm/atomic/vm/vm.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/graft/coreth/plugin/evm/atomic/vm/api.go b/graft/coreth/plugin/evm/atomic/vm/api.go index 2eaa45131ddb..6a2015cfa057 100644 --- a/graft/coreth/plugin/evm/atomic/vm/api.go +++ b/graft/coreth/plugin/evm/atomic/vm/api.go @@ -156,7 +156,7 @@ func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response // we push it to the network for inclusion. If the tx was previously added // to the mempool through p2p gossip, this will ensure this node also pushes // it to the network. - service.vm.atomicTxPushGossiper.Add(tx) + service.vm.AtomicTxPushGossiper.Add(tx) if err != nil { return nil } diff --git a/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go b/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go index ad6122aaeb3e..05569bb87738 100644 --- a/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go +++ b/graft/coreth/plugin/evm/atomic/vm/tx_gossip_test.go @@ -241,7 +241,7 @@ func TestAtomicTxPushGossipOutbound(t *testing.T) { tx, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.clock.Unix(), vm.Ctx.XChainID, address, vmtest.InitialBaseFee, secp256k1fx.NewKeychain(pk), []*avax.UTXO{utxo}) require.NoError(err) require.NoError(vm.AtomicMempool.AddLocalTx(tx)) - vm.atomicTxPushGossiper.Add(tx) + vm.AtomicTxPushGossiper.Add(tx) gossipedBytes := <-sender.SentAppGossip require.Equal(byte(p2p.AtomicTxGossipHandlerID), gossipedBytes[0]) diff --git a/graft/coreth/plugin/evm/atomic/vm/vm.go b/graft/coreth/plugin/evm/atomic/vm/vm.go index 31c1276f9a4d..506d4a924881 100644 --- a/graft/coreth/plugin/evm/atomic/vm/vm.go +++ b/graft/coreth/plugin/evm/atomic/vm/vm.go @@ -88,7 +88,7 @@ type VM struct { AtomicMempool *txpool.Mempool atomicGossipSet *avalanchegossip.SetWithBloomFilter[*atomic.Tx] - atomicTxPushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] + AtomicTxPushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] // AtomicTxRepository maintains two indexes on accepted atomic txs. // - txID to accepted atomic tx @@ -287,7 +287,7 @@ func (vm *VM) onNormalOperationsStarted() error { Peers: vm.InnerVM.Config().PushRegossipNumPeers, } - vm.atomicTxPushGossiper, err = avalanchegossip.NewPushGossiper[*atomic.Tx]( + vm.AtomicTxPushGossiper, err = avalanchegossip.NewPushGossiper[*atomic.Tx]( &atomicTxGossipMarshaller, vm.atomicGossipSet, vm.InnerVM.P2PValidators(), @@ -340,7 +340,7 @@ func (vm *VM) onNormalOperationsStarted() error { vm.shutdownWg.Add(1) go func() { - avalanchegossip.Every(ctx, vm.Ctx.Log, vm.atomicTxPushGossiper, vm.InnerVM.Config().PushGossipFrequency.Duration) + avalanchegossip.Every(ctx, vm.Ctx.Log, vm.AtomicTxPushGossiper, vm.InnerVM.Config().PushGossipFrequency.Duration) vm.shutdownWg.Done() }() From cfb128661e710ff689981c41d07e4e6e7d939781 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 19:23:44 -0500 Subject: [PATCH 16/18] reduce diff --- graft/coreth/plugin/evm/gossip.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/graft/coreth/plugin/evm/gossip.go b/graft/coreth/plugin/evm/gossip.go index b26c2ba68e89..a0def62a3614 100644 --- a/graft/coreth/plugin/evm/gossip.go +++ b/graft/coreth/plugin/evm/gossip.go @@ -17,8 +17,6 @@ import ( ethcommon "github.com/ava-labs/libevm/common" ) -const pendingTxsBuffer = 10 - var ( _ gossip.Gossipable = (*gossipTx)(nil) _ gossip.Marshaller[*gossipTx] = (*gossipTxMarshaller)(nil) From dfc464e6062163fff610c05cd4ae84a5ef5725eb Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 25 Nov 2025 19:39:54 -0500 Subject: [PATCH 17/18] Remove subnet-evm to test in CI --- go.mod | 3 +- go.sum | 3 -- vms/evm/emulate/emulate.go | 46 ----------------- vms/evm/emulate/emulate_test.go | 88 --------------------------------- 4 files changed, 2 insertions(+), 138 deletions(-) delete mode 100644 vms/evm/emulate/emulate.go delete mode 100644 vms/evm/emulate/emulate_test.go diff --git a/go.mod b/go.mod index 1ae9b6401d26..a610d8369767 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( github.com/antithesishq/antithesis-sdk-go v0.3.8 github.com/ava-labs/avalanchego/graft/coreth v0.16.0-rc.0 github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 - github.com/ava-labs/subnet-evm v0.8.1-0.20251124174652-9114d48a927d github.com/btcsuite/btcd/btcutil v1.1.3 github.com/cespare/xxhash/v2 v2.3.0 github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 @@ -86,6 +85,8 @@ require ( k8s.io/utils v0.0.0-20230726121419-3b25d923346b ) +require github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect diff --git a/go.sum b/go.sum index c0068716ca84..7a7f0682bce3 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,6 @@ github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 h1:hQ15IJxY7WO github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 h1:S6oFasZsplNmw8B2S8cMJQMa62nT5ZKGzZRdCpd+5qQ= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19/go.mod h1:GVzumIo3zR23/qGRN2AdnVkIPHcKMq/D89EGWZfMGQ0= -github.com/ava-labs/subnet-evm v0.8.1-0.20251124174652-9114d48a927d h1:7pjEE0BXLjzQlq5uKP5B2BTl9jTgDKaOfJx2Qfb01Jo= -github.com/ava-labs/subnet-evm v0.8.1-0.20251124174652-9114d48a927d/go.mod h1:JTvIe8YbCjHpy8vy9uZBSpDXxawNXSnIe/Wlf3I09Tk= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -274,7 +272,6 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= diff --git a/vms/evm/emulate/emulate.go b/vms/evm/emulate/emulate.go deleted file mode 100644 index bcd3a526f263..000000000000 --- a/vms/evm/emulate/emulate.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package emulate provides temporary emulation of coreth (C-Chain) and -// subnet-evm (EVM L1) behaviours. All functions are safe for concurrent use -// with each other, but all hold the same mutex so their execution SHOULD be -// short-lived. -package emulate - -import ( - cchain "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm" - subnet "github.com/ava-labs/subnet-evm/plugin/evm" -) - -// CChain executes `fn` as if running in a `coreth` node. -func CChain(fn func() error) error { - return cchain.WithTempRegisteredLibEVMExtras(fn) -} - -// SubnetEVM executes `fn` as if running in a `subnet-evm` node. -func SubnetEVM(fn func() error) error { - return subnet.WithTempRegisteredLibEVMExtras(fn) -} - -// CChainVal executes `fn` as if running in a `coreth` node. -func CChainVal[T any](fn func() (T, error)) (T, error) { - return val(CChain, fn) -} - -// SubnetEVMVal executes `fn` as if running in a `subnet-evm` node. -func SubnetEVMVal[T any](fn func() (T, error)) (T, error) { - return val(SubnetEVM, fn) -} - -func val[T any]( - wrap func(func() error) error, - fn func() (T, error), -) (T, error) { - var v T - err := wrap(func() error { - var err error - v, err = fn() - return err - }) - return v, err -} diff --git a/vms/evm/emulate/emulate_test.go b/vms/evm/emulate/emulate_test.go deleted file mode 100644 index a3f0e9d080d3..000000000000 --- a/vms/evm/emulate/emulate_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package emulate - -import ( - "errors" - "sync/atomic" - "testing" - - "github.com/ava-labs/libevm/core/types" - "github.com/stretchr/testify/require" - - cchain "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" - subnet "github.com/ava-labs/subnet-evm/plugin/evm/customtypes" -) - -// setAndGetMillis is an arbitrary function that can be run if and only if -// emulating either `coreth` or `subnet-evm`. If the respective emulation isn't -// active then it will cause `libevm` to panic. In addition to the panicking -// behaviour, it asserts that it is the only active emulation. -func setAndGetMillis[T *cchain.HeaderExtra | *subnet.HeaderExtra]( - t *testing.T, - active *atomic.Int64, - withExtra func(*types.Header, T) *types.Header, - extra T, - blockMillis func(*types.Block) *uint64, - retErr error, -) func() (*uint64, error) { - return func() (*uint64, error) { - require.True(t, active.CompareAndSwap(0, 1)) - defer func() { - require.True(t, active.CompareAndSwap(1, 0)) - }() - - b := types.NewBlockWithHeader(withExtra( - &types.Header{}, - extra, - )) - return blockMillis(b), retErr - } -} - -func TestEmulation(t *testing.T) { - const milli = uint64(1234) - newUint64 := func(u uint64) *uint64 { return &u } - sentinel := errors.New("uh oh") - - var active atomic.Int64 - onCChain := setAndGetMillis( - t, &active, - cchain.WithHeaderExtra, - &cchain.HeaderExtra{TimeMilliseconds: newUint64(milli)}, - cchain.BlockTimeMilliseconds, - sentinel, - ) - onSubnetEVM := setAndGetMillis( - t, &active, - subnet.WithHeaderExtra, - &subnet.HeaderExtra{TimeMilliseconds: newUint64(milli)}, - subnet.BlockTimeMilliseconds, - sentinel, - ) - - start := make(chan struct{}) - - t.Run("coreth", func(t *testing.T) { - t.Parallel() - <-start - for range 1000 { - got, err := CChainVal(onCChain) - require.ErrorIs(t, err, sentinel) - require.Equal(t, milli, *got) - } - }) - - t.Run("subnet-evm", func(t *testing.T) { - t.Parallel() - <-start - for range 1000 { - got, err := SubnetEVMVal(onSubnetEVM) - require.ErrorIs(t, err, sentinel) - require.Equal(t, milli, *got) - } - }) - - close(start) -} From c6439f2d55dcc4baf5380c6a58f7a25a1c110f85 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 26 Nov 2025 12:41:52 -0500 Subject: [PATCH 18/18] Skip incrementing bloom count after noop Add --- network/ip_tracker.go | 7 ++++--- network/p2p/gossip/bloom.go | 5 +++-- utils/bloom/filter.go | 16 +++++++++++++--- utils/bloom/filter_test.go | 15 ++++++++++++++- utils/bloom/hasher.go | 4 ++-- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/network/ip_tracker.go b/network/ip_tracker.go index a660fabf25de..6dfd25a831fc 100644 --- a/network/ip_tracker.go +++ b/network/ip_tracker.go @@ -552,9 +552,10 @@ func (i *ipTracker) updateMostRecentTrackedIP(node *trackedNode, ip *ips.Claimed return } - i.bloomAdditions[ip.NodeID] = oldCount + 1 - bloom.Add(i.bloom, ip.GossipID[:], i.bloomSalt) - i.bloomMetrics.Count.Inc() + if bloom.Add(i.bloom, ip.GossipID[:], i.bloomSalt) { + i.bloomAdditions[ip.NodeID] = oldCount + 1 + i.bloomMetrics.Count.Inc() + } } // ResetBloom prunes the current bloom filter. This must be called periodically diff --git a/network/p2p/gossip/bloom.go b/network/p2p/gossip/bloom.go index 9c05c8db78e0..c46e0776215c 100644 --- a/network/p2p/gossip/bloom.go +++ b/network/p2p/gossip/bloom.go @@ -62,8 +62,9 @@ type BloomFilter struct { func (b *BloomFilter) Add(gossipable Gossipable) { h := gossipable.GossipID() - bloom.Add(b.bloom, h[:], b.salt[:]) - b.metrics.Count.Inc() + if bloom.Add(b.bloom, h[:], b.salt[:]) { + b.metrics.Count.Inc() + } } func (b *BloomFilter) Has(gossipable Gossipable) bool { diff --git a/utils/bloom/filter.go b/utils/bloom/filter.go index d139caa52d69..487db11edbe6 100644 --- a/utils/bloom/filter.go +++ b/utils/bloom/filter.go @@ -59,19 +59,29 @@ func New(numHashes, numEntries int) (*Filter, error) { }, nil } -func (f *Filter) Add(hash uint64) { +// Add adds the provided hash to the bloom filter. It returns true if the hash +// was not already present in the bloom filter. +func (f *Filter) Add(hash uint64) bool { f.lock.Lock() defer f.lock.Unlock() - _ = 1 % f.numBits // hint to the compiler that numBits is not 0 + var ( + _ = 1 % f.numBits // hint to the compiler that numBits is not 0 + accumulator byte = 1 + ) for _, seed := range f.hashSeeds { hash = bits.RotateLeft64(hash, hashRotation) ^ seed index := hash % f.numBits byteIndex := index / bitsPerByte bitIndex := index % bitsPerByte + accumulator &= f.entries[byteIndex] >> bitIndex f.entries[byteIndex] |= 1 << bitIndex } - f.count++ + added := accumulator == 0 + if added { + f.count++ + } + return added } // Count returns the number of elements that have been added to the bloom diff --git a/utils/bloom/filter_test.go b/utils/bloom/filter_test.go index f8816e2cb89d..44937f66b549 100644 --- a/utils/bloom/filter_test.go +++ b/utils/bloom/filter_test.go @@ -42,6 +42,19 @@ func TestNewErrors(t *testing.T) { } } +func TestAdd(t *testing.T) { + require := require.New(t) + + initialNumHashes, initialNumBytes := OptimalParameters(1024, 0.01) + filter, err := New(initialNumHashes, initialNumBytes) + require.NoError(err) + + require.True(filter.Add(1)) + require.Equal(1, filter.Count()) + require.False(filter.Add(1)) + require.Equal(1, filter.Count()) +} + func TestNormalUsage(t *testing.T) { require := require.New(t) @@ -61,7 +74,7 @@ func TestNormalUsage(t *testing.T) { } } - require.Equal(len(toAdd), filter.Count()) + require.LessOrEqual(filter.Count(), len(toAdd)) filterBytes := filter.Marshal() parsedFilter, err := Parse(filterBytes) diff --git a/utils/bloom/hasher.go b/utils/bloom/hasher.go index 7b6441472c92..fca81a372b14 100644 --- a/utils/bloom/hasher.go +++ b/utils/bloom/hasher.go @@ -8,8 +8,8 @@ import ( "encoding/binary" ) -func Add(f *Filter, key, salt []byte) { - f.Add(Hash(key, salt)) +func Add(f *Filter, key, salt []byte) bool { + return f.Add(Hash(key, salt)) } func Contains(c Checker, key, salt []byte) bool {