mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-21 20:26:41 +00:00
This is the implementation of EIP-8070 Sparse Blobpool. It introduces protocol version eth/72 which relays blob transaction cells instead of full blobs. The blobpool now store 'incomplete' transactions, where only some of the cells are provided. The stored cell indexes are taken from the custody bitmap, which is provided by the consensus layer in forkchoiceUpdatedV4. This method will be called once Glamsterdam activates, and the default custody is full custody, so for now there is no change in the amount of stored cells for now. The main entities added are the BlobBuffer and BlobFetcher, which work together to track and fetch missing cells from connected peers. The partial transactions become available for inclusion in blocks when they are covered by enough peers that hold all cells. This change also introduces engine_getBlobsV4, which allows for cell-based responses (and partial blobs with only some of the cells). We maintain backward compatibility with getBlobsV3 which expects full blob responses by recovering the blob from available cells. Since this process is resource-intensive, we proactively cache the conversion so it is ready in time for getBlobsV3 calls. This mechanism will be removed once support for getBlobsV4 is universal across all consensus layer implementations. devp2p tests for eth/72 are not part of this initial change. This is to avoid breaking test success status for execution clients that do not have eth/72 implemented yet. The tests will be added in a subsequent change. --------- Co-authored-by: Felix Lange <fjl@twurst.com>
207 lines
6 KiB
Go
207 lines
6 KiB
Go
// Copyright 2026 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it 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 go-ethereum library is distributed in the hope that it 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 blobpool
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
)
|
|
|
|
// maxPendingConversionTasks caps the number of pending conversion tasks. This
|
|
// prevents excessive memory usage; the worst-case scenario (2k transactions
|
|
// with 6 blobs each) would consume approximately 1.5GB of memory.
|
|
const maxPendingConversionTasks = 2048
|
|
|
|
type convertResult struct {
|
|
ptx *BlobTxForPool
|
|
err error
|
|
}
|
|
|
|
// txConvert represents a conversion task with an attached legacy blob transaction.
|
|
type txConvert struct {
|
|
tx *types.Transaction // Legacy blob transaction
|
|
done chan convertResult // Channel for signaling back if the conversion succeeds
|
|
}
|
|
|
|
// conversionQueue is a dedicated queue for converting legacy blob transactions
|
|
// received from the network after the Osaka fork. Since conversion is expensive,
|
|
// it is performed in the background by a single thread, ensuring the main Geth
|
|
// process is not overloaded.
|
|
type conversionQueue struct {
|
|
tasks chan *txConvert
|
|
startConversion chan func()
|
|
quit chan struct{}
|
|
closed chan struct{}
|
|
|
|
queue []func()
|
|
taskDone chan struct{}
|
|
}
|
|
|
|
// newConversionQueue constructs the conversion queue.
|
|
func newConversionQueue() *conversionQueue {
|
|
q := &conversionQueue{
|
|
tasks: make(chan *txConvert),
|
|
startConversion: make(chan func()),
|
|
quit: make(chan struct{}),
|
|
closed: make(chan struct{}),
|
|
}
|
|
go q.loop()
|
|
return q
|
|
}
|
|
|
|
// convert accepts a legacy blob transaction with version-0 blobs and queues it
|
|
// for conversion.
|
|
//
|
|
// This function may block for a long time until the transaction is processed.
|
|
func (q *conversionQueue) convert(tx *types.Transaction) (*BlobTxForPool, error) {
|
|
done := make(chan convertResult, 1)
|
|
select {
|
|
case q.tasks <- &txConvert{tx: tx, done: done}:
|
|
res := <-done
|
|
return res.ptx, res.err
|
|
case <-q.closed:
|
|
return nil, errors.New("conversion queue closed")
|
|
}
|
|
}
|
|
|
|
// launchConversion starts a conversion task in the background.
|
|
func (q *conversionQueue) launchConversion(fn func()) error {
|
|
select {
|
|
case q.startConversion <- fn:
|
|
return nil
|
|
case <-q.closed:
|
|
return errors.New("conversion queue closed")
|
|
}
|
|
}
|
|
|
|
// close terminates the conversion queue.
|
|
func (q *conversionQueue) close() {
|
|
select {
|
|
case <-q.closed:
|
|
return
|
|
default:
|
|
close(q.quit)
|
|
<-q.closed
|
|
}
|
|
}
|
|
|
|
// run converts a batch of legacy blob txs to the new cell proof format.
|
|
func (q *conversionQueue) run(tasks []*txConvert, done chan struct{}, interrupt *atomic.Int32) {
|
|
defer close(done)
|
|
|
|
for _, t := range tasks {
|
|
if interrupt != nil && interrupt.Load() != 0 {
|
|
t.done <- convertResult{err: errors.New("conversion is interrupted")}
|
|
continue
|
|
}
|
|
// Run the conversion, the original sidecar will be mutated in place
|
|
start := time.Now()
|
|
ptx, err := newBlobTxForPool(t.tx)
|
|
t.done <- convertResult{ptx: ptx, err: err}
|
|
log.Trace("Converted legacy blob tx", "hash", t.tx.Hash(), "err", err, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
}
|
|
}
|
|
|
|
func (q *conversionQueue) loop() {
|
|
defer close(q.closed)
|
|
|
|
var (
|
|
done chan struct{} // Non-nil if background routine is active
|
|
interrupt *atomic.Int32 // Flag to signal conversion interruption
|
|
|
|
// The pending tasks for sidecar conversion. We assume the number of legacy
|
|
// blob transactions requiring conversion will not be excessive. However,
|
|
// a hard cap is applied as a protective measure.
|
|
txTasks []*txConvert
|
|
)
|
|
|
|
for {
|
|
select {
|
|
case t := <-q.tasks:
|
|
if len(txTasks) >= maxPendingConversionTasks {
|
|
t.done <- convertResult{err: errors.New("conversion queue is overloaded")}
|
|
continue
|
|
}
|
|
txTasks = append(txTasks, t)
|
|
|
|
// Launch the background conversion thread if it's idle
|
|
if done == nil {
|
|
done, interrupt = make(chan struct{}), new(atomic.Int32)
|
|
|
|
tasks := slices.Clone(txTasks)
|
|
txTasks = txTasks[:0]
|
|
go q.run(tasks, done, interrupt)
|
|
}
|
|
|
|
case <-done:
|
|
done, interrupt = nil, nil
|
|
if len(txTasks) > 0 {
|
|
done, interrupt = make(chan struct{}), new(atomic.Int32)
|
|
tasks := slices.Clone(txTasks)
|
|
txTasks = txTasks[:0]
|
|
go q.run(tasks, done, interrupt)
|
|
}
|
|
|
|
case fn := <-q.startConversion:
|
|
q.queue = append(q.queue, fn)
|
|
q.runNextTask()
|
|
|
|
case <-q.taskDone:
|
|
q.runNextTask()
|
|
|
|
case <-q.quit:
|
|
if done != nil {
|
|
log.Debug("Waiting for blob proof conversion to exit")
|
|
interrupt.Store(1)
|
|
<-done
|
|
}
|
|
if q.taskDone != nil {
|
|
log.Debug("Waiting for blobpool billy conversion to exit")
|
|
<-q.taskDone
|
|
}
|
|
// Signal any tasks that were queued for the next batch but never started
|
|
// so callers blocked in convert() receive an error instead of hanging.
|
|
for _, t := range txTasks {
|
|
// Best-effort notify; t.done is a buffered channel of size 1
|
|
// created by convert(), and we send exactly once per task.
|
|
t.done <- convertResult{err: errors.New("conversion queue closed")}
|
|
}
|
|
// Drop references to allow GC of the backing array.
|
|
txTasks = txTasks[:0]
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (q *conversionQueue) runNextTask() {
|
|
if len(q.queue) == 0 {
|
|
q.taskDone = nil
|
|
return
|
|
}
|
|
fn := q.queue[0]
|
|
q.queue = append(q.queue[:0], q.queue[1:]...)
|
|
|
|
done := make(chan struct{})
|
|
go func() { defer close(done); fn() }()
|
|
q.taskDone = done
|
|
}
|