Skip to content

Commit dc43a31

Browse files
authored
op-service: improve beacon client robustness (#17866)
* op-service: always fall back to fetching blob sidecars Previously, we only reverted to fetching sidecars when an error was received from the beacon server. In an attempt to be more robust, we also fetch sidecars when the beacon server gives us a semantically invalid response. * op-service: rearrange blobs based on provided hashes The behavior in the post-Fulu /blobs/ happy path and the /blob_sidecars/ fallback slightly differed: in the former case, returned blobs were ordered based their index in the blob, while in the latter case they were ordered based on the provided hashes. This bug was not caught in testing because all calls to BeaconBlobs provide a slice of hashes sorted by index.
1 parent 96fea29 commit dc43a31

File tree

2 files changed

+113
-17
lines changed

2 files changed

+113
-17
lines changed

op-service/sources/l1_beacon_client.go

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"net/url"
1111
"path"
12+
"slices"
1213
"strconv"
1314
"sync"
1415

@@ -303,30 +304,53 @@ func (cl *L1BeaconClient) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hash
303304
if err != nil {
304305
return nil, err
305306
}
307+
blobs, errBeaconBlobs := cl.beaconBlobs(ctx, slot, hashes)
308+
if errBeaconBlobs == nil {
309+
return blobs, nil
310+
}
311+
// If fetching from the post-Fulu /blobs/ endpoint fails, fall back to /blob_sidecars/.
312+
errBeaconBlobs = fmt.Errorf("failed to get blobs: %w", errBeaconBlobs)
313+
blobSidecars, err := cl.getBlobSidecars(ctx, slot, hashes)
314+
if err != nil {
315+
return nil, fmt.Errorf("%w; failed to get blob sidecars for L1BlockRef %s after falling back: %w", errBeaconBlobs, ref, err)
316+
}
317+
blobs, err = blobsFromSidecars(blobSidecars, hashes)
318+
if err != nil {
319+
return nil, fmt.Errorf("%w; failed to get blobs from sidecars for L1BlockRef %s after falling back: %w", errBeaconBlobs, ref, err)
320+
}
321+
return blobs, nil
322+
}
323+
324+
func (cl *L1BeaconClient) beaconBlobs(ctx context.Context, slot uint64, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) {
306325
resp, err := cl.cl.BeaconBlobs(ctx, slot, hashes)
307326
if err != nil {
308-
// We would normally check for an explicit error like "method not found", but the Beacon
309-
// API doesn't standardize such a response. Thus, we interpret all errors as
310-
// "method not found" and fall back to fetching sidecars.
311-
blobSidecars, err := cl.getBlobSidecars(ctx, slot, hashes)
312-
if err != nil {
313-
return nil, fmt.Errorf("failed to get blob sidecars for L1BlockRef %s: %w", ref, err)
314-
}
315-
blobs, err := blobsFromSidecars(blobSidecars, hashes)
316-
if err != nil {
317-
return nil, fmt.Errorf("failed to get blobs from sidecars for L1BlockRef %s: %w", ref, err)
318-
}
319-
return blobs, nil
327+
return nil, fmt.Errorf("get blobs from beacon client: %w", err)
320328
}
321329
if len(resp.Data) != len(hashes) {
322330
return nil, fmt.Errorf("expected %d blobs but got %d", len(hashes), len(resp.Data))
323331
}
324-
var blobs []*eth.Blob
325-
for i, blob := range resp.Data {
326-
if err := verifyBlob(blob, hashes[i].Hash); err != nil {
327-
return nil, fmt.Errorf("blob %d failed verification: %w", i, err)
332+
// This function guarantees that the returned blobs will be ordered according to the provided
333+
// hashes. The BeaconBlobs call above has a different ordering. From the getBlobs spec:
334+
// The returned blobs are ordered based on their kzg commitments in the block.
335+
// https://ethereum.github.io/beacon-APIs/beacon-node-oapi.yaml
336+
//
337+
// This loop
338+
// 1. verifies the integrity of each blob, and
339+
// 2. rearranges the blobs to match the order of the provided hashes.
340+
blobs := make([]*eth.Blob, len(hashes))
341+
for _, blob := range resp.Data {
342+
commitment, err := blob.ComputeKZGCommitment()
343+
if err != nil {
344+
return nil, fmt.Errorf("compute blob kzg commitment: %w", err)
345+
}
346+
got := eth.KZGToVersionedHash(commitment)
347+
idx := slices.IndexFunc(hashes, func(indexedHash eth.IndexedBlobHash) bool {
348+
return got == indexedHash.Hash
349+
})
350+
if idx == -1 {
351+
return nil, fmt.Errorf("received a blob hash that does not match any expected hash: %s", got)
328352
}
329-
blobs = append(blobs, blob)
353+
blobs[idx] = blob
330354
}
331355
return blobs, nil
332356
}

op-service/sources/l1_beacon_client_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,75 @@ func TestVerifyBlob(t *testing.T) {
321321
differentBlob[0] = byte(8)
322322
require.Error(t, verifyBlob(&differentBlob, versionedHash))
323323
}
324+
325+
func TestGetBlobs(t *testing.T) {
326+
hash0, sidecar0 := makeTestBlobSidecar(0)
327+
hash1, sidecar1 := makeTestBlobSidecar(1)
328+
hash2, sidecar2 := makeTestBlobSidecar(2)
329+
330+
hashes := []eth.IndexedBlobHash{hash0, hash2, hash1} // Mix up the order.
331+
332+
invalidBlob0 := sidecar0.Blob
333+
invalidBlob0[10]++
334+
335+
cases := []struct {
336+
name string
337+
beaconBlobs []*eth.Blob
338+
expectFallback bool
339+
}{
340+
{
341+
name: "happy path",
342+
// From the /blobs/ spec:
343+
// Blobs are returned as an ordered list matching the order of their corresponding
344+
// KZG commitments in the block.
345+
beaconBlobs: []*eth.Blob{&sidecar0.Blob, &sidecar1.Blob, &sidecar2.Blob},
346+
expectFallback: false,
347+
},
348+
{
349+
name: "fallback on client error",
350+
beaconBlobs: nil,
351+
expectFallback: true,
352+
},
353+
{
354+
name: "fallback on invalid number of blobs",
355+
beaconBlobs: []*eth.Blob{&sidecar0.Blob},
356+
expectFallback: true,
357+
},
358+
{
359+
name: "fallback on invalid blob",
360+
beaconBlobs: []*eth.Blob{&invalidBlob0, &sidecar1.Blob, &sidecar2.Blob},
361+
expectFallback: true,
362+
},
363+
}
364+
365+
for _, c := range cases {
366+
t.Run(c.name, func(t *testing.T) {
367+
ctx := context.Background()
368+
p := mocks.NewBeaconClient(t)
369+
p.EXPECT().BeaconGenesis(ctx).Return(eth.APIGenesisResponse{Data: eth.ReducedGenesisData{GenesisTime: 10}}, nil)
370+
p.EXPECT().ConfigSpec(ctx).Return(eth.APIConfigResponse{Data: eth.ReducedConfigData{SecondsPerSlot: 2}}, nil)
371+
client := NewL1BeaconClient(p, L1BeaconClientConfig{})
372+
ref := eth.L1BlockRef{Time: 12}
373+
374+
// construct the mock response for the beacon blobs call
375+
var beaconBlobsResponse eth.APIBeaconBlobsResponse
376+
var err error
377+
if c.beaconBlobs == nil {
378+
err = errors.New("client error")
379+
} else {
380+
beaconBlobsResponse = eth.APIBeaconBlobsResponse{Data: c.beaconBlobs}
381+
}
382+
p.EXPECT().BeaconBlobs(ctx, uint64(1), hashes).Return(beaconBlobsResponse, err)
383+
384+
if c.expectFallback {
385+
p.EXPECT().BeaconBlobSideCars(ctx, false, uint64(1), hashes).Return(eth.APIGetBlobSidecarsResponse{
386+
Data: toAPISideCars([]*eth.BlobSidecar{sidecar0, sidecar1, sidecar2}),
387+
}, nil)
388+
}
389+
390+
resp, err := client.GetBlobs(ctx, ref, hashes)
391+
require.NoError(t, err)
392+
require.Equal(t, []*eth.Blob{&sidecar0.Blob, &sidecar2.Blob, &sidecar1.Blob}, resp)
393+
})
394+
}
395+
}

0 commit comments

Comments
 (0)