From 5cbe6bd60c4d1ce10c3244530eb25b765e781e47 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 26 Sep 2025 09:22:22 -0700 Subject: [PATCH] eth/catalyst: implement getBlobsV3 --- eth/catalyst/api.go | 50 +++++++++++++++++++++++++++------------- eth/catalyst/api_test.go | 29 ++++++++++++++--------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index d6d3f579364..2be050f084d 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -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) ) type ConsensusAPI struct { @@ -565,6 +571,19 @@ 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))) } @@ -572,28 +591,22 @@ func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProo 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[:]) @@ -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 } diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index a023962b81d..4d7246d4ed2 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -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() @@ -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)) } } @@ -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 @@ -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)