From 6cccdf2b2c081c9eff3600d00c5e2d6b62d344b8 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Thu, 29 Jan 2026 17:00:44 -0500 Subject: [PATCH 01/19] feat: Allow custom bloom production --- eth/filters/filter.go | 4 ++-- eth/filters/filter_system.go | 2 +- eth/filters/filter_system.libevm.go | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 eth/filters/filter_system.libevm.go diff --git a/eth/filters/filter.go b/eth/filters/filter.go index 3f30c11a4bb1..e17695b67493 100644 --- a/eth/filters/filter.go +++ b/eth/filters/filter.go @@ -290,7 +290,7 @@ func (f *Filter) unindexedLogs(ctx context.Context, end uint64, logChan chan *ty // blockLogs returns the logs matching the filter criteria within a single block. func (f *Filter) blockLogs(ctx context.Context, header *types.Header) ([]*types.Log, error) { - if bloomFilter(header.Bloom, f.addresses, f.topics) { + if bloomFilter(getBloomFromHeader(header, f.sys.backend), f.addresses, f.topics) { return f.checkMatches(ctx, header) } return nil, nil @@ -337,7 +337,7 @@ func (f *Filter) pendingLogs() []*types.Log { if block == nil || receipts == nil { return nil } - if bloomFilter(block.Bloom(), f.addresses, f.topics) { + if bloomFilter(getBloomFromHeader(block.Header(), f.sys.backend), f.addresses, f.topics) { var unfiltered []*types.Log for _, r := range receipts { unfiltered = append(unfiltered, r.Logs...) diff --git a/eth/filters/filter_system.go b/eth/filters/filter_system.go index f59a688a39e0..9fc3cd0c4cc0 100644 --- a/eth/filters/filter_system.go +++ b/eth/filters/filter_system.go @@ -508,7 +508,7 @@ func (es *EventSystem) lightFilterNewHead(newHeader *types.Header, callBack func // filter logs of a single header in light client mode func (es *EventSystem) lightFilterLogs(header *types.Header, addresses []common.Address, topics [][]common.Hash, remove bool) []*types.Log { - if !bloomFilter(header.Bloom, addresses, topics) { + if !bloomFilter(getBloomFromHeader(header, es.backend), addresses, topics) { return nil } // Get the logs of the block diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go new file mode 100644 index 000000000000..2625b6021fb0 --- /dev/null +++ b/eth/filters/filter_system.libevm.go @@ -0,0 +1,14 @@ +package filters + +import "github.com/ava-labs/libevm/core/types" + +func getBloomFromHeader(header *types.Header, backend Backend) types.Bloom { + type bloomHeader interface { + HeaderBloom(*types.Header) types.Bloom + } + + if bh, ok := backend.(bloomHeader); ok { + return bh.HeaderBloom(header) + } + return header.Bloom +} From 34e0e87a4b2e00e4a0e2a517c4f28e47f56569f0 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Thu, 29 Jan 2026 17:23:35 -0500 Subject: [PATCH 02/19] chore: license header --- eth/filters/filter_system.libevm.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go index 2625b6021fb0..b44f8715be79 100644 --- a/eth/filters/filter_system.libevm.go +++ b/eth/filters/filter_system.libevm.go @@ -1,3 +1,19 @@ +// Copyright 2026 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + package filters import "github.com/ava-labs/libevm/core/types" From 5bf0ed381738a25606ccaa0976de2d65b974e023 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Fri, 30 Jan 2026 10:11:29 -0500 Subject: [PATCH 03/19] feat: Add more bloom index interceptors --- core/bloom_indexer.libevm.go | 39 +++++++++++++++++++++++++++++ eth/bloombits.libevm.go | 30 ++++++++++++++++++++++ eth/filters/filter_system.libevm.go | 13 ++++++---- 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 core/bloom_indexer.libevm.go create mode 100644 eth/bloombits.libevm.go diff --git a/core/bloom_indexer.libevm.go b/core/bloom_indexer.libevm.go new file mode 100644 index 000000000000..3190235bae93 --- /dev/null +++ b/core/bloom_indexer.libevm.go @@ -0,0 +1,39 @@ +// Copyright 2026 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// + +package core + +import ( + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/ethdb" +) + +// BloomThrottling is the time to wait between processing two consecutive index sections. +const BloomThrottling = bloomThrottling + +func NewBloomIndexerBackend(db ethdb.Database, size uint64) *BloomIndexer { + return &BloomIndexer{ + db: db, + size: size, + } +} + +// ProcessBloom is the same as Process, but takes the header and bloom separately. +func (b *BloomIndexer) ProcessBloom(header *types.Header, bloom types.Bloom) error { + b.gen.AddBloom(uint(header.Number.Uint64()-b.section*b.size), bloom) + b.head = header.Hash() + return nil +} diff --git a/eth/bloombits.libevm.go b/eth/bloombits.libevm.go new file mode 100644 index 000000000000..ed1b595bd55d --- /dev/null +++ b/eth/bloombits.libevm.go @@ -0,0 +1,30 @@ +// Copyright 2026 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package eth + +import ( + "github.com/ava-labs/libevm/core/bloombits" + "github.com/ava-labs/libevm/ethdb" +) + +func StartBloomHandlers(db ethdb.Database, bloomRequests chan chan *bloombits.Retrieval, closeBloomHandler chan struct{}, sectionSize uint64) { + (&Ethereum{ + bloomRequests: bloomRequests, + closeBloomHandler: closeBloomHandler, + chainDb: db, + }).startBloomHandlers(sectionSize) +} diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go index b44f8715be79..968fc3469df2 100644 --- a/eth/filters/filter_system.libevm.go +++ b/eth/filters/filter_system.libevm.go @@ -18,12 +18,15 @@ package filters import "github.com/ava-labs/libevm/core/types" -func getBloomFromHeader(header *types.Header, backend Backend) types.Bloom { - type bloomHeader interface { - HeaderBloom(*types.Header) types.Bloom - } +// BloomFromHeader represents backends that can retrieve a header's bloom. +// This is optional; if the backend does not implement it, the bloom is taken +// directly from the header. +type BloomFromHeader interface { + HeaderBloom(*types.Header) types.Bloom +} - if bh, ok := backend.(bloomHeader); ok { +func getBloomFromHeader(header *types.Header, backend Backend) types.Bloom { + if bh, ok := backend.(BloomFromHeader); ok { return bh.HeaderBloom(header) } return header.Bloom From 249ad53258c6f48efedce48b464f03b86dfee99a Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Fri, 30 Jan 2026 10:20:12 -0500 Subject: [PATCH 04/19] chore: license header fix --- core/bloom_indexer.libevm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/bloom_indexer.libevm.go b/core/bloom_indexer.libevm.go index 3190235bae93..21923778901d 100644 --- a/core/bloom_indexer.libevm.go +++ b/core/bloom_indexer.libevm.go @@ -12,7 +12,7 @@ // // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see -// +// . package core From 9efd7e6d83330377bee2a9b4b1c66c62e5a11541 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Fri, 30 Jan 2026 10:29:11 -0500 Subject: [PATCH 05/19] chore: docs and lint --- core/bloom_indexer.libevm.go | 4 +++- eth/bloombits.libevm.go | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/bloom_indexer.libevm.go b/core/bloom_indexer.libevm.go index 21923778901d..9a5fd5096784 100644 --- a/core/bloom_indexer.libevm.go +++ b/core/bloom_indexer.libevm.go @@ -24,6 +24,8 @@ import ( // BloomThrottling is the time to wait between processing two consecutive index sections. const BloomThrottling = bloomThrottling +// NewBloomIndexerBackend creates a [BloomIndexer] instance for the given database and section size, +// allowing users to provide custom functionality to the bloom indexer. func NewBloomIndexerBackend(db ethdb.Database, size uint64) *BloomIndexer { return &BloomIndexer{ db: db, @@ -33,7 +35,7 @@ func NewBloomIndexerBackend(db ethdb.Database, size uint64) *BloomIndexer { // ProcessBloom is the same as Process, but takes the header and bloom separately. func (b *BloomIndexer) ProcessBloom(header *types.Header, bloom types.Bloom) error { - b.gen.AddBloom(uint(header.Number.Uint64()-b.section*b.size), bloom) + b.gen.AddBloom(uint(header.Number.Uint64()-b.section*b.size), bloom) //nolint:errcheck,gosec // match original b.head = header.Hash() return nil } diff --git a/eth/bloombits.libevm.go b/eth/bloombits.libevm.go index ed1b595bd55d..67a5fe56a347 100644 --- a/eth/bloombits.libevm.go +++ b/eth/bloombits.libevm.go @@ -21,6 +21,9 @@ import ( "github.com/ava-labs/libevm/ethdb" ) +// StartBloomHandlers starts a batch of goroutines to accept bloom bit database +// retrievals from possibly a range of filters and serving the data to satisfy. +// This is identical to [Ethereum.startBloomHandlers], but exposed for use separately. func StartBloomHandlers(db ethdb.Database, bloomRequests chan chan *bloombits.Retrieval, closeBloomHandler chan struct{}, sectionSize uint64) { (&Ethereum{ bloomRequests: bloomRequests, From c3d9f415dcc7c4e92ea5f5d01a2664fcceb0f6fb Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Fri, 30 Jan 2026 12:53:40 -0500 Subject: [PATCH 06/19] feat: export some constants --- eth/bloombits.libevm.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/eth/bloombits.libevm.go b/eth/bloombits.libevm.go index 67a5fe56a347..d8c375ef3553 100644 --- a/eth/bloombits.libevm.go +++ b/eth/bloombits.libevm.go @@ -21,6 +21,20 @@ import ( "github.com/ava-labs/libevm/ethdb" ) +const ( + // BloomFilterThreads is the number of goroutines used locally per filter to + // multiplex requests onto the global servicing goroutines. + BloomFilterThreads = bloomFilterThreads + + // BloomRetrievalBatch is the maximum number of bloom bit retrievals to service + // in a single batch. + BloomRetrievalBatch = bloomRetrievalBatch + + // BloomRetrievalWait is the maximum time to wait for enough bloom bit requests + // to accumulate request an entire batch (avoiding hysteresis). + BloomRetrievalWait = bloomRetrievalWait +) + // StartBloomHandlers starts a batch of goroutines to accept bloom bit database // retrievals from possibly a range of filters and serving the data to satisfy. // This is identical to [Ethereum.startBloomHandlers], but exposed for use separately. From 2b537d339f7cf62ed2b3a33bb435041709538346 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Mon, 2 Feb 2026 09:44:14 -0500 Subject: [PATCH 07/19] style: rename everything --- core/bloom_indexer.libevm.go | 9 ++++++--- eth/filters/filter.go | 4 ++-- eth/filters/filter_system.go | 4 ++-- eth/filters/filter_system.libevm.go | 16 ++++++++-------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/core/bloom_indexer.libevm.go b/core/bloom_indexer.libevm.go index 9a5fd5096784..6b14337e23d2 100644 --- a/core/bloom_indexer.libevm.go +++ b/core/bloom_indexer.libevm.go @@ -33,9 +33,12 @@ func NewBloomIndexerBackend(db ethdb.Database, size uint64) *BloomIndexer { } } -// ProcessBloom is the same as Process, but takes the header and bloom separately. -func (b *BloomIndexer) ProcessBloom(header *types.Header, bloom types.Bloom) error { - b.gen.AddBloom(uint(header.Number.Uint64()-b.section*b.size), bloom) //nolint:errcheck,gosec // match original +// ProcessWithBloomOverride is the same as Process, but takes the header and bloom separately. +func (b *BloomIndexer) ProcessWithBloomOverride(header *types.Header, bloom types.Bloom) error { + index := uint(header.Number.Uint64() - b.section*b.size) + if err := b.gen.AddBloom(index, bloom); err != nil { + return err + } b.head = header.Hash() return nil } diff --git a/eth/filters/filter.go b/eth/filters/filter.go index e17695b67493..9aa3b53955bc 100644 --- a/eth/filters/filter.go +++ b/eth/filters/filter.go @@ -290,7 +290,7 @@ func (f *Filter) unindexedLogs(ctx context.Context, end uint64, logChan chan *ty // blockLogs returns the logs matching the filter criteria within a single block. func (f *Filter) blockLogs(ctx context.Context, header *types.Header) ([]*types.Log, error) { - if bloomFilter(getBloomFromHeader(header, f.sys.backend), f.addresses, f.topics) { + if bloomFilter(maybeOverrideBloom(header, f.sys.backend), f.addresses, f.topics) { return f.checkMatches(ctx, header) } return nil, nil @@ -337,7 +337,7 @@ func (f *Filter) pendingLogs() []*types.Log { if block == nil || receipts == nil { return nil } - if bloomFilter(getBloomFromHeader(block.Header(), f.sys.backend), f.addresses, f.topics) { + if bloomFilter(maybeOverrideBloom(block.Header(), f.sys.backend), f.addresses, f.topics) { var unfiltered []*types.Log for _, r := range receipts { unfiltered = append(unfiltered, r.Logs...) diff --git a/eth/filters/filter_system.go b/eth/filters/filter_system.go index 9fc3cd0c4cc0..2206b914cdc1 100644 --- a/eth/filters/filter_system.go +++ b/eth/filters/filter_system.go @@ -25,7 +25,7 @@ import ( "sync/atomic" "time" - "github.com/ava-labs/libevm" + ethereum "github.com/ava-labs/libevm" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/lru" "github.com/ava-labs/libevm/core" @@ -508,7 +508,7 @@ func (es *EventSystem) lightFilterNewHead(newHeader *types.Header, callBack func // filter logs of a single header in light client mode func (es *EventSystem) lightFilterLogs(header *types.Header, addresses []common.Address, topics [][]common.Hash, remove bool) []*types.Log { - if !bloomFilter(getBloomFromHeader(header, es.backend), addresses, topics) { + if !bloomFilter(maybeOverrideBloom(header, es.backend), addresses, topics) { return nil } // Get the logs of the block diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go index 968fc3469df2..a13fd2f062d1 100644 --- a/eth/filters/filter_system.libevm.go +++ b/eth/filters/filter_system.libevm.go @@ -18,16 +18,16 @@ package filters import "github.com/ava-labs/libevm/core/types" -// BloomFromHeader represents backends that can retrieve a header's bloom. -// This is optional; if the backend does not implement it, the bloom is taken -// directly from the header. -type BloomFromHeader interface { - HeaderBloom(*types.Header) types.Bloom +// BloomOverrider is an optional extension to [Backend], allowing arbitrary +// bloom filters to be returned for a header. If not implemented, +// [types.Header.Bloom] is used instead. +type BloomOverrider interface { + OverrideHeaderBloom(*types.Header) types.Bloom } -func getBloomFromHeader(header *types.Header, backend Backend) types.Bloom { - if bh, ok := backend.(BloomFromHeader); ok { - return bh.HeaderBloom(header) +func maybeOverrideBloom(header *types.Header, backend Backend) types.Bloom { + if bo, ok := backend.(BloomOverrider); ok { + return bo.OverrideHeaderBloom(header) } return header.Bloom } From dea2e30a07f38bddff345c7a89c6f3e34f506d7c Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Mon, 2 Feb 2026 15:15:21 -0500 Subject: [PATCH 08/19] feat: Add custom bloom indexer service --- eth/filters/filter_system.libevm.go | 102 +++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go index a13fd2f062d1..154220e5d7ff 100644 --- a/eth/filters/filter_system.libevm.go +++ b/eth/filters/filter_system.libevm.go @@ -16,7 +16,107 @@ package filters -import "github.com/ava-labs/libevm/core/types" +import ( + "context" + "math" + + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/bloombits" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/eth" + "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/internal/ethapi" + "github.com/ava-labs/libevm/params" +) + +var _ Backend = ethapi.Backend(nil) + +// BloomIndexerService tracks all necessary components to run a bloom indexer +// service alongside the Ethereum node, independent of the [eth.Ethereum] struct. +// The methods returned can be used to implement the [Backend] interface, but +// this CANNOT be embedded into the backend struct directly, as it would +// expose the [Close] method publicly. This method must be called once +// the service is no longer needed to gracefully terminate all goroutines. +type BloomIndexerService struct { + indexer *core.ChainIndexer + size uint64 + requests chan chan *bloombits.Retrieval + quit chan struct{} +} + +// NewBloomIndexerService creates and starts a bloom indexer service with the given +// backend and section size. If the section size is 0 or too large, it defaults +// to [params.BloomBitsBlocks]. +// The returned service immediately starts indexing the canonical chain and +// servicing bloom filter retrieval requests. +// Once done, the service should be closed with [BloomIndexerService.Close]. +func NewBloomIndexerService(b ethapi.Backend, size uint64) *BloomIndexerService { + if size == 0 || size > math.MaxInt32 { + size = params.BloomBitsBlocks + } + backend := &bloomBackend{ + BloomIndexer: core.NewBloomIndexerBackend(b.ChainDb(), size), + b: b, + db: b.ChainDb(), + } + table := rawdb.NewTable(b.ChainDb(), string(rawdb.BloomBitsIndexPrefix)) + s := &BloomIndexerService{ + indexer: core.NewChainIndexer(b.ChainDb(), table, backend, size, 0, core.BloomThrottling, "bloombits"), + size: size, + requests: make(chan chan *bloombits.Retrieval), + quit: make(chan struct{}), + } + + s.indexer.Start(b) + eth.StartBloomHandlers( + b.ChainDb(), + s.requests, + s.quit, + size, + ) + + return s +} + +// BloomStatus returns the section size and the number of sections indexed so far. +// Can be used as a [Backend] implementation. +func (s *BloomIndexerService) BloomStatus() (uint64, uint64) { + sections, _, _ := s.indexer.Sections() + return s.size, sections +} + +// ServiceFilter starts servicing bloom filter retrieval requests for the given +// matcher session. Can be used as a [Backend] implementation. +func (s *BloomIndexerService) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { + for range eth.BloomFilterThreads { + go session.Multiplex(eth.BloomRetrievalBatch, eth.BloomRetrievalWait, s.requests) + } +} + +// Close terminates the bloom indexer, current bloom filter retrieval requests, +// and the bloom retrieval server. +func (s *BloomIndexerService) Close() error { + close(s.quit) + return s.indexer.Close() +} + +var _ core.ChainIndexerBackend = (*bloomBackend)(nil) + +// bloomBackend is a wrapper around a [core.BloomIndexer] that +// overrides the bloom filter retrieval to allow for custom bloom filter generation. +type bloomBackend struct { + *core.BloomIndexer + b Backend + db ethdb.Database +} + +// Process adds a new header's bloom into the index, possibly overriding +// it using the backend's [BloomOverrider] implementation. +func (b *bloomBackend) Process(ctx context.Context, header *types.Header) error { + bloom := maybeOverrideBloom(header, b.b) + return b.ProcessWithBloomOverride(header, bloom) +} // BloomOverrider is an optional extension to [Backend], allowing arbitrary // bloom filters to be returned for a header. If not implemented, From 1169461ec2d67f4b1194657daed79a349d1a7ee0 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Thu, 5 Feb 2026 13:30:27 -0500 Subject: [PATCH 09/19] test: Ensure bloom override works --- eth/filters/filter_system.go | 2 +- eth/filters/filter_system.libevm.go | 11 +- eth/filters/filter_system.libevm_test.go | 170 +++++++++++++++++++++++ 3 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 eth/filters/filter_system.libevm_test.go diff --git a/eth/filters/filter_system.go b/eth/filters/filter_system.go index 2206b914cdc1..97b0e1d26f27 100644 --- a/eth/filters/filter_system.go +++ b/eth/filters/filter_system.go @@ -25,7 +25,7 @@ import ( "sync/atomic" "time" - ethereum "github.com/ava-labs/libevm" + "github.com/ava-labs/libevm" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/lru" "github.com/ava-labs/libevm/core" diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go index 154220e5d7ff..ee670f52e929 100644 --- a/eth/filters/filter_system.libevm.go +++ b/eth/filters/filter_system.libevm.go @@ -30,13 +30,18 @@ import ( "github.com/ava-labs/libevm/params" ) -var _ Backend = ethapi.Backend(nil) +var _ IndexerServiceProvider = ethapi.Backend(nil) + +type IndexerServiceProvider interface { + Backend + core.ChainIndexerChain +} // BloomIndexerService tracks all necessary components to run a bloom indexer // service alongside the Ethereum node, independent of the [eth.Ethereum] struct. // The methods returned can be used to implement the [Backend] interface, but // this CANNOT be embedded into the backend struct directly, as it would -// expose the [Close] method publicly. This method must be called once +// expose the [BloomIndexerService.Close] method publicly. The Close method must be called once // the service is no longer needed to gracefully terminate all goroutines. type BloomIndexerService struct { indexer *core.ChainIndexer @@ -51,7 +56,7 @@ type BloomIndexerService struct { // The returned service immediately starts indexing the canonical chain and // servicing bloom filter retrieval requests. // Once done, the service should be closed with [BloomIndexerService.Close]. -func NewBloomIndexerService(b ethapi.Backend, size uint64) *BloomIndexerService { +func NewBloomIndexerService(b IndexerServiceProvider, size uint64) *BloomIndexerService { if size == 0 || size > math.MaxInt32 { size = params.BloomBitsBlocks } diff --git a/eth/filters/filter_system.libevm_test.go b/eth/filters/filter_system.libevm_test.go new file mode 100644 index 000000000000..844d0d228776 --- /dev/null +++ b/eth/filters/filter_system.libevm_test.go @@ -0,0 +1,170 @@ +// Copyright 2026 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package filters + +import ( + "context" + "math/big" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/consensus/ethash" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/bloombits" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/event" + "github.com/ava-labs/libevm/params" +) + +var ( + _ BloomOverrider = (*overrideBloomsTestBackend)(nil) + _ IndexerServiceProvider = (*overrideBloomsTestBackend)(nil) +) + +type overrideBloomsTestBackend struct { + *testBackend + + blockFeed event.Feed + s *BloomIndexerService + blooms map[uint64]types.Bloom + overrideCalled atomic.Bool +} + +// SubscribeChainHeadEvent implements IndexerServiceProvider. +// CAN ONLY BE CALLED ONCE! +func (o *overrideBloomsTestBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { + return o.blockFeed.Subscribe(ch) +} + +func (o *overrideBloomsTestBackend) BloomStatus() (uint64, uint64) { + return o.s.BloomStatus() +} + +func (o *overrideBloomsTestBackend) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { + o.s.ServiceFilter(ctx, session) +} + +// OverrideHeaderBloom implements BloomOverrider. +func (o *overrideBloomsTestBackend) OverrideHeaderBloom(hdr *types.Header) types.Bloom { + o.overrideCalled.Store(true) + bloom, ok := o.blooms[hdr.Number.Uint64()] + if !ok { + return hdr.Bloom + } + return bloom +} + +func TestOverrideBlooms(t *testing.T) { + defer goleak.VerifyNone(t, goleak.IgnoreCurrent(), goleak.IgnoreTopFunction("github.com/ava-labs/libevm/eth/filters.(*EventSystem).eventLoop")) + + const ( + sectionSize = 8 + numBlocks = 10 + ) + + var ( + db = rawdb.NewMemoryDatabase() + backend = &overrideBloomsTestBackend{ + testBackend: &testBackend{db: db}, + } + sys = NewFilterSystem(backend, Config{}) + api = NewFilterAPI(sys, true) + ) + defer CloseAPI(api) + + var ( + signer = types.HomesteadSigner{} + key, _ = crypto.GenerateKey() + addr = crypto.PubkeyToAddress(key.PublicKey) + genesis = &core.Genesis{Config: params.TestChainConfig, + Alloc: types.GenesisAlloc{ + addr: {Balance: big.NewInt(params.Ether)}, + }, + } + firstAddr = common.HexToAddress("0x1111111111111111111111111111111111111111") + + receipts []*types.Receipt + blooms map[uint64]types.Bloom = make(map[uint64]types.Bloom) + ) + + // [core.GenerateChainWithGenesis] doesn't let us set blooms directly, + // so we create receipts with logs that produce the desired blooms. + // This also tests BloomOverrider in the process. + for i := range numBlocks { + blockNum := uint64(i + 1) //nolint:gosec // guaranteed by loop + log := &types.Log{Address: firstAddr, Topics: []common.Hash{}, Data: []byte{}, BlockNumber: blockNum, Index: 0} + receipt := &types.Receipt{ + Logs: []*types.Log{log}, + } + bloom := types.CreateBloom(types.Receipts{receipt}) + blooms[blockNum] = bloom + receipt.Bloom = bloom + receipts = append(receipts, receipt) + } + backend.blooms = blooms + + // Doesn't set the bloom + _, blocks, _ := core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), numBlocks, func(i int, b *core.BlockGen) { + b.AddUncheckedReceipt(receipts[i]) + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{ + Nonce: uint64(i), //nolint:gosec // verified above + To: &common.Address{}, + Value: big.NewInt(1000), + Gas: params.TxGas, + GasPrice: b.BaseFee(), + Data: nil, + }), signer, key) + b.AddTx(tx) + }) + + writeBlock := func(block *types.Block) { + rawdb.WriteBlock(db, block) + rawdb.WriteCanonicalHash(db, block.Hash(), block.NumberU64()) + rawdb.WriteHeadBlockHash(db, block.Hash()) + } + + // Write genesis block to start bloom indexer from there + writeBlock(genesis.ToBlock()) + backend.s = NewBloomIndexerService(backend, sectionSize) + defer func() { + require.NoError(t, backend.s.Close()) + }() + time.Sleep(100 * time.Millisecond) // give the indexer some time to start + + for i, block := range blocks { + writeBlock(block) + rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), []*types.Receipt{receipts[i]}) + backend.blockFeed.Send(core.ChainHeadEvent{Block: block}) + } + + require.Eventually(t, func() bool { + _, indexedSections := backend.BloomStatus() + return indexedSections > 0 + }, 10*time.Second, 100*time.Millisecond, "bloom indexer did not index all blocks in time") + + filter := FilterCriteria{Addresses: []common.Address{firstAddr}, FromBlock: big.NewInt(1), ToBlock: big.NewInt(int64(numBlocks + 1))} + results, err := api.GetLogs(t.Context(), filter) + require.NoErrorf(t, err, "%T.GetLogs", api) + require.Len(t, results, numBlocks, "expected to find logs for all blocks") +} From 8b90483ae5dc229c0e5d150b1968ee72297d27ea Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Thu, 5 Feb 2026 14:55:18 -0500 Subject: [PATCH 10/19] chore: comments --- eth/filters/filter_system.libevm.go | 5 +---- eth/filters/filter_system.libevm_test.go | 16 ++++++---------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go index ee670f52e929..ee1887d0f0e3 100644 --- a/eth/filters/filter_system.libevm.go +++ b/eth/filters/filter_system.libevm.go @@ -25,7 +25,6 @@ import ( "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/eth" - "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/internal/ethapi" "github.com/ava-labs/libevm/params" ) @@ -63,7 +62,6 @@ func NewBloomIndexerService(b IndexerServiceProvider, size uint64) *BloomIndexer backend := &bloomBackend{ BloomIndexer: core.NewBloomIndexerBackend(b.ChainDb(), size), b: b, - db: b.ChainDb(), } table := rawdb.NewTable(b.ChainDb(), string(rawdb.BloomBitsIndexPrefix)) s := &BloomIndexerService{ @@ -112,8 +110,7 @@ var _ core.ChainIndexerBackend = (*bloomBackend)(nil) // overrides the bloom filter retrieval to allow for custom bloom filter generation. type bloomBackend struct { *core.BloomIndexer - b Backend - db ethdb.Database + b Backend } // Process adds a new header's bloom into the index, possibly overriding diff --git a/eth/filters/filter_system.libevm_test.go b/eth/filters/filter_system.libevm_test.go index 844d0d228776..43b683caa7fb 100644 --- a/eth/filters/filter_system.libevm_test.go +++ b/eth/filters/filter_system.libevm_test.go @@ -19,7 +19,6 @@ package filters import ( "context" "math/big" - "sync/atomic" "testing" "time" @@ -45,14 +44,12 @@ var ( type overrideBloomsTestBackend struct { *testBackend - blockFeed event.Feed - s *BloomIndexerService - blooms map[uint64]types.Bloom - overrideCalled atomic.Bool + blockFeed event.Feed + s *BloomIndexerService + blooms map[uint64]types.Bloom } -// SubscribeChainHeadEvent implements IndexerServiceProvider. -// CAN ONLY BE CALLED ONCE! +// SubscribeChainHeadEvent forwards accepted blocks to the [core.ChainIndexer]. func (o *overrideBloomsTestBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { return o.blockFeed.Subscribe(ch) } @@ -65,9 +62,9 @@ func (o *overrideBloomsTestBackend) ServiceFilter(ctx context.Context, session * o.s.ServiceFilter(ctx, session) } -// OverrideHeaderBloom implements BloomOverrider. +// OverrideHeaderBloom replaces the bloom of the given header if we know a custom one for its block number. +// This is because [core.GenerateChainWithGenesis] doesn't let us set blooms directly. func (o *overrideBloomsTestBackend) OverrideHeaderBloom(hdr *types.Header) types.Bloom { - o.overrideCalled.Store(true) bloom, ok := o.blooms[hdr.Number.Uint64()] if !ok { return hdr.Bloom @@ -150,7 +147,6 @@ func TestOverrideBlooms(t *testing.T) { defer func() { require.NoError(t, backend.s.Close()) }() - time.Sleep(100 * time.Millisecond) // give the indexer some time to start for i, block := range blocks { writeBlock(block) From b2baaf9f6fa5f08dce2983d7feb8a987c196163a Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 9 Feb 2026 12:45:56 +0000 Subject: [PATCH 11/19] refactor: `NewBloomIndexerService()` --- core/bloom_indexer.libevm.go | 2 +- eth/filters/filter_system.libevm.go | 71 ++++++++++++------------ eth/filters/filter_system.libevm_test.go | 38 ++++++------- 3 files changed, 57 insertions(+), 54 deletions(-) diff --git a/core/bloom_indexer.libevm.go b/core/bloom_indexer.libevm.go index 6b14337e23d2..92664f8e4cbb 100644 --- a/core/bloom_indexer.libevm.go +++ b/core/bloom_indexer.libevm.go @@ -33,7 +33,7 @@ func NewBloomIndexerBackend(db ethdb.Database, size uint64) *BloomIndexer { } } -// ProcessWithBloomOverride is the same as Process, but takes the header and bloom separately. +// ProcessWithBloomOverride is the same as [BloomIndexer.Process], but takes the header and bloom separately. func (b *BloomIndexer) ProcessWithBloomOverride(header *types.Header, bloom types.Bloom) error { index := uint(header.Number.Uint64() - b.section*b.size) if err := b.gen.AddBloom(index, bloom); err != nil { diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go index ee1887d0f0e3..18f1803d3ef4 100644 --- a/eth/filters/filter_system.libevm.go +++ b/eth/filters/filter_system.libevm.go @@ -25,17 +25,10 @@ import ( "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/eth" - "github.com/ava-labs/libevm/internal/ethapi" + "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/params" ) -var _ IndexerServiceProvider = ethapi.Backend(nil) - -type IndexerServiceProvider interface { - Backend - core.ChainIndexerChain -} - // BloomIndexerService tracks all necessary components to run a bloom indexer // service alongside the Ethereum node, independent of the [eth.Ethereum] struct. // The methods returned can be used to implement the [Backend] interface, but @@ -54,26 +47,28 @@ type BloomIndexerService struct { // to [params.BloomBitsBlocks]. // The returned service immediately starts indexing the canonical chain and // servicing bloom filter retrieval requests. -// Once done, the service should be closed with [BloomIndexerService.Close]. -func NewBloomIndexerService(b IndexerServiceProvider, size uint64) *BloomIndexerService { +// Once done, the service should be closed with [CloseBloomIndexerService]. +// The [BloomOverrider] MAY be nil, in which case the [types.Header] bloom is +// always used. +func NewBloomIndexerService(db ethdb.Database, chain core.ChainIndexerChain, override BloomOverrider, size uint64) *BloomIndexerService { if size == 0 || size > math.MaxInt32 { size = params.BloomBitsBlocks } backend := &bloomBackend{ - BloomIndexer: core.NewBloomIndexerBackend(b.ChainDb(), size), - b: b, + BloomIndexer: core.NewBloomIndexerBackend(db, size), + override: override, } - table := rawdb.NewTable(b.ChainDb(), string(rawdb.BloomBitsIndexPrefix)) + table := rawdb.NewTable(db, string(rawdb.BloomBitsIndexPrefix)) s := &BloomIndexerService{ - indexer: core.NewChainIndexer(b.ChainDb(), table, backend, size, 0, core.BloomThrottling, "bloombits"), + indexer: core.NewChainIndexer(db, table, backend, size, 0, core.BloomThrottling, "bloombits"), size: size, requests: make(chan chan *bloombits.Retrieval), quit: make(chan struct{}), } - s.indexer.Start(b) + s.indexer.Start(chain) eth.StartBloomHandlers( - b.ChainDb(), + db, s.requests, s.quit, size, @@ -98,28 +93,14 @@ func (s *BloomIndexerService) ServiceFilter(ctx context.Context, session *bloomb } // Close terminates the bloom indexer, current bloom filter retrieval requests, -// and the bloom retrieval server. -func (s *BloomIndexerService) Close() error { +// and the bloom retrieval server. It is defined as a function instead of a +// method to allow embedding of a [BloomIndexerService] without exposing it as +// an RPC method. +func CloseBloomIndexerService(s *BloomIndexerService) error { close(s.quit) return s.indexer.Close() } -var _ core.ChainIndexerBackend = (*bloomBackend)(nil) - -// bloomBackend is a wrapper around a [core.BloomIndexer] that -// overrides the bloom filter retrieval to allow for custom bloom filter generation. -type bloomBackend struct { - *core.BloomIndexer - b Backend -} - -// Process adds a new header's bloom into the index, possibly overriding -// it using the backend's [BloomOverrider] implementation. -func (b *bloomBackend) Process(ctx context.Context, header *types.Header) error { - bloom := maybeOverrideBloom(header, b.b) - return b.ProcessWithBloomOverride(header, bloom) -} - // BloomOverrider is an optional extension to [Backend], allowing arbitrary // bloom filters to be returned for a header. If not implemented, // [types.Header.Bloom] is used instead. @@ -133,3 +114,25 @@ func maybeOverrideBloom(header *types.Header, backend Backend) types.Bloom { } return header.Bloom } + +var _ core.ChainIndexerBackend = (*bloomBackend)(nil) + +// bloomBackend is a wrapper around a [core.BloomIndexer] that +// overrides Process() to allow for custom bloom filter generation. +type bloomBackend struct { + *core.BloomIndexer + override BloomOverrider +} + +func (b *bloomBackend) bloom(h *types.Header) types.Bloom { + if b.override == nil { + return h.Bloom + } + return b.override.OverrideHeaderBloom(h) +} + +// Process adds a new header's bloom into the index, possibly overriding +// it using the backend's [BloomOverrider] implementation. +func (b *bloomBackend) Process(ctx context.Context, header *types.Header) error { + return b.ProcessWithBloomOverride(header, b.bloom(header)) +} diff --git a/eth/filters/filter_system.libevm_test.go b/eth/filters/filter_system.libevm_test.go index 43b683caa7fb..0778609a7e08 100644 --- a/eth/filters/filter_system.libevm_test.go +++ b/eth/filters/filter_system.libevm_test.go @@ -36,17 +36,10 @@ import ( "github.com/ava-labs/libevm/params" ) -var ( - _ BloomOverrider = (*overrideBloomsTestBackend)(nil) - _ IndexerServiceProvider = (*overrideBloomsTestBackend)(nil) -) - type overrideBloomsTestBackend struct { *testBackend - - blockFeed event.Feed + blockFeed event.FeedOf[core.ChainHeadEvent] s *BloomIndexerService - blooms map[uint64]types.Bloom } // SubscribeChainHeadEvent forwards accepted blocks to the [core.ChainIndexer]. @@ -62,9 +55,13 @@ func (o *overrideBloomsTestBackend) ServiceFilter(ctx context.Context, session * o.s.ServiceFilter(ctx, session) } +type bloomOverrider struct { + blooms map[uint64]types.Bloom +} + // OverrideHeaderBloom replaces the bloom of the given header if we know a custom one for its block number. // This is because [core.GenerateChainWithGenesis] doesn't let us set blooms directly. -func (o *overrideBloomsTestBackend) OverrideHeaderBloom(hdr *types.Header) types.Bloom { +func (o *bloomOverrider) OverrideHeaderBloom(hdr *types.Header) types.Bloom { bloom, ok := o.blooms[hdr.Number.Uint64()] if !ok { return hdr.Bloom @@ -81,14 +78,8 @@ func TestOverrideBlooms(t *testing.T) { ) var ( - db = rawdb.NewMemoryDatabase() - backend = &overrideBloomsTestBackend{ - testBackend: &testBackend{db: db}, - } - sys = NewFilterSystem(backend, Config{}) - api = NewFilterAPI(sys, true) + db = rawdb.NewMemoryDatabase() ) - defer CloseAPI(api) var ( signer = types.HomesteadSigner{} @@ -119,7 +110,6 @@ func TestOverrideBlooms(t *testing.T) { receipt.Bloom = bloom receipts = append(receipts, receipt) } - backend.blooms = blooms // Doesn't set the bloom _, blocks, _ := core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), numBlocks, func(i int, b *core.BlockGen) { @@ -143,9 +133,19 @@ func TestOverrideBlooms(t *testing.T) { // Write genesis block to start bloom indexer from there writeBlock(genesis.ToBlock()) - backend.s = NewBloomIndexerService(backend, sectionSize) + + var ( + backend = &overrideBloomsTestBackend{ + testBackend: &testBackend{db: db}, + } + sys = NewFilterSystem(backend, Config{}) + api = NewFilterAPI(sys, true) + ) + // TODO(arr4n): DO NOT MERGE: this circular dependency needs to be addressed. + backend.s = NewBloomIndexerService(db, backend, &bloomOverrider{blooms}, sectionSize) defer func() { - require.NoError(t, backend.s.Close()) + CloseAPI(api) + require.NoError(t, CloseBloomIndexerService(backend.s)) }() for i, block := range blocks { From 48b01d5a22b4856e8f91fbd4a1c91c843c8c5bfb Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 10 Feb 2026 14:03:44 +0000 Subject: [PATCH 12/19] feat: `eth.BloomHandlers` with cleanup --- eth/bloombits.go | 5 +- eth/bloombits.libevm.go | 66 +++++++-- eth/bloombits.libevm_test.go | 29 ++++ eth/filters/filter_system.libevm_test.go | 166 ----------------------- 4 files changed, 87 insertions(+), 179 deletions(-) create mode 100644 eth/bloombits.libevm_test.go delete mode 100644 eth/filters/filter_system.libevm_test.go diff --git a/eth/bloombits.go b/eth/bloombits.go index a87ea8f96ce3..cc680daae8fc 100644 --- a/eth/bloombits.go +++ b/eth/bloombits.go @@ -43,9 +43,12 @@ const ( // startBloomHandlers starts a batch of goroutines to accept bloom bit database // retrievals from possibly a range of filters and serving the data to satisfy. -func (eth *Ethereum) startBloomHandlers(sectionSize uint64) { +func (eth *Ethereum) startBloomHandlers(sectionSize uint64, opts ...bloomHandlersOption) { + wg := bloomHandlersWG(opts...) for i := 0; i < bloomServiceThreads; i++ { + wg.Add(1) go func() { + defer wg.Done() for { select { case <-eth.closeBloomHandler: diff --git a/eth/bloombits.libevm.go b/eth/bloombits.libevm.go index d8c375ef3553..00c83889daa4 100644 --- a/eth/bloombits.libevm.go +++ b/eth/bloombits.libevm.go @@ -17,8 +17,11 @@ package eth import ( + "sync" + "github.com/ava-labs/libevm/core/bloombits" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/options" ) const ( @@ -26,22 +29,61 @@ const ( // multiplex requests onto the global servicing goroutines. BloomFilterThreads = bloomFilterThreads - // BloomRetrievalBatch is the maximum number of bloom bit retrievals to service - // in a single batch. + // BloomRetrievalBatch is the maximum number of bloom bit retrievals to + // service in a single batch. BloomRetrievalBatch = bloomRetrievalBatch - // BloomRetrievalWait is the maximum time to wait for enough bloom bit requests - // to accumulate request an entire batch (avoiding hysteresis). + // BloomRetrievalWait is the maximum time to wait for enough bloom bit + // requests to accumulate request an entire batch (avoiding hysteresis). BloomRetrievalWait = bloomRetrievalWait ) -// StartBloomHandlers starts a batch of goroutines to accept bloom bit database -// retrievals from possibly a range of filters and serving the data to satisfy. -// This is identical to [Ethereum.startBloomHandlers], but exposed for use separately. -func StartBloomHandlers(db ethdb.Database, bloomRequests chan chan *bloombits.Retrieval, closeBloomHandler chan struct{}, sectionSize uint64) { - (&Ethereum{ - bloomRequests: bloomRequests, - closeBloomHandler: closeBloomHandler, +// A bloomHandlersOption configures [Ethereum.startBloomHandlers]. +type bloomHandlersOption = options.Option[bloomHandlersConfig] + +type bloomHandlersConfig struct { + wg *sync.WaitGroup +} + +// bloomHandlersWG returns the last [sync.WaitGroup] set by an option, or a +// default value that exists only to avoid panics when used but it otherwise +// inaccessible. +func bloomHandlersWG(opts ...bloomHandlersOption) *sync.WaitGroup { + blackhole := &sync.WaitGroup{} + return options.ApplyTo(&bloomHandlersConfig{blackhole}, opts...).wg +} + +// StartBloomHandlers starts a batch of goroutines to serve data for +// [bloombits.Retrieval] requests from any number of filters. This is identical +// to [Ethereum.startBloomHandlers], but exposed for independent use. +func StartBloomHandlers(db ethdb.Database, sectionSize uint64) *BloomHandlers { + bh := &BloomHandlers{ + Requests: make(chan chan *bloombits.Retrieval), + quit: make(chan struct{}), + } + eth := &Ethereum{ + bloomRequests: bh.Requests, + closeBloomHandler: bh.quit, chainDb: db, - }).startBloomHandlers(sectionSize) + } + eth.startBloomHandlers(sectionSize, options.Func[bloomHandlersConfig](func(c *bloomHandlersConfig) { + c.wg = &bh.wg + })) + return bh +} + +// BloomHandlers serve data for [bloombits.Retrieval] requests from any number +// of filters. [BloomHandlers.Close] MUST be called to release goroutines, after +// which a send on the requests channel will block indefinitely. +type BloomHandlers struct { + Requests chan chan *bloombits.Retrieval + quit chan struct{} + wg sync.WaitGroup +} + +// Close releases resources in use by the [BloomHandlers]; repeated calls will +// panic. +func (bh *BloomHandlers) Close() { + close(bh.quit) + bh.wg.Wait() } diff --git a/eth/bloombits.libevm_test.go b/eth/bloombits.libevm_test.go new file mode 100644 index 000000000000..b1a17a6400a3 --- /dev/null +++ b/eth/bloombits.libevm_test.go @@ -0,0 +1,29 @@ +// Copyright 2026 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package eth + +import ( + "testing" + + "github.com/ava-labs/libevm/core/rawdb" + "go.uber.org/goleak" +) + +func TestStartBloomHandlersNoLeaks(t *testing.T) { + defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) + StartBloomHandlers(rawdb.NewMemoryDatabase(), 42).Close() +} diff --git a/eth/filters/filter_system.libevm_test.go b/eth/filters/filter_system.libevm_test.go deleted file mode 100644 index 0778609a7e08..000000000000 --- a/eth/filters/filter_system.libevm_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2026 the libevm authors. -// -// The libevm additions to go-ethereum are free software: you can redistribute -// them and/or modify them under the terms of the GNU Lesser General Public License -// as published by the Free Software Foundation, either version 3 of the License, -// or (at your option) any later version. -// -// The libevm additions are distributed in the hope that they will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser -// General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see -// . - -package filters - -import ( - "context" - "math/big" - "testing" - "time" - - "github.com/stretchr/testify/require" - "go.uber.org/goleak" - - "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/consensus/ethash" - "github.com/ava-labs/libevm/core" - "github.com/ava-labs/libevm/core/bloombits" - "github.com/ava-labs/libevm/core/rawdb" - "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/crypto" - "github.com/ava-labs/libevm/event" - "github.com/ava-labs/libevm/params" -) - -type overrideBloomsTestBackend struct { - *testBackend - blockFeed event.FeedOf[core.ChainHeadEvent] - s *BloomIndexerService -} - -// SubscribeChainHeadEvent forwards accepted blocks to the [core.ChainIndexer]. -func (o *overrideBloomsTestBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { - return o.blockFeed.Subscribe(ch) -} - -func (o *overrideBloomsTestBackend) BloomStatus() (uint64, uint64) { - return o.s.BloomStatus() -} - -func (o *overrideBloomsTestBackend) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { - o.s.ServiceFilter(ctx, session) -} - -type bloomOverrider struct { - blooms map[uint64]types.Bloom -} - -// OverrideHeaderBloom replaces the bloom of the given header if we know a custom one for its block number. -// This is because [core.GenerateChainWithGenesis] doesn't let us set blooms directly. -func (o *bloomOverrider) OverrideHeaderBloom(hdr *types.Header) types.Bloom { - bloom, ok := o.blooms[hdr.Number.Uint64()] - if !ok { - return hdr.Bloom - } - return bloom -} - -func TestOverrideBlooms(t *testing.T) { - defer goleak.VerifyNone(t, goleak.IgnoreCurrent(), goleak.IgnoreTopFunction("github.com/ava-labs/libevm/eth/filters.(*EventSystem).eventLoop")) - - const ( - sectionSize = 8 - numBlocks = 10 - ) - - var ( - db = rawdb.NewMemoryDatabase() - ) - - var ( - signer = types.HomesteadSigner{} - key, _ = crypto.GenerateKey() - addr = crypto.PubkeyToAddress(key.PublicKey) - genesis = &core.Genesis{Config: params.TestChainConfig, - Alloc: types.GenesisAlloc{ - addr: {Balance: big.NewInt(params.Ether)}, - }, - } - firstAddr = common.HexToAddress("0x1111111111111111111111111111111111111111") - - receipts []*types.Receipt - blooms map[uint64]types.Bloom = make(map[uint64]types.Bloom) - ) - - // [core.GenerateChainWithGenesis] doesn't let us set blooms directly, - // so we create receipts with logs that produce the desired blooms. - // This also tests BloomOverrider in the process. - for i := range numBlocks { - blockNum := uint64(i + 1) //nolint:gosec // guaranteed by loop - log := &types.Log{Address: firstAddr, Topics: []common.Hash{}, Data: []byte{}, BlockNumber: blockNum, Index: 0} - receipt := &types.Receipt{ - Logs: []*types.Log{log}, - } - bloom := types.CreateBloom(types.Receipts{receipt}) - blooms[blockNum] = bloom - receipt.Bloom = bloom - receipts = append(receipts, receipt) - } - - // Doesn't set the bloom - _, blocks, _ := core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), numBlocks, func(i int, b *core.BlockGen) { - b.AddUncheckedReceipt(receipts[i]) - tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{ - Nonce: uint64(i), //nolint:gosec // verified above - To: &common.Address{}, - Value: big.NewInt(1000), - Gas: params.TxGas, - GasPrice: b.BaseFee(), - Data: nil, - }), signer, key) - b.AddTx(tx) - }) - - writeBlock := func(block *types.Block) { - rawdb.WriteBlock(db, block) - rawdb.WriteCanonicalHash(db, block.Hash(), block.NumberU64()) - rawdb.WriteHeadBlockHash(db, block.Hash()) - } - - // Write genesis block to start bloom indexer from there - writeBlock(genesis.ToBlock()) - - var ( - backend = &overrideBloomsTestBackend{ - testBackend: &testBackend{db: db}, - } - sys = NewFilterSystem(backend, Config{}) - api = NewFilterAPI(sys, true) - ) - // TODO(arr4n): DO NOT MERGE: this circular dependency needs to be addressed. - backend.s = NewBloomIndexerService(db, backend, &bloomOverrider{blooms}, sectionSize) - defer func() { - CloseAPI(api) - require.NoError(t, CloseBloomIndexerService(backend.s)) - }() - - for i, block := range blocks { - writeBlock(block) - rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), []*types.Receipt{receipts[i]}) - backend.blockFeed.Send(core.ChainHeadEvent{Block: block}) - } - - require.Eventually(t, func() bool { - _, indexedSections := backend.BloomStatus() - return indexedSections > 0 - }, 10*time.Second, 100*time.Millisecond, "bloom indexer did not index all blocks in time") - - filter := FilterCriteria{Addresses: []common.Address{firstAddr}, FromBlock: big.NewInt(1), ToBlock: big.NewInt(int64(numBlocks + 1))} - results, err := api.GetLogs(t.Context(), filter) - require.NoErrorf(t, err, "%T.GetLogs", api) - require.Len(t, results, numBlocks, "expected to find logs for all blocks") -} From 2e227581e2a6dd198ea6de5357137ed2716d0db1 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 10 Feb 2026 14:07:48 +0000 Subject: [PATCH 13/19] refactor!: remove `filters.BloomIndexerService` to place in SAE instead for unified testing --- eth/filters/filter_system.libevm.go | 103 ---------------------------- 1 file changed, 103 deletions(-) diff --git a/eth/filters/filter_system.libevm.go b/eth/filters/filter_system.libevm.go index 18f1803d3ef4..5762aa0065e6 100644 --- a/eth/filters/filter_system.libevm.go +++ b/eth/filters/filter_system.libevm.go @@ -17,90 +17,9 @@ package filters import ( - "context" - "math" - - "github.com/ava-labs/libevm/core" - "github.com/ava-labs/libevm/core/bloombits" - "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/eth" - "github.com/ava-labs/libevm/ethdb" - "github.com/ava-labs/libevm/params" ) -// BloomIndexerService tracks all necessary components to run a bloom indexer -// service alongside the Ethereum node, independent of the [eth.Ethereum] struct. -// The methods returned can be used to implement the [Backend] interface, but -// this CANNOT be embedded into the backend struct directly, as it would -// expose the [BloomIndexerService.Close] method publicly. The Close method must be called once -// the service is no longer needed to gracefully terminate all goroutines. -type BloomIndexerService struct { - indexer *core.ChainIndexer - size uint64 - requests chan chan *bloombits.Retrieval - quit chan struct{} -} - -// NewBloomIndexerService creates and starts a bloom indexer service with the given -// backend and section size. If the section size is 0 or too large, it defaults -// to [params.BloomBitsBlocks]. -// The returned service immediately starts indexing the canonical chain and -// servicing bloom filter retrieval requests. -// Once done, the service should be closed with [CloseBloomIndexerService]. -// The [BloomOverrider] MAY be nil, in which case the [types.Header] bloom is -// always used. -func NewBloomIndexerService(db ethdb.Database, chain core.ChainIndexerChain, override BloomOverrider, size uint64) *BloomIndexerService { - if size == 0 || size > math.MaxInt32 { - size = params.BloomBitsBlocks - } - backend := &bloomBackend{ - BloomIndexer: core.NewBloomIndexerBackend(db, size), - override: override, - } - table := rawdb.NewTable(db, string(rawdb.BloomBitsIndexPrefix)) - s := &BloomIndexerService{ - indexer: core.NewChainIndexer(db, table, backend, size, 0, core.BloomThrottling, "bloombits"), - size: size, - requests: make(chan chan *bloombits.Retrieval), - quit: make(chan struct{}), - } - - s.indexer.Start(chain) - eth.StartBloomHandlers( - db, - s.requests, - s.quit, - size, - ) - - return s -} - -// BloomStatus returns the section size and the number of sections indexed so far. -// Can be used as a [Backend] implementation. -func (s *BloomIndexerService) BloomStatus() (uint64, uint64) { - sections, _, _ := s.indexer.Sections() - return s.size, sections -} - -// ServiceFilter starts servicing bloom filter retrieval requests for the given -// matcher session. Can be used as a [Backend] implementation. -func (s *BloomIndexerService) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { - for range eth.BloomFilterThreads { - go session.Multiplex(eth.BloomRetrievalBatch, eth.BloomRetrievalWait, s.requests) - } -} - -// Close terminates the bloom indexer, current bloom filter retrieval requests, -// and the bloom retrieval server. It is defined as a function instead of a -// method to allow embedding of a [BloomIndexerService] without exposing it as -// an RPC method. -func CloseBloomIndexerService(s *BloomIndexerService) error { - close(s.quit) - return s.indexer.Close() -} - // BloomOverrider is an optional extension to [Backend], allowing arbitrary // bloom filters to be returned for a header. If not implemented, // [types.Header.Bloom] is used instead. @@ -114,25 +33,3 @@ func maybeOverrideBloom(header *types.Header, backend Backend) types.Bloom { } return header.Bloom } - -var _ core.ChainIndexerBackend = (*bloomBackend)(nil) - -// bloomBackend is a wrapper around a [core.BloomIndexer] that -// overrides Process() to allow for custom bloom filter generation. -type bloomBackend struct { - *core.BloomIndexer - override BloomOverrider -} - -func (b *bloomBackend) bloom(h *types.Header) types.Bloom { - if b.override == nil { - return h.Bloom - } - return b.override.OverrideHeaderBloom(h) -} - -// Process adds a new header's bloom into the index, possibly overriding -// it using the backend's [BloomOverrider] implementation. -func (b *bloomBackend) Process(ctx context.Context, header *types.Header) error { - return b.ProcessWithBloomOverride(header, b.bloom(header)) -} From 187c1288585428f96c97eede30c3d1bbbbced26c Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Tue, 10 Feb 2026 11:49:49 -0500 Subject: [PATCH 14/19] test: Ensure bloom overrider is called --- core/bloom_indexer.libevm.go | 2 ++ eth/bloombits.libevm_test.go | 3 ++- eth/filters/filter_system.libevm_test.go | 26 ++++++++++++++++++++++++ eth/filters/filter_system_test.go | 24 ++++++++++++++-------- eth/filters/filter_test.go | 9 ++++++-- 5 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 eth/filters/filter_system.libevm_test.go diff --git a/core/bloom_indexer.libevm.go b/core/bloom_indexer.libevm.go index 92664f8e4cbb..70bb86cd95c9 100644 --- a/core/bloom_indexer.libevm.go +++ b/core/bloom_indexer.libevm.go @@ -34,6 +34,8 @@ func NewBloomIndexerBackend(db ethdb.Database, size uint64) *BloomIndexer { } // ProcessWithBloomOverride is the same as [BloomIndexer.Process], but takes the header and bloom separately. +// This must obey the same invariates as [BloomIndexer.Process], including calling [BloomIndexer.Reset] +// to start a new section prior to this call, otherwise this function will panic. func (b *BloomIndexer) ProcessWithBloomOverride(header *types.Header, bloom types.Bloom) error { index := uint(header.Number.Uint64() - b.section*b.size) if err := b.gen.AddBloom(index, bloom); err != nil { diff --git a/eth/bloombits.libevm_test.go b/eth/bloombits.libevm_test.go index b1a17a6400a3..0e4dcba5a8d3 100644 --- a/eth/bloombits.libevm_test.go +++ b/eth/bloombits.libevm_test.go @@ -19,8 +19,9 @@ package eth import ( "testing" - "github.com/ava-labs/libevm/core/rawdb" "go.uber.org/goleak" + + "github.com/ava-labs/libevm/core/rawdb" ) func TestStartBloomHandlersNoLeaks(t *testing.T) { diff --git a/eth/filters/filter_system.libevm_test.go b/eth/filters/filter_system.libevm_test.go new file mode 100644 index 000000000000..5a5cbf91f4c0 --- /dev/null +++ b/eth/filters/filter_system.libevm_test.go @@ -0,0 +1,26 @@ +// Copyright 2026 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package filters + +import "github.com/ava-labs/libevm/core/types" + +var _ BloomOverrider = (*testBackend)(nil) + +func (b *testBackend) OverrideHeaderBloom(header *types.Header) types.Bloom { + b.overrideBloomCalled.Store(true) + return header.Bloom +} diff --git a/eth/filters/filter_system_test.go b/eth/filters/filter_system_test.go index 822300d696d7..184bd17763cb 100644 --- a/eth/filters/filter_system_test.go +++ b/eth/filters/filter_system_test.go @@ -24,6 +24,7 @@ import ( "math/rand" "reflect" "runtime" + "sync/atomic" "testing" "time" @@ -43,15 +44,16 @@ import ( ) type testBackend struct { - db ethdb.Database - sections uint64 - txFeed event.Feed - logsFeed event.Feed - rmLogsFeed event.Feed - pendingLogsFeed event.Feed - chainFeed event.Feed - pendingBlock *types.Block - pendingReceipts types.Receipts + db ethdb.Database + sections uint64 + txFeed event.Feed + logsFeed event.Feed + rmLogsFeed event.Feed + pendingLogsFeed event.Feed + chainFeed event.Feed + pendingBlock *types.Block + pendingReceipts types.Receipts + overrideBloomCalled atomic.Bool } func (b *testBackend) ChainConfig() *params.ChainConfig { @@ -903,6 +905,10 @@ func TestLightFilterLogs(t *testing.T) { } } } + + if !backend.overrideBloomCalled.Load() { + t.Error("expected OverrideHeaderBloom to be called") + } } // TestPendingTxFilterDeadlock tests if the event loop hangs when pending diff --git a/eth/filters/filter_test.go b/eth/filters/filter_test.go index c67d96fbba37..0809faed858b 100644 --- a/eth/filters/filter_test.go +++ b/eth/filters/filter_test.go @@ -109,8 +109,8 @@ func BenchmarkFilters(b *testing.B) { func TestFilters(t *testing.T) { var ( - db = rawdb.NewMemoryDatabase() - _, sys = newTestFilterSystem(t, db, Config{}) + db = rawdb.NewMemoryDatabase() + backend, sys = newTestFilterSystem(t, db, Config{}) // Sender account key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") addr = crypto.PubkeyToAddress(key1.PublicKey) @@ -373,6 +373,11 @@ func TestFilters(t *testing.T) { if string(have) != tc.want { t.Fatalf("test %d, have:\n%s\nwant:\n%s", i, have, tc.want) } + + if !backend.overrideBloomCalled.Load() { + t.Error("expected OverrideHeaderBloom to be called") + } + backend.overrideBloomCalled.Store(false) } t.Run("timeout", func(t *testing.T) { From a64a2860a8da370fe1a1a6d46fb19acf4e5e04d9 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Wed, 11 Feb 2026 09:58:44 -0500 Subject: [PATCH 15/19] chore: remove barely unnecessary change --- eth/bloombits.go | 5 +---- eth/bloombits.libevm.go | 20 +------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/eth/bloombits.go b/eth/bloombits.go index cc680daae8fc..a87ea8f96ce3 100644 --- a/eth/bloombits.go +++ b/eth/bloombits.go @@ -43,12 +43,9 @@ const ( // startBloomHandlers starts a batch of goroutines to accept bloom bit database // retrievals from possibly a range of filters and serving the data to satisfy. -func (eth *Ethereum) startBloomHandlers(sectionSize uint64, opts ...bloomHandlersOption) { - wg := bloomHandlersWG(opts...) +func (eth *Ethereum) startBloomHandlers(sectionSize uint64) { for i := 0; i < bloomServiceThreads; i++ { - wg.Add(1) go func() { - defer wg.Done() for { select { case <-eth.closeBloomHandler: diff --git a/eth/bloombits.libevm.go b/eth/bloombits.libevm.go index 00c83889daa4..a32fb81b8cca 100644 --- a/eth/bloombits.libevm.go +++ b/eth/bloombits.libevm.go @@ -21,7 +21,6 @@ import ( "github.com/ava-labs/libevm/core/bloombits" "github.com/ava-labs/libevm/ethdb" - "github.com/ava-labs/libevm/libevm/options" ) const ( @@ -38,21 +37,6 @@ const ( BloomRetrievalWait = bloomRetrievalWait ) -// A bloomHandlersOption configures [Ethereum.startBloomHandlers]. -type bloomHandlersOption = options.Option[bloomHandlersConfig] - -type bloomHandlersConfig struct { - wg *sync.WaitGroup -} - -// bloomHandlersWG returns the last [sync.WaitGroup] set by an option, or a -// default value that exists only to avoid panics when used but it otherwise -// inaccessible. -func bloomHandlersWG(opts ...bloomHandlersOption) *sync.WaitGroup { - blackhole := &sync.WaitGroup{} - return options.ApplyTo(&bloomHandlersConfig{blackhole}, opts...).wg -} - // StartBloomHandlers starts a batch of goroutines to serve data for // [bloombits.Retrieval] requests from any number of filters. This is identical // to [Ethereum.startBloomHandlers], but exposed for independent use. @@ -66,9 +50,7 @@ func StartBloomHandlers(db ethdb.Database, sectionSize uint64) *BloomHandlers { closeBloomHandler: bh.quit, chainDb: db, } - eth.startBloomHandlers(sectionSize, options.Func[bloomHandlersConfig](func(c *bloomHandlersConfig) { - c.wg = &bh.wg - })) + eth.startBloomHandlers(sectionSize) return bh } From 4c97664cb1098074dcb90e0c70a543dd209fed54 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Wed, 11 Feb 2026 11:02:20 -0500 Subject: [PATCH 16/19] chore: oops --- eth/bloombits.libevm.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/eth/bloombits.libevm.go b/eth/bloombits.libevm.go index a32fb81b8cca..70b7604da72f 100644 --- a/eth/bloombits.libevm.go +++ b/eth/bloombits.libevm.go @@ -17,8 +17,6 @@ package eth import ( - "sync" - "github.com/ava-labs/libevm/core/bloombits" "github.com/ava-labs/libevm/ethdb" ) @@ -60,12 +58,10 @@ func StartBloomHandlers(db ethdb.Database, sectionSize uint64) *BloomHandlers { type BloomHandlers struct { Requests chan chan *bloombits.Retrieval quit chan struct{} - wg sync.WaitGroup } // Close releases resources in use by the [BloomHandlers]; repeated calls will // panic. func (bh *BloomHandlers) Close() { close(bh.quit) - bh.wg.Wait() } From 6d2eb68f45874134c90a3b44c23fa84c2d945b3a Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Wed, 11 Feb 2026 17:25:52 -0500 Subject: [PATCH 17/19] chore: reduce diff --- eth/filters/filter_system_test.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/eth/filters/filter_system_test.go b/eth/filters/filter_system_test.go index 184bd17763cb..08d75d0470a9 100644 --- a/eth/filters/filter_system_test.go +++ b/eth/filters/filter_system_test.go @@ -28,7 +28,7 @@ import ( "testing" "time" - "github.com/ava-labs/libevm" + ethereum "github.com/ava-labs/libevm" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/consensus/ethash" "github.com/ava-labs/libevm/core" @@ -44,16 +44,17 @@ import ( ) type testBackend struct { - db ethdb.Database - sections uint64 - txFeed event.Feed - logsFeed event.Feed - rmLogsFeed event.Feed - pendingLogsFeed event.Feed - chainFeed event.Feed - pendingBlock *types.Block - pendingReceipts types.Receipts - overrideBloomCalled atomic.Bool + db ethdb.Database + sections uint64 + txFeed event.Feed + logsFeed event.Feed + rmLogsFeed event.Feed + pendingLogsFeed event.Feed + chainFeed event.Feed + pendingBlock *types.Block + pendingReceipts types.Receipts + + overrideBloomCalled atomic.Bool //libevm } func (b *testBackend) ChainConfig() *params.ChainConfig { From e380f220bae2c3cdda57fc7c56cd58d358593111 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 12 Feb 2026 13:07:36 +0000 Subject: [PATCH 18/19] test: non-intrusive testing of Bloom overriding --- eth/filters/filter_system.go | 2 +- eth/filters/filter_system.libevm_test.go | 73 ++++++++++++++++++++++-- eth/filters/filter_system_test.go | 9 +-- eth/filters/filter_test.go | 9 +-- 4 files changed, 73 insertions(+), 20 deletions(-) diff --git a/eth/filters/filter_system.go b/eth/filters/filter_system.go index 97b0e1d26f27..2206b914cdc1 100644 --- a/eth/filters/filter_system.go +++ b/eth/filters/filter_system.go @@ -25,7 +25,7 @@ import ( "sync/atomic" "time" - "github.com/ava-labs/libevm" + ethereum "github.com/ava-labs/libevm" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/lru" "github.com/ava-labs/libevm/core" diff --git a/eth/filters/filter_system.libevm_test.go b/eth/filters/filter_system.libevm_test.go index 5a5cbf91f4c0..c0de2c0da041 100644 --- a/eth/filters/filter_system.libevm_test.go +++ b/eth/filters/filter_system.libevm_test.go @@ -16,11 +16,76 @@ package filters -import "github.com/ava-labs/libevm/core/types" +import ( + "math/big" + "testing" -var _ BloomOverrider = (*testBackend)(nil) + "github.com/stretchr/testify/require" -func (b *testBackend) OverrideHeaderBloom(header *types.Header) types.Bloom { - b.overrideBloomCalled.Store(true) + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/rpc" +) + +type bloomOverriderBackend struct { + *testBackend + overridden chan struct{} +} + +var _ BloomOverrider = (*bloomOverriderBackend)(nil) + +func (b *bloomOverriderBackend) OverrideHeaderBloom(header *types.Header) types.Bloom { + b.overridden <- struct{}{} return header.Bloom } + +func TestBloomOverride(t *testing.T) { + db := rawdb.NewMemoryDatabase() + backend, sys := newTestFilterSystem(t, db, Config{}) + sut := &bloomOverriderBackend{ + testBackend: backend, + overridden: make(chan struct{}), + } + sys.backend = sut + + t.Run("lightFilterLogs", func(t *testing.T) { + api := NewFilterAPI(sys, true /*lightMode*/) + defer CloseAPI(api) + + id, err := api.NewFilter(FilterCriteria{}) + require.NoErrorf(t, err, "%T.NewFilter()", api) + defer api.UninstallFilter(id) + + // If there is no historical header then the filter system returns early. + for i := range int64(2) { + sut.chainFeed.Send(core.ChainEvent{ + Block: types.NewBlockWithHeader(&types.Header{ + Number: big.NewInt(i), + }), + }) + } + <-sut.overridden + }) + + t.Run("blockLogs", func(t *testing.T) { + hdr := &types.Header{Number: big.NewInt(0)} + h := hdr.Hash() + rawdb.WriteHeader(db, hdr) + rawdb.WriteCanonicalHash(db, h, 0) + rawdb.WriteHeaderNumber(db, h, 0) + + go sys.NewBlockFilter(h, nil, nil).Logs(t.Context()) //nolint:errcheck // Known but irrelevant error + <-sut.overridden + }) + + t.Run("pendingLogs", func(t *testing.T) { + hdr := &types.Header{Number: big.NewInt(1)} + sut.pendingBlock = types.NewBlockWithHeader(hdr) + sut.pendingReceipts = types.Receipts{} + + n := rpc.PendingBlockNumber.Int64() + go sys.NewRangeFilter(n, n, nil, nil).Logs(t.Context()) //nolint:errcheck // Known but irrelevant error + <-sut.overridden + }) +} diff --git a/eth/filters/filter_system_test.go b/eth/filters/filter_system_test.go index 08d75d0470a9..822300d696d7 100644 --- a/eth/filters/filter_system_test.go +++ b/eth/filters/filter_system_test.go @@ -24,11 +24,10 @@ import ( "math/rand" "reflect" "runtime" - "sync/atomic" "testing" "time" - ethereum "github.com/ava-labs/libevm" + "github.com/ava-labs/libevm" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/consensus/ethash" "github.com/ava-labs/libevm/core" @@ -53,8 +52,6 @@ type testBackend struct { chainFeed event.Feed pendingBlock *types.Block pendingReceipts types.Receipts - - overrideBloomCalled atomic.Bool //libevm } func (b *testBackend) ChainConfig() *params.ChainConfig { @@ -906,10 +903,6 @@ func TestLightFilterLogs(t *testing.T) { } } } - - if !backend.overrideBloomCalled.Load() { - t.Error("expected OverrideHeaderBloom to be called") - } } // TestPendingTxFilterDeadlock tests if the event loop hangs when pending diff --git a/eth/filters/filter_test.go b/eth/filters/filter_test.go index 0809faed858b..c67d96fbba37 100644 --- a/eth/filters/filter_test.go +++ b/eth/filters/filter_test.go @@ -109,8 +109,8 @@ func BenchmarkFilters(b *testing.B) { func TestFilters(t *testing.T) { var ( - db = rawdb.NewMemoryDatabase() - backend, sys = newTestFilterSystem(t, db, Config{}) + db = rawdb.NewMemoryDatabase() + _, sys = newTestFilterSystem(t, db, Config{}) // Sender account key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") addr = crypto.PubkeyToAddress(key1.PublicKey) @@ -373,11 +373,6 @@ func TestFilters(t *testing.T) { if string(have) != tc.want { t.Fatalf("test %d, have:\n%s\nwant:\n%s", i, have, tc.want) } - - if !backend.overrideBloomCalled.Load() { - t.Error("expected OverrideHeaderBloom to be called") - } - backend.overrideBloomCalled.Store(false) } t.Run("timeout", func(t *testing.T) { From 260017494c5731be22570bdce4c30bbc2a043a00 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 12 Feb 2026 13:09:50 +0000 Subject: [PATCH 19/19] refactor: revert `ethereum` aliasing --- eth/filters/filter_system.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eth/filters/filter_system.go b/eth/filters/filter_system.go index 2206b914cdc1..97b0e1d26f27 100644 --- a/eth/filters/filter_system.go +++ b/eth/filters/filter_system.go @@ -25,7 +25,7 @@ import ( "sync/atomic" "time" - ethereum "github.com/ava-labs/libevm" + "github.com/ava-labs/libevm" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/lru" "github.com/ava-labs/libevm/core"