Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 34 additions & 16 deletions eth/catalyst/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ var (

// Number of times getBlobsV2 responded with “miss”
getBlobsV2RequestMiss = metrics.NewRegisteredCounter("engine/getblobs/miss", nil)

// Number of blobs getBlobsV3 could return
getBlobsV3RequestHit = metrics.NewRegisteredCounter("engine/getblobsV3/hit", nil)

// Number of blobs getBlobsV3 could not return
getBlobsV3RequestMiss = metrics.NewRegisteredCounter("engine/getblobsV3/miss", nil)
Copy link
Contributor

@fjl fjl Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why it's a separate metric for v3. We can just use the existing getblobs metrics. If we add the V3 in metrics names, all the dashboards have to be changed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing metric only counts if all blobs are available or unavailable. The new one counts how many were available. We could change the old one and integrate both

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the old metric would change the semantics though. The old metric implicitly counts "number of requests that returned data". I'd keep v2 as is (and potentially add the blob-counting version too to v2, as it's not easy to get down to blob hits/misses today AFAIK -- multiplying the metric by blobs included is not an option).

)

type ConsensusAPI struct {
Expand Down Expand Up @@ -565,35 +571,42 @@ func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProo
if api.config().LatestFork(head.Time) < forks.Osaka {
return nil, unsupportedForkErr("engine_getBlobsV2 is not available before Osaka fork")
}
return api.getBlobs(hashes, false)
}

// GetBlobsV3 returns a set of blobs from the transaction pool. Same as
// GetBlobsV2, except will return partial responses in case there is a missing
// blob.
func (api *ConsensusAPI) GetBlobsV3(hashes []common.Hash) ([]*engine.BlobAndProofV2, error) {
return api.getBlobs(hashes, true)
}

// getBlobs returns all available blobs.
// if allowPartials is not set, either all or no blobs are returned.
func (api *ConsensusAPI) getBlobs(hashes []common.Hash, allowPartials bool) ([]*engine.BlobAndProofV2, error) {
if len(hashes) > 128 {
return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes)))
}
available := api.eth.BlobTxPool().AvailableBlobs(hashes)
getBlobsRequestedCounter.Inc(int64(len(hashes)))
getBlobsAvailableCounter.Inc(int64(available))

// Optimization: check first if all blobs are available, if not, return empty response
if available != len(hashes) {
getBlobsV2RequestMiss.Inc(1)
return nil, nil
}

blobs, _, proofs, err := api.eth.BlobTxPool().GetBlobs(hashes, types.BlobSidecarVersion1, false)
if err != nil {
return nil, engine.InvalidParams.With(err)
}

// To comply with API spec, check again that we really got all data needed
for _, blob := range blobs {
if blob == nil {
getBlobsV2RequestMiss.Inc(1)
return nil, nil
}
}
getBlobsV2RequestHit.Inc(1)

res := make([]*engine.BlobAndProofV2, len(hashes))
for i := 0; i < len(blobs); i++ {
for i := range blobs {
if blobs[i] == nil {
if allowPartials {
getBlobsV3RequestMiss.Inc(1)
continue
} else {
getBlobsV2RequestMiss.Inc(1)
return nil, nil
}
}
var cellProofs []hexutil.Bytes
for _, proof := range proofs[i] {
cellProofs = append(cellProofs, proof[:])
Expand All @@ -603,6 +616,11 @@ func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProo
CellProofs: cellProofs,
}
}
if allowPartials {
getBlobsV3RequestHit.Inc(int64(len(blobs)))
} else {
getBlobsV2RequestHit.Inc(1)
}
return res, nil
}

Expand Down
29 changes: 18 additions & 11 deletions eth/catalyst/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2016,7 +2016,7 @@ func TestGetBlobsV1AfterOsakaFork(t *testing.T) {
}
}

func TestGetBlobsV2(t *testing.T) {
func TestGetBlobsV2And3(t *testing.T) {
n, api := newGetBlobEnv(t, 1)
defer n.Close()

Expand Down Expand Up @@ -2045,7 +2045,8 @@ func TestGetBlobsV2(t *testing.T) {
},
}
for i, suite := range suites {
runGetBlobsV2(t, api, suite.start, suite.limit, suite.fillRandom, fmt.Sprintf("suite=%d", i))
runGetBlobs(t, api.GetBlobsV2, suite.start, suite.limit, suite.fillRandom, false, fmt.Sprintf("GetBlobsV2 suite=%d", i))
runGetBlobs(t, api.GetBlobsV3, suite.start, suite.limit, suite.fillRandom, true, fmt.Sprintf("GetBlobsV3 suite=%d %v", i, suite))
}
}

Expand All @@ -2060,22 +2061,20 @@ func BenchmarkGetBlobsV2(b *testing.B) {
name := fmt.Sprintf("blobs=%d", blobs)
b.Run(name, func(b *testing.B) {
for b.Loop() {
runGetBlobsV2(b, api, 0, blobs, false, name)
runGetBlobs(b, api.GetBlobsV2, 0, blobs, false, false, name)
}
})
}
}

func runGetBlobsV2(t testing.TB, api *ConsensusAPI, start, limit int, fillRandom bool, name string) {
type getBlobsFn func(hashes []common.Hash) ([]*engine.BlobAndProofV2, error)

func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom bool, expectPartialResponse bool, name string) {
// Fill the request for retrieving blobs
var (
vhashes []common.Hash
expect []*engine.BlobAndProofV2
)
// fill missing blob
if fillRandom {
vhashes = append(vhashes, testrand.Hash())
}
for j := start; j < limit; j++ {
vhashes = append(vhashes, testBlobVHashes[j])
var cellProofs []hexutil.Bytes
Expand All @@ -2087,13 +2086,21 @@ func runGetBlobsV2(t testing.TB, api *ConsensusAPI, start, limit int, fillRandom
CellProofs: cellProofs,
})
}
result, err := api.GetBlobsV2(vhashes)
// fill missing blob
if fillRandom {
vhashes = append(vhashes, testrand.Hash())
}
result, err := getBlobs(vhashes)
if err != nil {
t.Errorf("Unexpected error for case %s, %v", name, err)
}
// null is responded if any blob is missing
if fillRandom {
expect = nil
if expectPartialResponse {
expect = append(expect, nil)
} else {
// Nil is expected if getBlobs can not return a partial response
expect = nil
}
}
if !reflect.DeepEqual(result, expect) {
t.Fatalf("Unexpected result for case %s", name)
Expand Down