diff --git a/core/txpool/blobpool/limbo.go b/core/txpool/blobpool/limbo.go index e696a6fcc0..f941f06ec4 100644 --- a/core/txpool/blobpool/limbo.go +++ b/core/txpool/blobpool/limbo.go @@ -65,11 +65,18 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) { } // Index all limboed blobs on disk and delete anything unprocessable - var fails []uint64 + var ( + fails []uint64 + convert []uint64 + ) index := func(id uint64, size uint32, data []byte) { - if l.parseBlob(id, data) != nil { + err := l.parseBlob(id, data) + if err != nil { fails = append(fails, id) } + if errors.Is(err, errLegacyTx) { + convert = append(convert, id) + } } store, err := billy.Open(billy.Options{Path: datadir, Repair: true}, slotter, index) if err != nil { @@ -77,6 +84,39 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) { } l.store = store + // Migrate legacy limbo entries to blobTxForPool. Note all ids in `converted` are also + // in `fails`, and the converted entries will be deleted by the loop that handles + // `fails`. + for _, id := range convert { + data, err := l.store.Get(id) + if err != nil { + continue + } + var legacy struct { + TxHash common.Hash + Block uint64 + Tx *types.Transaction + } + if err := rlp.DecodeBytes(data, &legacy); err != nil { + continue + } + blob, err := rlp.EncodeToBytes(&limboBlob{ + TxHash: legacy.TxHash, + Block: legacy.Block, + Ptx: newBlobTxForPool(legacy.Tx), + }) + if err != nil { + continue + } + newID, err := l.store.Put(blob) + if err != nil { + continue + } + if err := l.parseBlob(newID, blob); err != nil { + fails = append(fails, newID) + } + } + if len(fails) > 0 { log.Warn("Dropping invalidated limboed blobs", "ids", fails) for _, id := range fails { @@ -99,6 +139,10 @@ func (l *limbo) Close() error { func (l *limbo) parseBlob(id uint64, data []byte) error { item := new(limboBlob) if err := rlp.DecodeBytes(data, item); err != nil { + // This entry may have been stored with the legacy limboBlob type + if isLegacy(data) { + return errLegacyTx + } // This path is impossible unless the disk data representation changes // across restarts. For that ever improbable case, recover gracefully // by ignoring this data entry. @@ -122,6 +166,17 @@ func (l *limbo) parseBlob(id uint64, data []byte) error { return nil } +// isLegacy reports whether data is a limbo entry with the legacy limboBlob +// {TxHash, Block, Tx *types.Transaction} type. +func isLegacy(data []byte) bool { + elems, err := rlp.SplitListValues(data) + if err != nil || len(elems) < 3 { + return false + } + kind, content, _, err := rlp.Split(elems[2]) + return err == nil && kind == rlp.String && len(content) > 1 && content[0] == types.BlobTxType +} + // finalize evicts all blobs belonging to a recently finalized block or older. func (l *limbo) finalize(final *types.Header) { // Just in case there's no final block yet (network not yet merged, weird diff --git a/core/txpool/blobpool/limbo_test.go b/core/txpool/blobpool/limbo_test.go new file mode 100644 index 0000000000..722587b5c1 --- /dev/null +++ b/core/txpool/blobpool/limbo_test.go @@ -0,0 +1,72 @@ +// 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 . + +package blobpool + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/billy" +) + +// TestLimboLegacyMigration checks whether limbo entries in the legacy limboBlob type +// are migrated to the blobTxForPool layout on startup instead of being dropped. +func TestLimboLegacyMigration(t *testing.T) { + key, _ := crypto.GenerateKey() + tx := makeMultiBlobTx(0, 10, 100, 100, 2, 0, key) + + dir := t.TempDir() + + // Write a single entry using the legacy on-disk layout. + store, err := billy.Open(billy.Options{Path: dir}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil) + if err != nil { + t.Fatalf("failed to open store: %v", err) + } + legacy := struct { + TxHash common.Hash + Block uint64 + Tx *types.Transaction + }{tx.Hash(), 42, tx} + data, err := rlp.EncodeToBytes(&legacy) + if err != nil { + t.Fatalf("failed to encode legacy entry: %v", err) + } + if _, err := store.Put(data); err != nil { + t.Fatalf("failed to store legacy entry: %v", err) + } + store.Close() + + // Open the limbo, which should migrate the legacy entry. + l, err := newLimbo(new(params.ChainConfig), dir) + if err != nil { + t.Fatalf("failed to open limbo: %v", err) + } + defer l.Close() + + // The migrated transaction must be tracked and reconstruct the original. + ptx, err := l.pull(tx.Hash()) + if err != nil { + t.Fatalf("failed to pull migrated tx: %v", err) + } + if got := ptx.ToTx().Hash(); got != tx.Hash() { + t.Fatalf("migrated tx hash mismatch: got %x, want %x", got, tx.Hash()) + } +}