Skip to content

Commit 00516c7

Browse files
authored
core/txpool/blobpool, eth/catalyst: place null for missing blob (#32536)
This pull request fixes a regression, introduced in #32190 Specifically, in GetBlobsV1 engine API, if any blob is missing, the null should be placed in response, unfortunately a behavioral change was introduced in #32190, returning an error instead. What's more, a more comprehensive test suite is added to cover both `GetBlobsV1` and `GetBlobsV2` endpoints.
1 parent 0e82b6b commit 00516c7

File tree

4 files changed

+429
-63
lines changed

4 files changed

+429
-63
lines changed

core/txpool/blobpool/blobpool.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,13 @@ func (p *BlobPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
12981298
}
12991299

13001300
// GetBlobs returns a number of blobs and proofs for the given versioned hashes.
1301+
// Blobpool must place responses in the order given in the request, using null
1302+
// for any missing blobs.
1303+
//
1304+
// For instance, if the request is [A_versioned_hash, B_versioned_hash,
1305+
// C_versioned_hash] and blobpool has data for blobs A and C, but doesn't have
1306+
// data for B, the response MUST be [A, null, C].
1307+
//
13011308
// This is a utility method for the engine API, enabling consensus clients to
13021309
// retrieve blobs from the pools directly instead of the network.
13031310
func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blob, []kzg4844.Commitment, [][]kzg4844.Proof, error) {
@@ -1317,12 +1324,13 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo
13171324
if _, ok := filled[vhash]; ok {
13181325
continue
13191326
}
1320-
// Retrieve the corresponding blob tx with the vhash
1327+
// Retrieve the corresponding blob tx with the vhash, skip blob resolution
1328+
// if it's not found locally and place the null instead.
13211329
p.lock.RLock()
13221330
txID, exists := p.lookup.storeidOfBlob(vhash)
13231331
p.lock.RUnlock()
13241332
if !exists {
1325-
return nil, nil, nil, fmt.Errorf("blob with vhash %x is not found", vhash)
1333+
continue
13261334
}
13271335
data, err := p.store.Get(txID)
13281336
if err != nil {

core/txpool/blobpool/blobpool_test.go

Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"fmt"
2525
"math"
2626
"math/big"
27+
"math/rand"
2728
"os"
2829
"path/filepath"
2930
"reflect"
@@ -41,6 +42,7 @@ import (
4142
"github.com/ethereum/go-ethereum/core/types"
4243
"github.com/ethereum/go-ethereum/crypto"
4344
"github.com/ethereum/go-ethereum/crypto/kzg4844"
45+
"github.com/ethereum/go-ethereum/internal/testrand"
4446
"github.com/ethereum/go-ethereum/params"
4547
"github.com/ethereum/go-ethereum/rlp"
4648
"github.com/holiman/billy"
@@ -1814,10 +1816,10 @@ func TestGetBlobs(t *testing.T) {
18141816
}
18151817

18161818
cases := []struct {
1817-
start int
1818-
limit int
1819-
version byte
1820-
expErr bool
1819+
start int
1820+
limit int
1821+
fillRandom bool
1822+
version byte
18211823
}{
18221824
{
18231825
start: 0, limit: 6,
@@ -1827,6 +1829,14 @@ func TestGetBlobs(t *testing.T) {
18271829
start: 0, limit: 6,
18281830
version: types.BlobSidecarVersion1,
18291831
},
1832+
{
1833+
start: 0, limit: 6, fillRandom: true,
1834+
version: types.BlobSidecarVersion0,
1835+
},
1836+
{
1837+
start: 0, limit: 6, fillRandom: true,
1838+
version: types.BlobSidecarVersion1,
1839+
},
18301840
{
18311841
start: 3, limit: 9,
18321842
version: types.BlobSidecarVersion0,
@@ -1835,6 +1845,14 @@ func TestGetBlobs(t *testing.T) {
18351845
start: 3, limit: 9,
18361846
version: types.BlobSidecarVersion1,
18371847
},
1848+
{
1849+
start: 3, limit: 9, fillRandom: true,
1850+
version: types.BlobSidecarVersion0,
1851+
},
1852+
{
1853+
start: 3, limit: 9, fillRandom: true,
1854+
version: types.BlobSidecarVersion1,
1855+
},
18381856
{
18391857
start: 3, limit: 15,
18401858
version: types.BlobSidecarVersion0,
@@ -1843,6 +1861,14 @@ func TestGetBlobs(t *testing.T) {
18431861
start: 3, limit: 15,
18441862
version: types.BlobSidecarVersion1,
18451863
},
1864+
{
1865+
start: 3, limit: 15, fillRandom: true,
1866+
version: types.BlobSidecarVersion0,
1867+
},
1868+
{
1869+
start: 3, limit: 15, fillRandom: true,
1870+
version: types.BlobSidecarVersion1,
1871+
},
18461872
{
18471873
start: 0, limit: 18,
18481874
version: types.BlobSidecarVersion0,
@@ -1852,58 +1878,79 @@ func TestGetBlobs(t *testing.T) {
18521878
version: types.BlobSidecarVersion1,
18531879
},
18541880
{
1855-
start: 18, limit: 20,
1881+
start: 0, limit: 18, fillRandom: true,
18561882
version: types.BlobSidecarVersion0,
1857-
expErr: true,
1883+
},
1884+
{
1885+
start: 0, limit: 18, fillRandom: true,
1886+
version: types.BlobSidecarVersion1,
18581887
},
18591888
}
18601889
for i, c := range cases {
1861-
var vhashes []common.Hash
1890+
var (
1891+
vhashes []common.Hash
1892+
filled = make(map[int]struct{})
1893+
)
1894+
if c.fillRandom {
1895+
filled[len(vhashes)] = struct{}{}
1896+
vhashes = append(vhashes, testrand.Hash())
1897+
}
18621898
for j := c.start; j < c.limit; j++ {
18631899
vhashes = append(vhashes, testBlobVHashes[j])
1900+
if c.fillRandom && rand.Intn(2) == 0 {
1901+
filled[len(vhashes)] = struct{}{}
1902+
vhashes = append(vhashes, testrand.Hash())
1903+
}
1904+
}
1905+
if c.fillRandom {
1906+
filled[len(vhashes)] = struct{}{}
1907+
vhashes = append(vhashes, testrand.Hash())
18641908
}
18651909
blobs, _, proofs, err := pool.GetBlobs(vhashes, c.version)
1910+
if err != nil {
1911+
t.Errorf("Unexpected error for case %d, %v", i, err)
1912+
}
18661913

1867-
if c.expErr {
1868-
if err == nil {
1869-
t.Errorf("Unexpected return, want error for case %d", i)
1914+
// Cross validate what we received vs what we wanted
1915+
length := c.limit - c.start
1916+
wantLen := length + len(filled)
1917+
if len(blobs) != wantLen || len(proofs) != wantLen {
1918+
t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want %d", len(blobs), len(proofs), wantLen)
1919+
continue
1920+
}
1921+
1922+
var unknown int
1923+
for j := 0; j < len(blobs); j++ {
1924+
if _, exist := filled[j]; exist {
1925+
if blobs[j] != nil || proofs[j] != nil {
1926+
t.Errorf("Unexpected blob and proof, item %d", j)
1927+
}
1928+
unknown++
1929+
continue
18701930
}
1871-
} else {
1872-
if err != nil {
1873-
t.Errorf("Unexpected error for case %d, %v", i, err)
1931+
// If an item is missing, but shouldn't, error
1932+
if blobs[j] == nil || proofs[j] == nil {
1933+
t.Errorf("tracked blob retrieval failed: item %d, hash %x", j, vhashes[j])
1934+
continue
18741935
}
1875-
// Cross validate what we received vs what we wanted
1876-
length := c.limit - c.start
1877-
if len(blobs) != length || len(proofs) != length {
1878-
t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want %d", len(blobs), len(proofs), length)
1936+
// Item retrieved, make sure the blob matches the expectation
1937+
if *blobs[j] != *testBlobs[c.start+j-unknown] {
1938+
t.Errorf("retrieved blob mismatch: item %d, hash %x", j, vhashes[j])
18791939
continue
18801940
}
1881-
for j := 0; j < len(blobs); j++ {
1882-
// If an item is missing, but shouldn't, error
1883-
if blobs[j] == nil || proofs[j] == nil {
1884-
t.Errorf("tracked blob retrieval failed: item %d, hash %x", j, vhashes[j])
1885-
continue
1941+
// Item retrieved, make sure the proof matches the expectation
1942+
if c.version == types.BlobSidecarVersion0 {
1943+
if proofs[j][0] != testBlobProofs[c.start+j-unknown] {
1944+
t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
18861945
}
1887-
// Item retrieved, make sure the blob matches the expectation
1888-
if *blobs[j] != *testBlobs[c.start+j] {
1889-
t.Errorf("retrieved blob mismatch: item %d, hash %x", j, vhashes[j])
1890-
continue
1891-
}
1892-
// Item retrieved, make sure the proof matches the expectation
1893-
if c.version == types.BlobSidecarVersion0 {
1894-
if proofs[j][0] != testBlobProofs[c.start+j] {
1895-
t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
1896-
}
1897-
} else {
1898-
want, _ := kzg4844.ComputeCellProofs(blobs[j])
1899-
if !reflect.DeepEqual(want, proofs[j]) {
1900-
t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
1901-
}
1946+
} else {
1947+
want, _ := kzg4844.ComputeCellProofs(blobs[j])
1948+
if !reflect.DeepEqual(want, proofs[j]) {
1949+
t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
19021950
}
19031951
}
19041952
}
19051953
}
1906-
19071954
pool.Close()
19081955
}
19091956

eth/catalyst/api.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,26 @@ func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool) (*eng
458458
}
459459

460460
// GetBlobsV1 returns a blob from the transaction pool.
461+
//
462+
// Specification:
463+
//
464+
// Given an array of blob versioned hashes client software MUST respond with an
465+
// array of BlobAndProofV1 objects with matching versioned hashes, respecting the
466+
// order of versioned hashes in the input array.
467+
//
468+
// Client software MUST place responses in the order given in the request, using
469+
// null for any missing blobs. For instance:
470+
//
471+
// if the request is [A_versioned_hash, B_versioned_hash, C_versioned_hash] and
472+
// client software has data for blobs A and C, but doesn't have data for B, the
473+
// response MUST be [A, null, C].
474+
//
475+
// Client software MUST support request sizes of at least 128 blob versioned hashes.
476+
// The client MUST return -38004: Too large request error if the number of requested
477+
// blobs is too large.
478+
//
479+
// Client software MAY return an array of all null entries if syncing or otherwise
480+
// unable to serve blob pool data.
461481
func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProofV1, error) {
462482
if len(hashes) > 128 {
463483
return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes)))
@@ -468,6 +488,10 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProo
468488
}
469489
res := make([]*engine.BlobAndProofV1, len(hashes))
470490
for i := 0; i < len(blobs); i++ {
491+
// Skip the non-existing blob
492+
if blobs[i] == nil {
493+
continue
494+
}
471495
res[i] = &engine.BlobAndProofV1{
472496
Blob: blobs[i][:],
473497
Proof: proofs[i][0][:],
@@ -477,6 +501,33 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProo
477501
}
478502

479503
// GetBlobsV2 returns a blob from the transaction pool.
504+
//
505+
// Specification:
506+
// Refer to the specification for engine_getBlobsV1 with changes of the following:
507+
//
508+
// Given an array of blob versioned hashes client software MUST respond with an
509+
// array of BlobAndProofV2 objects with matching versioned hashes, respecting
510+
// the order of versioned hashes in the input array.
511+
//
512+
// Client software MUST return null in case of any missing or older version blobs.
513+
// For instance,
514+
//
515+
// - if the request is [A_versioned_hash, B_versioned_hash, C_versioned_hash] and
516+
// client software has data for blobs A and C, but doesn't have data for B, the
517+
// response MUST be null.
518+
//
519+
// - if the request is [A_versioned_hash_for_blob_with_blob_proof], the response
520+
// MUST be null as well.
521+
//
522+
// Note, geth internally make the conversion from old version to new one, so the
523+
// data will be returned normally.
524+
//
525+
// Client software MUST support request sizes of at least 128 blob versioned
526+
// hashes. The client MUST return -38004: Too large request error if the number
527+
// of requested blobs is too large.
528+
//
529+
// Client software MUST return null if syncing or otherwise unable to serve
530+
// blob pool data.
480531
func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProofV2, error) {
481532
if len(hashes) > 128 {
482533
return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes)))
@@ -498,6 +549,12 @@ func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProo
498549
}
499550
res := make([]*engine.BlobAndProofV2, len(hashes))
500551
for i := 0; i < len(blobs); i++ {
552+
// the blob is missing, return null as response. It should
553+
// be caught by `AvailableBlobs` though, perhaps data race
554+
// occurs between two calls.
555+
if blobs[i] == nil {
556+
return nil, nil
557+
}
501558
var cellProofs []hexutil.Bytes
502559
for _, proof := range proofs[i] {
503560
cellProofs = append(cellProofs, proof[:])

0 commit comments

Comments
 (0)