mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
core/txpool/blobpool: preserve limbo entry on update store failure
The previous limbo.update implementation deleted the existing entry from the store and indices before re-inserting it under the new block. If the re-insertion (setAndIndex) failed, the blob sidecar was permanently lost: limbo is the only source of truth for the sidecar once the blob transaction has been offloaded out of the blobpool, and EIP-4844 blob data is not stored in chain state. Reorder the operations so the new entry is written first. Only after the store Put succeeds is the old entry dropped from the store and the old group mapping cleaned up. A failure to drop the old store id now only leaks a billy slot - the blob itself is preserved under the new id, and a follow-up reorg-rollback can still re-inject it. Adds a round-trip test that updates the inclusion block of a limbo'd blob and verifies it remains pullable under the new block. Fixes #34944
This commit is contained in:
parent
da34eb59fd
commit
64822da467
2 changed files with 100 additions and 3 deletions
|
|
@ -202,16 +202,37 @@ func (l *limbo) update(txhash common.Hash, block uint64) {
|
|||
return
|
||||
}
|
||||
// Retrieve the old blobs from the data store and write them back with a new
|
||||
// block number. IF anything fails, there's not much to do, go on.
|
||||
item, err := l.getAndDrop(id)
|
||||
// block number. The new entry is written before the old one is dropped: blob
|
||||
// sidecars are not stored in chain state, so the limbo is the only place
|
||||
// from which a re-injection on a reorg-rollback can recover them. Dropping
|
||||
// the existing entry first (as the previous implementation did) would lose
|
||||
// the blob outright if the subsequent setAndIndex call failed.
|
||||
data, err := l.store.Get(id)
|
||||
if err != nil {
|
||||
log.Error("Failed to get and drop limboed blobs", "tx", txhash, "id", id, "err", err)
|
||||
log.Error("Failed to load limboed blobs", "tx", txhash, "id", id, "err", err)
|
||||
return
|
||||
}
|
||||
item := new(limboBlob)
|
||||
if err := rlp.DecodeBytes(data, item); err != nil {
|
||||
log.Error("Failed to decode limboed blobs", "tx", txhash, "id", id, "err", err)
|
||||
return
|
||||
}
|
||||
if err := l.setAndIndex(item.Ptx, block); err != nil {
|
||||
log.Error("Failed to set and index limboed blobs", "tx", txhash, "err", err)
|
||||
return
|
||||
}
|
||||
// The new entry is now durable in the store and indexed under the new
|
||||
// block; setAndIndex has overwritten l.index[txhash] with the new id, so
|
||||
// clean up the old store entry and group mapping. A failure to drop the
|
||||
// old store id only leaks a billy slot - the blob is still preserved
|
||||
// under the new id.
|
||||
if err := l.store.Delete(id); err != nil {
|
||||
log.Error("Failed to drop superseded limbo entry", "tx", txhash, "old-id", id, "err", err)
|
||||
}
|
||||
delete(l.groups[item.Block], id)
|
||||
if len(l.groups[item.Block]) == 0 {
|
||||
delete(l.groups, item.Block)
|
||||
}
|
||||
log.Trace("Blob transaction updated in limbo", "tx", txhash, "old-block", item.Block, "new-block", block)
|
||||
}
|
||||
|
||||
|
|
|
|||
76
core/txpool/blobpool/limbo_test.go
Normal file
76
core/txpool/blobpool/limbo_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
// TestLimboUpdateRoundTrip checks that update() relocates a tracked blob
|
||||
// transaction to a new block while keeping it pullable.
|
||||
//
|
||||
// This is the regression test for #34944. The previous implementation deleted
|
||||
// the existing limbo entry before inserting the relocated one, so a failure of
|
||||
// the second step would have permanently dropped the blob sidecar. The current
|
||||
// implementation writes the new entry first and only drops the old one once
|
||||
// the new one is durable; the round-trip below exercises that happy path.
|
||||
func TestLimboUpdateRoundTrip(t *testing.T) {
|
||||
limbo, err := newLimbo(params.MainnetChainConfig, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open limbo: %v", err)
|
||||
}
|
||||
defer limbo.Close()
|
||||
|
||||
key, _ := crypto.GenerateKey()
|
||||
tx := makeTx(0, 1, 1, 1, key)
|
||||
hash := tx.Hash()
|
||||
|
||||
if err := limbo.push(newBlobTxForPool(tx), 100); err != nil {
|
||||
t.Fatalf("push failed: %v", err)
|
||||
}
|
||||
if _, ok := limbo.index[hash]; !ok {
|
||||
t.Fatalf("tx not indexed after push")
|
||||
}
|
||||
oldID := limbo.index[hash]
|
||||
if _, ok := limbo.groups[100][oldID]; !ok {
|
||||
t.Fatalf("tx not in groups[100] after push")
|
||||
}
|
||||
|
||||
limbo.update(hash, 101)
|
||||
|
||||
if _, ok := limbo.groups[100]; ok {
|
||||
t.Fatalf("old block group not cleaned up after update")
|
||||
}
|
||||
newID, ok := limbo.index[hash]
|
||||
if !ok {
|
||||
t.Fatalf("tx no longer indexed after update")
|
||||
}
|
||||
if _, ok := limbo.groups[101][newID]; !ok {
|
||||
t.Fatalf("tx not in groups[101] after update")
|
||||
}
|
||||
|
||||
pulled, err := limbo.pull(hash)
|
||||
if err != nil {
|
||||
t.Fatalf("pull after update failed: %v", err)
|
||||
}
|
||||
if pulled.Tx.Hash() != hash {
|
||||
t.Fatalf("pulled tx hash mismatch: got %x want %x", pulled.Tx.Hash(), hash)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue