Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
67 changes: 58 additions & 9 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 @@ -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)
Comment thread
geoknee marked this conversation as resolved.
Outdated
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
Comment thread
geoknee marked this conversation as resolved.
// "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 thread
joshklop marked this conversation as resolved.
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 the commitments and indices.
Comment thread
geoknee marked this conversation as resolved.
Outdated
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)
clientWithValidation := NewL1BeaconClient(p, L1BeaconClientConfig{})
Comment thread
geoknee marked this conversation as resolved.
Outdated
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 := clientWithValidation.GetBlobs(ctx, ref, hashes)
assert.NoError(t, err) // The verification flow does not require a valid proof
Comment thread
geoknee marked this conversation as resolved.
Outdated
})

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)
clientWithValidation := NewL1BeaconClient(p, L1BeaconClientConfig{})
ref := eth.L1BlockRef{Time: 12}
p.EXPECT().BeaconBlobs(ctx, uint64(1), hashes).Return(blobs, nil)
_, err := clientWithValidation.GetBlobs(ctx, ref, hashes)
assert.NoError(t, err) // The verification flow does not require a valid proof
})
}

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.