Skip to content

Commit d97b518

Browse files
committed
core/txpool/blobpool: test provider serving of extension columns end to end
Add an integration test for the headline behavior of the provider extension: a transaction acquired as a full fetch (data cells only) is run through the buffer into a real pool, and must come out fully advertised (GetCustody all-ones, the mask announcements carry) and fully servable (GetBlobCells returns byte-correct extension cells with proofs -- the call behind both the eth GetCells handler and the engine_getBlobsV4 miss path). The test deliberately uses only APIs that predate the provider extension, so it runs unmodified against older code and demonstrates the gap there: advertised custody stays at the 64 data cells and every extension column is returned as nil (verified against master).
1 parent 0143a15 commit d97b518

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2026 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package blobpool
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
24+
"github.com/ethereum/go-ethereum/core/state"
25+
"github.com/ethereum/go-ethereum/core/tracing"
26+
"github.com/ethereum/go-ethereum/core/types"
27+
"github.com/ethereum/go-ethereum/crypto"
28+
"github.com/ethereum/go-ethereum/crypto/kzg4844"
29+
"github.com/ethereum/go-ethereum/params"
30+
"github.com/holiman/billy"
31+
"github.com/holiman/uint256"
32+
)
33+
34+
// TestProviderExtensionServing checks, end to end, that a transaction acquired
35+
// as a full fetch (only the data cells delivered) ends up fully servable and
36+
// fully advertised: the buffer completes it, the pool stores it, and
37+
//
38+
// - GetCustody reports all-ones -- the mask the tx announcement carries, so
39+
// peers may request any column, and
40+
// - GetBlobCells serves extension columns (>= DataPerBlob) byte-correctly --
41+
// the exact call behind both the eth GetCells handler (p2p serving) and the
42+
// engine_getBlobsV4 cache miss path (CL serving).
43+
//
44+
// This test intentionally uses only APIs that predate the provider-extension
45+
// change, so it can run against older code to demonstrate the gap: there, the
46+
// stored/advertised custody is just the data cells and extension columns are
47+
// returned as nil.
48+
func TestProviderExtensionServing(t *testing.T) {
49+
storage := t.TempDir()
50+
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
51+
store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil)
52+
store.Close()
53+
54+
var (
55+
key, _ = crypto.GenerateKey()
56+
addr = crypto.PubkeyToAddress(key.PublicKey)
57+
blobCount = 2
58+
// Post-eth/72 shape: blob payload elided, commitments and cell proofs kept.
59+
tx = removeBlobs(makeMultiBlobTx(0, 10, 2*params.InitialBaseFee, 100, blobCount, 0, key))
60+
hash = tx.Hash()
61+
)
62+
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
63+
statedb.AddBalance(addr, uint256.NewInt(1_000_000_000_000_000_000), tracing.BalanceChangeUnspecified)
64+
statedb.Commit(0, true, false)
65+
66+
chain := &testBlockChain{
67+
config: params.MainnetChainConfig,
68+
basefee: uint256.NewInt(params.InitialBaseFee),
69+
blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice),
70+
statedb: statedb,
71+
}
72+
pool := New(Config{Datadir: storage}, chain, nil)
73+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
74+
t.Fatalf("failed to create blob pool: %v", err)
75+
}
76+
defer pool.Close()
77+
78+
// Wire the buffer to the real pool, as the eth handler does, and run the
79+
// full-fetch ingest flow: tx body first, then a data-cells-only delivery.
80+
buf := NewBlobBuffer(BlobBufferFunctions{
81+
ValidateTx: pool.ValidateTxBasics,
82+
AddToPool: pool.AddPooledTx,
83+
DropPeer: func(peer string) {},
84+
})
85+
if err := buf.AddTx([]*types.Transaction{tx}, "peerA")[0]; err != nil {
86+
t.Fatalf("AddTx: %v", err)
87+
}
88+
dataIndices := make([]uint64, kzg4844.DataPerBlob)
89+
for i := range dataIndices {
90+
dataIndices[i] = uint64(i)
91+
}
92+
buf.AddCells(hash, map[string]*PeerDelivery{"peerB": makePeerDelivery(t, 0, blobCount, dataIndices)}, types.NewCustodyBitmap(dataIndices))
93+
94+
hashes, errs := buf.Flush()
95+
if len(hashes) != 1 || errs[0] != nil {
96+
t.Fatalf("expected 1 pooled tx, got %d (err %v)", len(hashes), errs)
97+
}
98+
99+
// Advertising: the announcement mask is built from GetCustody, so all-ones
100+
// here means peers are invited to request any column.
101+
if custody := pool.GetCustody(hash); custody == nil || *custody != types.CustodyBitmapAll {
102+
have := -1
103+
if custody != nil {
104+
have = custody.OneCount()
105+
}
106+
t.Errorf("advertised custody not all-ones: have %d cells, want %d", have, kzg4844.CellsPerBlob)
107+
}
108+
109+
// Serving: request extension columns the node never downloaded.
110+
extIndices := make([]uint64, 32)
111+
for i := range extIndices {
112+
extIndices[i] = uint64(kzg4844.DataPerBlob + i)
113+
}
114+
vhashes := pool.GetBlobHashes(hash)
115+
if len(vhashes) != blobCount {
116+
t.Fatalf("expected %d versioned hashes, got %d", blobCount, len(vhashes))
117+
}
118+
cells, proofs, err := pool.GetBlobCells(vhashes, types.NewCustodyBitmap(extIndices))
119+
if err != nil {
120+
t.Fatalf("GetBlobCells: %v", err)
121+
}
122+
for b := 0; b < blobCount; b++ {
123+
truth, err := kzg4844.ComputeCells([]kzg4844.Blob{*testBlobs[b]})
124+
if err != nil {
125+
t.Fatal(err)
126+
}
127+
for k, idx := range extIndices {
128+
if cells[b] == nil || cells[b][k] == nil {
129+
t.Errorf("blob %d: extension cell %d not served", b, idx)
130+
continue
131+
}
132+
if *cells[b][k] != truth[idx] {
133+
t.Errorf("blob %d: extension cell %d does not match ground truth", b, idx)
134+
}
135+
if proofs[b] == nil || proofs[b][k] == nil {
136+
t.Errorf("blob %d: extension proof %d not served", b, idx)
137+
}
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)