diff --git a/core/bloom_indexer.libevm.go b/core/bloom_indexer.libevm.go new file mode 100644 index 0000000000..70bb86cd95 --- /dev/null +++ b/core/bloom_indexer.libevm.go @@ -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 +// . + +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 +} diff --git a/eth/bloombits.libevm.go b/eth/bloombits.libevm.go new file mode 100644 index 0000000000..70b7604da7 --- /dev/null +++ b/eth/bloombits.libevm.go @@ -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 +// . + +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) +} diff --git a/eth/bloombits.libevm_test.go b/eth/bloombits.libevm_test.go new file mode 100644 index 0000000000..0e4dcba5a8 --- /dev/null +++ b/eth/bloombits.libevm_test.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 ( + "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() +} diff --git a/eth/filters/filter.go b/eth/filters/filter.go index 3f30c11a4b..9aa3b53955 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(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...) diff --git a/eth/filters/filter_system.go b/eth/filters/filter_system.go index f59a688a39..97b0e1d26f 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(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 new file mode 100644 index 0000000000..5762aa0065 --- /dev/null +++ b/eth/filters/filter_system.libevm.go @@ -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 +// . + +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 +} diff --git a/eth/filters/filter_system.libevm_test.go b/eth/filters/filter_system.libevm_test.go new file mode 100644 index 0000000000..c0de2c0da0 --- /dev/null +++ b/eth/filters/filter_system.libevm_test.go @@ -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 +// . + +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 + }) +}