feat: allow custom bloom production (#261)

## Why this should be merged

See ava-labs/strevm#120. To get bloom filters for a block when the
header doesn't contain the correct bloom because of SAE's delayed
settlement, you need a custom accessor to provide arbitrary bloom.

## How this works

Introduces an optional interface which, if satisfied by
`filters.Backend` implementations, is used to override the Bloom filter
instead of retrieving it from the `types.Header`.

## How this was tested

Existing tests unchanged. Integration test demonstrates calling of the
override method when present.

---------

Co-authored-by: Arran Schlosberg <me@arranschlosberg.com>
This commit is contained in:
Austin Larson 2026-02-12 08:36:04 -05:00 committed by GitHub
parent e673c70970
commit 62502c6712
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 272 additions and 3 deletions

View file

@ -0,0 +1,46 @@
// 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
// <http://www.gnu.org/licenses/>.
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
// 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,
size: size,
}
}
// 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 {
return err
}
b.head = header.Hash()
return nil
}

67
eth/bloombits.libevm.go Normal file
View file

@ -0,0 +1,67 @@
// 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
// <http://www.gnu.org/licenses/>.
package eth
import (
"github.com/ava-labs/libevm/core/bloombits"
"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 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,
}
eth.startBloomHandlers(sectionSize)
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{}
}
// Close releases resources in use by the [BloomHandlers]; repeated calls will
// panic.
func (bh *BloomHandlers) Close() {
close(bh.quit)
}

View file

@ -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
// <http://www.gnu.org/licenses/>.
package eth
import (
"testing"
"go.uber.org/goleak"
"github.com/ava-labs/libevm/core/rawdb"
)
func TestStartBloomHandlersNoLeaks(t *testing.T) {
defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
StartBloomHandlers(rawdb.NewMemoryDatabase(), 42).Close()
}

View file

@ -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(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(block.Bloom(), 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...)

View file

@ -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(maybeOverrideBloom(header, es.backend), addresses, topics) {
return nil
}
// Get the logs of the block

View file

@ -0,0 +1,35 @@
// 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
// <http://www.gnu.org/licenses/>.
package filters
import (
"github.com/ava-labs/libevm/core/types"
)
// 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 maybeOverrideBloom(header *types.Header, backend Backend) types.Bloom {
if bo, ok := backend.(BloomOverrider); ok {
return bo.OverrideHeaderBloom(header)
}
return header.Bloom
}

View file

@ -0,0 +1,91 @@
// 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
// <http://www.gnu.org/licenses/>.
package filters
import (
"math/big"
"testing"
"github.com/stretchr/testify/require"
"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
})
}