Skip to content
Merged
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
1 change: 1 addition & 0 deletions op-service/apis/beacon.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type BeaconClient interface {
NodeVersion(ctx context.Context) (string, error)
ConfigSpec(ctx context.Context) (eth.APIConfigResponse, error)
BeaconGenesis(ctx context.Context) (eth.APIGenesisResponse, error)
BeaconBlobs(ctx context.Context, slot uint64, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error)
BlobSideCarsClient
}

Expand Down
71 changes: 60 additions & 11 deletions op-service/sources/l1_beacon_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
specMethod = "eth/v1/config/spec"
genesisMethod = "eth/v1/beacon/genesis"
sidecarsMethodPrefix = "eth/v1/beacon/blob_sidecars/"
blobsMethodPrefix = "eth/v1/beacon/blobs/"
)

type L1BeaconClientConfig struct {
Expand Down Expand Up @@ -100,6 +101,19 @@ func (cl *BeaconHTTPClient) BeaconGenesis(ctx context.Context) (eth.APIGenesisRe
return genesisResp, nil
}

func (cl *BeaconHTTPClient) BeaconBlobs(ctx context.Context, slot uint64, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) {
reqQuery := url.Values{}
for _, hash := range hashes {
reqQuery.Add("versioned_hashes", hash.Hash.Hex())
}
reqPath := path.Join(blobsMethodPrefix, strconv.FormatUint(slot, 10))
var blobsResp []*eth.Blob
if err := cl.apiReq(ctx, &blobsResp, reqPath, reqQuery); err != nil {
return nil, err
}
return blobsResp, nil
}

func (cl *BeaconHTTPClient) BeaconBlobSideCars(ctx context.Context, fetchAllSidecars bool, slot uint64, hashes []eth.IndexedBlobHash) (eth.APIGetBlobSidecarsResponse, error) {
reqPath := path.Join(sidecarsMethodPrefix, strconv.FormatUint(slot, 10))
var reqQuery url.Values
Expand Down Expand Up @@ -170,8 +184,8 @@ func NewL1BeaconClient(cl apis.BeaconClient, cfg L1BeaconClientConfig, fallbacks

type TimeToSlotFn func(timestamp uint64) (uint64, error)

// GetTimeToSlotFn returns a function that converts a timestamp to a slot number.
func (cl *L1BeaconClient) GetTimeToSlotFn(ctx context.Context) (TimeToSlotFn, error) {
// getTimeToSlotFn returns a function that converts a timestamp to a slot number.
func (cl *L1BeaconClient) getTimeToSlotFn(ctx context.Context) (TimeToSlotFn, error) {
cl.initLock.Lock()
defer cl.initLock.Unlock()
if cl.timeToSlotFn != nil {
Expand Down Expand Up @@ -202,6 +216,18 @@ func (cl *L1BeaconClient) GetTimeToSlotFn(ctx context.Context) (TimeToSlotFn, er
return cl.timeToSlotFn, nil
}

func (cl *L1BeaconClient) timeToSlot(ctx context.Context, timestamp uint64) (uint64, error) {
slotFn, err := cl.getTimeToSlotFn(ctx)
if err != nil {
return 0, fmt.Errorf("get time to slot fn: %w", err)
}
slot, err := slotFn(timestamp)
if err != nil {
return 0, fmt.Errorf("convert timestamp %d to slot number: %w", timestamp, err)
}
return slot, nil
}

func (cl *L1BeaconClient) fetchSidecars(ctx context.Context, slot uint64, hashes []eth.IndexedBlobHash) (eth.APIGetBlobSidecarsResponse, error) {
var errs []error
for i := 0; i < cl.pool.Len(); i++ {
Expand All @@ -225,18 +251,21 @@ func (cl *L1BeaconClient) GetBlobSidecars(ctx context.Context, ref eth.L1BlockRe
if len(hashes) == 0 {
return []*eth.BlobSidecar{}, nil
}
slotFn, err := cl.GetTimeToSlotFn(ctx)
slot, err := cl.timeToSlot(ctx, ref.Time)
if err != nil {
return nil, fmt.Errorf("failed to get time to slot function: %w", err)
return nil, err
}
slot, err := slotFn(ref.Time)
sidecars, err := cl.getBlobSidecars(ctx, slot, hashes)
if err != nil {
return nil, fmt.Errorf("error in converting ref.Time to slot: %w", err)
return nil, fmt.Errorf("get blob sidecars for block %v: %w", ref, err)
}
return sidecars, nil
}

func (cl *L1BeaconClient) getBlobSidecars(ctx context.Context, slot uint64, hashes []eth.IndexedBlobHash) ([]*eth.BlobSidecar, error) {
resp, err := cl.fetchSidecars(ctx, slot, hashes)
if err != nil {
return nil, fmt.Errorf("failed to fetch blob sidecars for slot %v block %v: %w", slot, ref, err)
return nil, fmt.Errorf("failed to fetch blob sidecars for slot %v: %w", slot, err)
}

apiscs := make([]*eth.APIBlobSidecar, 0, len(hashes))
Expand Down Expand Up @@ -267,17 +296,37 @@ func (cl *L1BeaconClient) GetBlobSidecars(ctx context.Context, ref eth.L1BlockRe
// blob's validity by checking its proof against the commitment, and confirming the commitment
// hashes to the expected value. Returns error if any blob is found invalid.
func (cl *L1BeaconClient) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) {
blobSidecars, err := cl.GetBlobSidecars(ctx, ref, hashes)
if len(hashes) == 0 {
return []*eth.Blob{}, nil
}
slot, err := cl.timeToSlot(ctx, ref.Time)
if err != nil {
return nil, fmt.Errorf("failed to get blob sidecars for L1BlockRef %s: %w", ref, err)
return nil, err
}
blobs, err := blobsFromSidecars(blobSidecars, hashes)
blobs, err := cl.cl.BeaconBlobs(ctx, slot, hashes)
if err != nil {
return nil, fmt.Errorf("failed to get blobs from sidecars for L1BlockRef %s: %w", ref, err)
// We would normally check for an explicit error like "method not found", but the Beacon
// API doesn't standardize such a response. Thus, we interpret all errors as
// "method not found" and fall back to fetching sidecars.
blobSidecars, err := cl.getBlobSidecars(ctx, slot, hashes)
if err != nil {
return nil, fmt.Errorf("failed to get blob sidecars for L1BlockRef %s: %w", ref, err)
}
blobs, err := blobsFromSidecars(blobSidecars, hashes)
Comment on lines +311 to +315
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suggest we set a state variable like cl.useSidecars in the client if it ever had to fall back to querying sidecars and that was successful, so the client doesn't every time query the blobs endpoint again that's already proven not to work. If encountering an error, it can set this back to false so that an endpoint that suddenly upgrades to have the new endpoint would also keep working.

Suggested change
blobSidecars, err := cl.getBlobSidecars(ctx, slot, hashes)
if err != nil {
return nil, fmt.Errorf("failed to get blob sidecars for L1BlockRef %s: %w", ref, err)
}
blobs, err := blobsFromSidecars(blobSidecars, hashes)
blobSidecars, err := cl.getBlobSidecars(ctx, slot, hashes)
if err != nil {
cl.useSidecars = false
return nil, fmt.Errorf("failed to get blob sidecars for L1BlockRef %s: %w", ref, err)
}
cl.useSidecars = true
blobs, err := blobsFromSidecars(blobSidecars, hashes)

This state variable would be checked above before calling cl.cl.BeaconBlobs(ctx, slot, hashes)

if err != nil {
return nil, fmt.Errorf("failed to get blobs from sidecars for L1BlockRef %s: %w", ref, err)
}
return blobs, nil
}
for i, blob := range blobs {
if err := verifyBlob(blob, hashes[i].Hash); err != nil {
return nil, fmt.Errorf("blob %d failed verification: %w", i, err)
}
}
return blobs, nil
}

// blobsFromSidecars pulls the blobs from the sidecars and verifies them against the supplied hashes.
func blobsFromSidecars(blobSidecars []*eth.BlobSidecar, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) {
if len(blobSidecars) != len(hashes) {
return nil, fmt.Errorf("number of hashes and blobSidecars mismatch, %d != %d", len(hashes), len(blobSidecars))
Expand Down
34 changes: 25 additions & 9 deletions op-service/sources/l1_beacon_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,20 +233,36 @@ func TestBeaconClientBadProof(t *testing.T) {

hashes := []eth.IndexedBlobHash{index0, index1, index2}
sidecars := []*eth.BlobSidecar{sidecar0, sidecar1, sidecar2}
blobs := []*eth.Blob{&sidecar0.Blob, &sidecar1.Blob, &sidecar2.Blob}

// invalidate proof
sidecar1.KZGProof = eth.Bytes48(badProof)
apiSidecars := toAPISideCars(sidecars)

ctx := context.Background()
p := mocks.NewBeaconClient(t)

p.EXPECT().BeaconGenesis(ctx).Return(eth.APIGenesisResponse{Data: eth.ReducedGenesisData{GenesisTime: 10}}, nil)
p.EXPECT().ConfigSpec(ctx).Return(eth.APIConfigResponse{Data: eth.ReducedConfigData{SecondsPerSlot: 2}}, nil)
clientWithValidation := NewL1BeaconClient(p, L1BeaconClientConfig{})
p.EXPECT().BeaconBlobSideCars(ctx, false, uint64(1), hashes).Return(eth.APIGetBlobSidecarsResponse{Data: apiSidecars}, nil)
_, err := clientWithValidation.GetBlobs(ctx, eth.L1BlockRef{Time: 12}, hashes)
assert.NoError(t, err) // The verification flow does not require a valid proof
t.Run("fallback to BeaconBlobSideCars", func(t *testing.T) {
ctx := context.Background()
p := mocks.NewBeaconClient(t)
p.EXPECT().BeaconGenesis(ctx).Return(eth.APIGenesisResponse{Data: eth.ReducedGenesisData{GenesisTime: 10}}, nil)
p.EXPECT().ConfigSpec(ctx).Return(eth.APIConfigResponse{Data: eth.ReducedConfigData{SecondsPerSlot: 2}}, nil)
client := NewL1BeaconClient(p, L1BeaconClientConfig{})
ref := eth.L1BlockRef{Time: 12}
p.EXPECT().BeaconBlobs(ctx, uint64(1), hashes).Return(nil, errors.New("the sky is falling"))
p.EXPECT().BeaconBlobSideCars(ctx, false, uint64(1), hashes).Return(eth.APIGetBlobSidecarsResponse{Data: apiSidecars}, nil)
_, err := client.GetBlobs(ctx, ref, hashes)
assert.NoError(t, err)
})

t.Run("BeaconBlobs", func(t *testing.T) {
ctx := context.Background()
p := mocks.NewBeaconClient(t)
p.EXPECT().BeaconGenesis(ctx).Return(eth.APIGenesisResponse{Data: eth.ReducedGenesisData{GenesisTime: 10}}, nil)
p.EXPECT().ConfigSpec(ctx).Return(eth.APIConfigResponse{Data: eth.ReducedConfigData{SecondsPerSlot: 2}}, nil)
client := NewL1BeaconClient(p, L1BeaconClientConfig{})
ref := eth.L1BlockRef{Time: 12}
p.EXPECT().BeaconBlobs(ctx, uint64(1), hashes).Return(blobs, nil)
_, err := client.GetBlobs(ctx, ref, hashes)
assert.NoError(t, err)
})
}

func TestBeaconHTTPClient(t *testing.T) {
Expand Down
60 changes: 60 additions & 0 deletions op-service/sources/mocks/BeaconClient.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.