diff --git a/op-node/config/beacon.go b/op-node/config/beacon.go index 2de46e217aaf0..679deba22d1d9 100644 --- a/op-node/config/beacon.go +++ b/op-node/config/beacon.go @@ -19,15 +19,17 @@ type L1BeaconEndpointSetup interface { // ShouldIgnoreBeaconCheck returns true if the Beacon-node version check should not halt startup. ShouldIgnoreBeaconCheck() bool ShouldFetchAllSidecars() bool + ShouldSkipBlobVerification() bool Check() error } type L1BeaconEndpointConfig struct { - BeaconAddr string // Address of L1 User Beacon-API endpoint to use (beacon namespace required) - BeaconHeader string // Optional HTTP header for all requests to L1 Beacon - BeaconFallbackAddrs []string // Addresses of L1 Beacon-API fallback endpoints (only for blob sidecars retrieval) - BeaconCheckIgnore bool // When false, halt startup if the beacon version endpoint fails - BeaconFetchAllSidecars bool // Whether to fetch all blob sidecars and filter locally + BeaconAddr string // Address of L1 User Beacon-API endpoint to use (beacon namespace required) + BeaconHeader string // Optional HTTP header for all requests to L1 Beacon + BeaconFallbackAddrs []string // Addresses of L1 Beacon-API fallback endpoints (only for blob sidecars retrieval) + BeaconCheckIgnore bool // When false, halt startup if the beacon version endpoint fails + BeaconFetchAllSidecars bool // Whether to fetch all blob sidecars and filter locally + BeaconSkipBlobVerification bool // Whether to skip the verification of the kzg_proof for each blob returned by the Beacon node } var _ L1BeaconEndpointSetup = (*L1BeaconEndpointConfig)(nil) @@ -66,6 +68,10 @@ func (cfg *L1BeaconEndpointConfig) ShouldFetchAllSidecars() bool { return cfg.BeaconFetchAllSidecars } +func (cfg *L1BeaconEndpointConfig) ShouldSkipBlobVerification() bool { + return cfg.BeaconSkipBlobVerification +} + func parseHTTPHeader(headerStr string) (http.Header, error) { h := make(http.Header, 1) s := strings.SplitN(headerStr, ": ", 2) diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index d98e959a2953e..d7ff28eb46816 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -107,6 +107,14 @@ var ( EnvVars: prefixEnvVars("L1_BEACON_FETCH_ALL_SIDECARS"), Category: L1RPCCategory, } + BeaconSkipBlobVerification = &cli.BoolFlag{ + Name: "l1.beacon.skip-blob-verification", + Usage: "If true, skips the verification of the kzg_proof for each blob returned by the Beacon node. Not recommended unless the provided beacon endpoints are trusted.", + Required: false, + Value: false, + EnvVars: prefixEnvVars("L1_BEACON_SKIP_BLOB_VERIFICATION"), + Category: L1RPCCategory, + } SyncModeFlag = &cli.GenericFlag{ Name: "syncmode", Usage: fmt.Sprintf("Blockchain sync mode (options: %s)", openum.EnumString(sync.ModeStrings)), @@ -434,6 +442,7 @@ var optionalFlags = []cli.Flag{ BeaconFallbackAddrs, BeaconCheckIgnore, BeaconFetchAllSidecars, + BeaconSkipBlobVerification, SyncModeFlag, FetchWithdrawalRootFromState, L1TrustRPC, diff --git a/op-node/node/node.go b/op-node/node/node.go index 2cf93f6800e99..960e822434ebf 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -362,7 +362,8 @@ func (n *OpNode) initL1BeaconAPI(ctx context.Context, cfg *config.Config) error return fmt.Errorf("failed to setup L1 Beacon API client: %w", err) } beaconCfg := sources.L1BeaconClientConfig{ - FetchAllSidecars: cfg.Beacon.ShouldFetchAllSidecars(), + FetchAllSidecars: cfg.Beacon.ShouldFetchAllSidecars(), + SkipBlobVerification: cfg.Beacon.ShouldSkipBlobVerification(), } n.beacon = sources.NewL1BeaconClient(beaconClient, beaconCfg, fallbacks...) diff --git a/op-node/service.go b/op-node/service.go index af72a6c4b78cb..875b366bee8f0 100644 --- a/op-node/service.go +++ b/op-node/service.go @@ -158,11 +158,12 @@ func NewSupervisorEndpointConfig(ctx *cli.Context) *interop.Config { func NewBeaconEndpointConfig(ctx *cli.Context) config.L1BeaconEndpointSetup { return &config.L1BeaconEndpointConfig{ - BeaconAddr: ctx.String(flags.BeaconAddr.Name), - BeaconHeader: ctx.String(flags.BeaconHeader.Name), - BeaconFallbackAddrs: ctx.StringSlice(flags.BeaconFallbackAddrs.Name), - BeaconCheckIgnore: ctx.Bool(flags.BeaconCheckIgnore.Name), - BeaconFetchAllSidecars: ctx.Bool(flags.BeaconFetchAllSidecars.Name), + BeaconAddr: ctx.String(flags.BeaconAddr.Name), + BeaconHeader: ctx.String(flags.BeaconHeader.Name), + BeaconFallbackAddrs: ctx.StringSlice(flags.BeaconFallbackAddrs.Name), + BeaconCheckIgnore: ctx.Bool(flags.BeaconCheckIgnore.Name), + BeaconFetchAllSidecars: ctx.Bool(flags.BeaconFetchAllSidecars.Name), + BeaconSkipBlobVerification: ctx.Bool(flags.BeaconSkipBlobVerification.Name), } } diff --git a/op-service/sources/l1_beacon_client.go b/op-service/sources/l1_beacon_client.go index 0999d279ddde7..f6586de8d40d4 100644 --- a/op-service/sources/l1_beacon_client.go +++ b/op-service/sources/l1_beacon_client.go @@ -28,7 +28,8 @@ const ( ) type L1BeaconClientConfig struct { - FetchAllSidecars bool + FetchAllSidecars bool + SkipBlobVerification bool } // L1BeaconClient is a high level golang client for the Beacon API. @@ -271,14 +272,14 @@ func (cl *L1BeaconClient) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hash if err != nil { return nil, fmt.Errorf("failed to get blob sidecars for L1BlockRef %s: %w", ref, err) } - blobs, err := blobsFromSidecars(blobSidecars, hashes) + blobs, err := blobsFromSidecars(blobSidecars, hashes, cl.cfg.SkipBlobVerification) if err != nil { return nil, fmt.Errorf("failed to get blobs from sidecars for L1BlockRef %s: %w", ref, err) } return blobs, nil } -func blobsFromSidecars(blobSidecars []*eth.BlobSidecar, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) { +func blobsFromSidecars(blobSidecars []*eth.BlobSidecar, hashes []eth.IndexedBlobHash, skipBlobVerification bool) ([]*eth.Blob, error) { if len(blobSidecars) != len(hashes) { return nil, fmt.Errorf("number of hashes and blobSidecars mismatch, %d != %d", len(hashes), len(blobSidecars)) } @@ -296,9 +297,11 @@ func blobsFromSidecars(blobSidecars []*eth.BlobSidecar, hashes []eth.IndexedBlob return nil, fmt.Errorf("expected hash %s for blob at index %d but got %s", ih.Hash, ih.Index, hash) } - // confirm blob data is valid by verifying its proof against the commitment - if err := eth.VerifyBlobProof(&sidecar.Blob, kzg4844.Commitment(sidecar.KZGCommitment), kzg4844.Proof(sidecar.KZGProof)); err != nil { - return nil, fmt.Errorf("blob at index %d failed verification: %w", i, err) + if !skipBlobVerification { + // confirm blob data is valid by verifying its proof against the commitment + if err := eth.VerifyBlobProof(&sidecar.Blob, kzg4844.Commitment(sidecar.KZGCommitment), kzg4844.Proof(sidecar.KZGProof)); err != nil { + return nil, fmt.Errorf("blob at index %d failed verification: %w", i, err) + } } out[i] = &sidecar.Blob } diff --git a/op-service/sources/l1_beacon_client_test.go b/op-service/sources/l1_beacon_client_test.go index 8e312f3c8eb27..2a6cfafba0a03 100644 --- a/op-service/sources/l1_beacon_client_test.go +++ b/op-service/sources/l1_beacon_client_test.go @@ -13,8 +13,10 @@ import ( "strconv" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto/kzg4844" client_mocks "github.com/ethereum-optimism/optimism/op-service/client/mocks" @@ -61,17 +63,17 @@ func TestBlobsFromSidecars(t *testing.T) { // put the sidecars in scrambled order to confirm error sidecars := []*eth.BlobSidecar{sidecar2, sidecar0, sidecar1} - _, err := blobsFromSidecars(sidecars, hashes) + _, err := blobsFromSidecars(sidecars, hashes, false) require.Error(t, err) // too few sidecars should error sidecars = []*eth.BlobSidecar{sidecar0, sidecar1} - _, err = blobsFromSidecars(sidecars, hashes) + _, err = blobsFromSidecars(sidecars, hashes, false) require.Error(t, err) // correct order should work sidecars = []*eth.BlobSidecar{sidecar0, sidecar1, sidecar2} - blobs, err := blobsFromSidecars(sidecars, hashes) + blobs, err := blobsFromSidecars(sidecars, hashes, false) require.NoError(t, err) // confirm order by checking first blob byte against expected index for i := range blobs { @@ -82,27 +84,65 @@ func TestBlobsFromSidecars(t *testing.T) { badProof := *sidecar0 badProof.KZGProof[11]++ sidecars[1] = &badProof - _, err = blobsFromSidecars(sidecars, hashes) + _, err = blobsFromSidecars(sidecars, hashes, false) require.Error(t, err) // mangle a commitment to make sure it's detected badCommitment := *sidecar0 badCommitment.KZGCommitment[13]++ sidecars[1] = &badCommitment - _, err = blobsFromSidecars(sidecars, hashes) + _, err = blobsFromSidecars(sidecars, hashes, false) require.Error(t, err) // mangle a hash to make sure it's detected sidecars[1] = sidecar0 hashes[2].Hash[17]++ - _, err = blobsFromSidecars(sidecars, hashes) + _, err = blobsFromSidecars(sidecars, hashes, false) require.Error(t, err) + +} + +func KZGProofFromHex(s string) (kzg4844.Proof, error) { + var out kzg4844.Proof // underlying size is 48 bytes + b, err := hexutil.Decode(s) + if err != nil { + return out, err + } + if len(b) != 48 { + return out, fmt.Errorf("want 48 bytes, got %d", len(b)) + } + copy(out[:], b) + return out, nil +} + +var badProof, _ = KZGProofFromHex("0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + +func TestBlobsFromSidecars_SkipBlobVerification(t *testing.T) { + indices := []uint64{5, 7, 2} + index0, sidecar0 := makeTestBlobSidecar(indices[0]) + index1, sidecar1 := makeTestBlobSidecar(indices[1]) + index2, sidecar2 := makeTestBlobSidecar(indices[2]) + hashes := []eth.IndexedBlobHash{index0, index1, index2} + + sidecars := []*eth.BlobSidecar{sidecar0, sidecar1, sidecar2} + + // Set proof to a bad / stubbed value + sidecars[1].KZGProof = eth.Bytes48(badProof) + + // Check that verification succeeds when skipBlobVerification is true + _, err := blobsFromSidecars(sidecars, hashes, true) + require.NoError(t, err) + + // Check that verification fails when skipBlobVerification is false + _, err = blobsFromSidecars(sidecars, hashes, false) + require.Error(t, err) + } func TestBlobsFromSidecars_EmptySidecarList(t *testing.T) { hashes := []eth.IndexedBlobHash{} sidecars := []*eth.BlobSidecar{} - blobs, err := blobsFromSidecars(sidecars, hashes) + blobs, err := blobsFromSidecars(sidecars, hashes, false) require.NoError(t, err) require.Empty(t, blobs, "blobs should be empty when no sidecars are provided") } @@ -186,7 +226,37 @@ func TestBeaconClientFallback(t *testing.T) { resp, err = c.GetBlobSidecars(ctx, eth.L1BlockRef{Time: 14}, hashes) require.Equal(t, sidecars, resp) require.NoError(t, err) +} + +func TestBeaconClientSkipBlobVerification(t *testing.T) { + indices := []uint64{5, 7, 2} + index0, sidecar0 := makeTestBlobSidecar(indices[0]) + index1, sidecar1 := makeTestBlobSidecar(indices[1]) + index2, sidecar2 := makeTestBlobSidecar(indices[2]) + + hashes := []eth.IndexedBlobHash{index0, index1, index2} + sidecars := []*eth.BlobSidecar{sidecar0, sidecar1, sidecar2} + + // 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{SkipBlobVerification: false}) + p.EXPECT().BeaconBlobSideCars(ctx, false, uint64(1), hashes).Return(eth.APIGetBlobSidecarsResponse{Data: apiSidecars}, nil) + _, err := clientWithValidation.GetBlobs(ctx, eth.L1BlockRef{Time: 12}, hashes) + assert.Error(t, err) + + 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) + clientWithoutValidation := NewL1BeaconClient(p, L1BeaconClientConfig{SkipBlobVerification: true}) + p.EXPECT().BeaconBlobSideCars(ctx, false, uint64(1), hashes).Return(eth.APIGetBlobSidecarsResponse{Data: apiSidecars}, nil) + _, err = clientWithoutValidation.GetBlobs(ctx, eth.L1BlockRef{Time: 12}, hashes) + assert.NoError(t, err) } func TestBeaconHTTPClient(t *testing.T) {