diff --git a/beacon-chain/core/peerdas/helpers.go b/beacon-chain/core/peerdas/helpers.go index 9681b365ad08..2792c4b1a9b2 100644 --- a/beacon-chain/core/peerdas/helpers.go +++ b/beacon-chain/core/peerdas/helpers.go @@ -255,7 +255,7 @@ func DataColumnSidecarsForReconstruct( // VerifyDataColumnSidecarKZGProofs verifies the provided KZG Proofs for the particular // data column. -func VerifyDataColumnSidecarKZGProofs(sc *ethpb.DataColumnSidecar) (bool, error) { +func VerifyDataColumnSidecarKZGProofs(sc blocks.RODataColumn) (bool, error) { if sc.ColumnIndex >= params.BeaconConfig().NumberOfColumns { return false, errIndexTooLarge } diff --git a/beacon-chain/core/peerdas/helpers_test.go b/beacon-chain/core/peerdas/helpers_test.go index 83a9ca3371e4..040bcaf3d061 100644 --- a/beacon-chain/core/peerdas/helpers_test.go +++ b/beacon-chain/core/peerdas/helpers_test.go @@ -85,7 +85,9 @@ func TestVerifyDataColumnSidecarKZGProofs(t *testing.T) { require.NoError(t, err) for i, sidecar := range sCars { - verified, err := peerdas.VerifyDataColumnSidecarKZGProofs(sidecar) + roCol, err := blocks.NewRODataColumn(sidecar) + require.NoError(t, err) + verified, err := peerdas.VerifyDataColumnSidecarKZGProofs(roCol) require.NoError(t, err) require.Equal(t, true, verified, fmt.Sprintf("sidecar %d failed", i)) } diff --git a/beacon-chain/sync/backfill/blobs.go b/beacon-chain/sync/backfill/blobs.go index 1f7da626844b..62a6a335af91 100644 --- a/beacon-chain/sync/backfill/blobs.go +++ b/beacon-chain/sync/backfill/blobs.go @@ -107,7 +107,7 @@ type blobBatchVerifier struct { func (bbv *blobBatchVerifier) newVerifier(rb blocks.ROBlob) verification.BlobVerifier { m := bbv.verifiers[rb.BlockRoot()] - m[rb.Index] = bbv.newBlobVerifier(rb, verification.BackfillSidecarRequirements) + m[rb.Index] = bbv.newBlobVerifier(rb, verification.BackfillBlobSidecarRequirements) bbv.verifiers[rb.BlockRoot()] = m return m[rb.Index] } diff --git a/beacon-chain/sync/data_columns_sampling.go b/beacon-chain/sync/data_columns_sampling.go index 1bb0aaf58e6e..dada08922d64 100644 --- a/beacon-chain/sync/data_columns_sampling.go +++ b/beacon-chain/sync/data_columns_sampling.go @@ -522,7 +522,7 @@ func verifyColumn( } // Filter out columns which did not pass the KZG inclusion proof verification. - if err := blocks.VerifyKZGInclusionProofColumn(roDataColumn.DataColumnSidecar); err != nil { + if err := blocks.VerifyKZGInclusionProofColumn(roDataColumn); err != nil { log.WithFields(logrus.Fields{ "peerID": pid, "root": fmt.Sprintf("%#x", root), @@ -533,7 +533,7 @@ func verifyColumn( } // Filter out columns which did not pass the KZG proof verification. - verified, err := peerdas.VerifyDataColumnSidecarKZGProofs(roDataColumn.DataColumnSidecar) + verified, err := peerdas.VerifyDataColumnSidecarKZGProofs(roDataColumn) if err != nil { log.WithFields(logrus.Fields{ "peerID": pid, diff --git a/beacon-chain/sync/initial-sync/round_robin.go b/beacon-chain/sync/initial-sync/round_robin.go index 496d2ee8b8ea..ce7c7138ae68 100644 --- a/beacon-chain/sync/initial-sync/round_robin.go +++ b/beacon-chain/sync/initial-sync/round_robin.go @@ -197,7 +197,7 @@ func (s *Service) processFetchedDataRegSync( } } } else { - bv := verification.NewBlobBatchVerifier(s.newBlobVerifier, verification.InitsyncSidecarRequirements) + bv := verification.NewBlobBatchVerifier(s.newBlobVerifier, verification.InitsyncBlobSidecarRequirements) avs := das.NewLazilyPersistentStore(s.cfg.BlobStorage, bv) batchFields := logrus.Fields{ "firstSlot": data.bwb[0].Block.Block().Slot(), @@ -370,7 +370,7 @@ func (s *Service) processBatchedBlocks(ctx context.Context, genesis time.Time, } aStore = avs } else { - bv := verification.NewBlobBatchVerifier(s.newBlobVerifier, verification.InitsyncSidecarRequirements) + bv := verification.NewBlobBatchVerifier(s.newBlobVerifier, verification.InitsyncBlobSidecarRequirements) avs := das.NewLazilyPersistentStore(s.cfg.BlobStorage, bv) s.logBatchSyncStatus(genesis, first, len(bwb)) for _, bb := range bwb { diff --git a/beacon-chain/sync/initial-sync/service.go b/beacon-chain/sync/initial-sync/service.go index e58211b801b5..e08039a5425f 100644 --- a/beacon-chain/sync/initial-sync/service.go +++ b/beacon-chain/sync/initial-sync/service.go @@ -400,7 +400,7 @@ func (s *Service) fetchOriginBlobs(pids []peer.ID) error { if len(sidecars) != len(req) { continue } - bv := verification.NewBlobBatchVerifier(s.newBlobVerifier, verification.InitsyncSidecarRequirements) + bv := verification.NewBlobBatchVerifier(s.newBlobVerifier, verification.InitsyncBlobSidecarRequirements) avs := das.NewLazilyPersistentStore(s.cfg.BlobStorage, bv) current := s.clock.CurrentSlot() if err := avs.Persist(current, sidecars...); err != nil { diff --git a/beacon-chain/sync/rpc_beacon_blocks_by_root.go b/beacon-chain/sync/rpc_beacon_blocks_by_root.go index 8dda5333a7ce..ee6c4edbe51d 100644 --- a/beacon-chain/sync/rpc_beacon_blocks_by_root.go +++ b/beacon-chain/sync/rpc_beacon_blocks_by_root.go @@ -167,7 +167,7 @@ func (s *Service) sendAndSaveBlobSidecars(ctx context.Context, request types.Blo if len(sidecars) != len(request) { return fmt.Errorf("received %d blob sidecars, expected %d for RPC", len(sidecars), len(request)) } - bv := verification.NewBlobBatchVerifier(s.newBlobVerifier, verification.PendingQueueSidecarRequirements) + bv := verification.NewBlobBatchVerifier(s.newBlobVerifier, verification.PendingQueueBlobSidecarRequirements) for _, sidecar := range sidecars { if err := verify.BlobAlignsWithBlock(sidecar, RoBlock); err != nil { return err diff --git a/beacon-chain/sync/service.go b/beacon-chain/sync/service.go index 38a608621808..266aeaaf7eb3 100644 --- a/beacon-chain/sync/service.go +++ b/beacon-chain/sync/service.go @@ -164,7 +164,7 @@ type Service struct { initialSyncComplete chan struct{} verifierWaiter *verification.InitializerWaiter newBlobVerifier verification.NewBlobVerifier - newColumnProposerVerifier verification.NewColumnVerifier + newColumnVerifier verification.NewColumnVerifier availableBlocker coverage.AvailableBlocker dataColumsnReconstructionLock sync.Mutex receivedDataColumnsFromRoot map[[fieldparams.RootLength]byte]map[uint64]bool @@ -228,6 +228,12 @@ func newBlobVerifierFromInitializer(ini *verification.Initializer) verification. } } +func newColumnVerifierFromInitializer(ini *verification.Initializer) verification.NewColumnVerifier { + return func(d blocks.RODataColumn, reqs []verification.Requirement) verification.DataColumnVerifier { + return ini.NewColumnVerifier(d, reqs) + } +} + // Start the regular sync service. func (s *Service) Start() { v, err := s.verifierWaiter.WaitForInitializer(s.ctx) @@ -236,7 +242,7 @@ func (s *Service) Start() { return } s.newBlobVerifier = newBlobVerifierFromInitializer(v) - s.newColumnProposerVerifier = v.VerifyProposer + s.newColumnVerifier = newColumnVerifierFromInitializer(v) go s.verifierRoutine() go s.startTasksPostInitialSync() diff --git a/beacon-chain/sync/validate_blob.go b/beacon-chain/sync/validate_blob.go index ff774396cadb..fe9f0f686e97 100644 --- a/beacon-chain/sync/validate_blob.go +++ b/beacon-chain/sync/validate_blob.go @@ -51,7 +51,7 @@ func (s *Service) validateBlob(ctx context.Context, pid peer.ID, msg *pubsub.Mes if err != nil { return pubsub.ValidationReject, errors.Wrap(err, "roblob conversion failure") } - vf := s.newBlobVerifier(blob, verification.GossipSidecarRequirements) + vf := s.newBlobVerifier(blob, verification.GossipBlobSidecarRequirements) if err := vf.BlobIndexInBounds(); err != nil { return pubsub.ValidationReject, err diff --git a/beacon-chain/sync/validate_data_column.go b/beacon-chain/sync/validate_data_column.go index 334025854b54..fefbc2bf89d8 100644 --- a/beacon-chain/sync/validate_data_column.go +++ b/beacon-chain/sync/validate_data_column.go @@ -8,17 +8,15 @@ import ( pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/peer" "github.com/pkg/errors" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain" - coreBlocks "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/blocks" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/peerdas" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/verification" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/crypto/rand" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" prysmTime "github.com/prysmaticlabs/prysm/v5/time" "github.com/prysmaticlabs/prysm/v5/time/slots" - "github.com/sirupsen/logrus" ) // https://github.com/ethereum/consensus-specs/blob/dev/specs/_features/eip7594/p2p-interface.md#the-gossip-domain-gossipsub @@ -48,15 +46,19 @@ func (s *Service) validateDataColumn(ctx context.Context, pid peer.ID, msg *pubs } // Ignore messages that are not of the expected type. - ds, ok := m.(*eth.DataColumnSidecar) + dspb, ok := m.(*eth.DataColumnSidecar) if !ok { log.WithField("message", m).Error("Message is not of type *eth.DataColumnSidecar") return pubsub.ValidationReject, errWrongMessage } + ds, err := blocks.NewRODataColumn(dspb) + if err != nil { + return pubsub.ValidationReject, errors.Wrap(err, "roDataColumn conversion failure") + } + vf := s.newColumnVerifier(ds, verification.GossipColumnSidecarRequirements) - // [REJECT] The sidecar's index is consistent with NUMBER_OF_COLUMNS -- i.e. sidecar.index < NUMBER_OF_COLUMNS. - if ds.ColumnIndex >= params.BeaconConfig().NumberOfColumns { - return pubsub.ValidationReject, errors.Errorf("invalid column index provided, got %d", ds.ColumnIndex) + if err := vf.DataColumnIndexInBounds(); err != nil { + return pubsub.ValidationReject, err } // [REJECT] The sidecar is for the correct subnet -- i.e. compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id. @@ -66,115 +68,84 @@ func (s *Service) validateDataColumn(ctx context.Context, pid peer.ID, msg *pubs return pubsub.ValidationReject, fmt.Errorf("wrong topic name: %s", *msg.Topic) } - // [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. validate that block_header.slot <= current_slot (a client MAY queue future sidecars for processing at the appropriate slot). - if err := slots.VerifyTime(uint64(s.cfg.clock.GenesisTime().Unix()), ds.SignedBlockHeader.Header.Slot, params.BeaconConfig().MaximumGossipClockDisparityDuration()); err != nil { - log.WithError(err).Debug("Ignored sidecar: could not verify slot time") - return pubsub.ValidationIgnore, nil + if err := vf.NotFromFutureSlot(); err != nil { + return pubsub.ValidationIgnore, err } - // [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that block_header.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) - cp := s.cfg.chain.FinalizedCheckpt() - startSlot, err := slots.EpochStart(cp.Epoch) - if err != nil { - log.WithError(err).Debug("Ignored column sidecar: could not calculate epoch start slot") + // [IGNORE] The sidecar is the first sidecar for the tuple (block_header.slot, block_header.proposer_index, sidecar.index) with valid header signature, sidecar inclusion proof, and kzg proof. + if s.hasSeenDataColumnIndex(ds.Slot(), ds.ProposerIndex(), ds.DataColumnSidecar.ColumnIndex) { return pubsub.ValidationIgnore, nil } - if startSlot >= ds.SignedBlockHeader.Header.Slot { - err := fmt.Errorf("finalized slot %d greater or equal to block slot %d", startSlot, ds.SignedBlockHeader.Header.Slot) - log.Debug(err) + if err := vf.SlotAboveFinalized(); err != nil { return pubsub.ValidationIgnore, err } - - // [IGNORE] The sidecar's block's parent (defined by block_header.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue sidecars for processing once the parent block is retrieved). - if !s.cfg.chain.HasBlock(ctx, [32]byte(ds.SignedBlockHeader.Header.ParentRoot)) { - err := errors.Errorf("unknown parent for data column sidecar with slot %d and parent root %#x", ds.SignedBlockHeader.Header.Slot, ds.SignedBlockHeader.Header.ParentRoot) - log.WithError(err).Debug("Could not identify parent for data column sidecar") + if err := vf.SidecarParentSeen(s.hasBadBlock); err != nil { + go func() { + if err := s.sendBatchRootRequest(context.Background(), [][32]byte{ds.ParentRoot()}, rand.NewGenerator()); err != nil { + log.WithError(err).WithFields(columnFields(ds)).Debug("Failed to send batch root request") + } + }() return pubsub.ValidationIgnore, err } - - // [REJECT] The sidecar's block's parent (defined by block_header.parent_root) passes validation. - if s.hasBadBlock([32]byte(ds.SignedBlockHeader.Header.ParentRoot)) { - bRoot, err := ds.SignedBlockHeader.Header.HashTreeRoot() - if err != nil { - return pubsub.ValidationIgnore, err - } - - // If parent is bad, we set the block as bad. - s.setBadBlock(ctx, bRoot) - return pubsub.ValidationReject, errors.Errorf("column sidecar with bad parent provided") + if err := vf.SidecarParentValid(s.hasBadBlock); err != nil { + return pubsub.ValidationReject, err } - // [REJECT] The sidecar is from a higher slot than the sidecar's block's parent (defined by block_header.parent_root). - parentSlot, err := s.cfg.chain.RecentBlockSlot([32]byte(ds.SignedBlockHeader.Header.ParentRoot)) - if err != nil { - return pubsub.ValidationIgnore, err + if err := vf.SidecarParentSlotLower(); err != nil { + return pubsub.ValidationReject, err } - if ds.SignedBlockHeader.Header.Slot <= parentSlot { - return pubsub.ValidationReject, errors.Errorf("invalid column sidecar slot: %d", ds.SignedBlockHeader.Header.Slot) + if err := vf.SidecarDescendsFromFinalized(); err != nil { + return pubsub.ValidationReject, err } - // [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block -- i.e. get_checkpoint_block(store, block_header.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root. - if !s.cfg.chain.InForkchoice([32]byte(ds.SignedBlockHeader.Header.ParentRoot)) { - return pubsub.ValidationReject, blockchain.ErrNotDescendantOfFinalized + if err := vf.SidecarInclusionProven(); err != nil { + return pubsub.ValidationReject, err } - // [REJECT] The sidecar's kzg_commitments field inclusion proof is valid as verified by verify_data_column_sidecar_inclusion_proof(sidecar). - if err := blocks.VerifyKZGInclusionProofColumn(ds); err != nil { + if err := vf.SidecarKzgProofVerified(); err != nil { return pubsub.ValidationReject, err } - - // [REJECT] The sidecar's column data is valid as verified by verify_data_column_sidecar_kzg_proofs(sidecar). - verified, err := peerdas.VerifyDataColumnSidecarKZGProofs(ds) - if err != nil { + if err := vf.ValidProposerSignature(ctx); err != nil { return pubsub.ValidationReject, err } - - if !verified { - return pubsub.ValidationReject, errors.New("failed to verify kzg proof of column") + if err := vf.SidecarProposerExpected(ctx); err != nil { + return pubsub.ValidationReject, err } - // [REJECT] The proposer signature of sidecar.signed_block_header, is valid with respect to the block_header.proposer_index pubkey. - parentState, err := s.cfg.stateGen.StateByRoot(ctx, [32]byte(ds.SignedBlockHeader.Header.ParentRoot)) + // Get the time at slot start. + startTime, err := slots.ToTime(uint64(s.cfg.chain.GenesisTime().Unix()), ds.SignedBlockHeader.Header.Slot) if err != nil { return pubsub.ValidationIgnore, err } - if err := coreBlocks.VerifyBlockHeaderSignatureUsingCurrentFork(parentState, ds.SignedBlockHeader); err != nil { - return pubsub.ValidationReject, err - } - roDataColumn, err := blocks.NewRODataColumn(ds) - if err != nil { - return pubsub.ValidationReject, errors.Wrap(err, "new RO data columns") - } - - if err := s.newColumnProposerVerifier(ctx, roDataColumn); err != nil { - return pubsub.ValidationReject, errors.Wrap(err, "could not verify proposer") - } - - // Get the time at slot start. - startTime, err := slots.ToTime(uint64(s.cfg.chain.GenesisTime().Unix()), ds.SignedBlockHeader.Header.Slot) + fields := columnFields(ds) + sinceSlotStartTime := receivedTime.Sub(startTime) + validationTime := s.cfg.clock.Now().Sub(receivedTime) + fields["sinceSlotStartTime"] = sinceSlotStartTime + fields["validationTime"] = validationTime + log.WithFields(fields).Debug("Received data column sidecar gossip") - // Add specific debug log. - if err == nil { - log.WithFields(logrus.Fields{ - "sinceSlotStartTime": receivedTime.Sub(startTime), - "validationTime": s.cfg.clock.Now().Sub(receivedTime), - "columnIndex": ds.ColumnIndex, - }).Debug("Received data column sidecar") - } else { - log.WithError(err).Error("Failed to calculate slot time") + verifiedRODataColumn, err := vf.VerifiedRODataColumn() + if err != nil { + return pubsub.ValidationReject, err } - // TODO: Transform this whole function so it looks like to the `validateBlob` - // with the tiny verifiers inside. - verifiedRODataColumn := blocks.NewVerifiedRODataColumn(roDataColumn) - msg.ValidatorData = verifiedRODataColumn return pubsub.ValidationAccept, nil } +// Returns true if the column with the same slot, proposer index, and column index has been seen before. +func (s *Service) hasSeenDataColumnIndex(slot primitives.Slot, proposerIndex primitives.ValidatorIndex, index uint64) bool { + s.seenDataColumnLock.RLock() + defer s.seenDataColumnLock.RUnlock() + b := append(bytesutil.Bytes32(uint64(slot)), bytesutil.Bytes32(uint64(proposerIndex))...) + b = append(b, bytesutil.Bytes32(index)...) + _, seen := s.seenDataColumnCache.Get(string(b)) + return seen +} + // Sets the data column with the same slot, proposer index, and data column index as seen. func (s *Service) setSeenDataColumnIndex(slot primitives.Slot, proposerIndex primitives.ValidatorIndex, index uint64) { s.seenDataColumnLock.Lock() diff --git a/beacon-chain/sync/verify/blob.go b/beacon-chain/sync/verify/blob.go index b08b7096e241..af4af9c59ff3 100644 --- a/beacon-chain/sync/verify/blob.go +++ b/beacon-chain/sync/verify/blob.go @@ -76,12 +76,12 @@ func ColumnAlignsWithBlock(col blocks.RODataColumn, block blocks.ROBlock) error } // Filter out columns which did not pass the KZG inclusion proof verification. - if err := blocks.VerifyKZGInclusionProofColumn(col.DataColumnSidecar); err != nil { + if err := blocks.VerifyKZGInclusionProofColumn(col); err != nil { return err } // Filter out columns which did not pass the KZG proof verification. - verified, err := peerdas.VerifyDataColumnSidecarKZGProofs(col.DataColumnSidecar) + verified, err := peerdas.VerifyDataColumnSidecarKZGProofs(col) if err != nil { return err } diff --git a/beacon-chain/verification/BUILD.bazel b/beacon-chain/verification/BUILD.bazel index fa95e5451e65..a9e52fdbfdf8 100644 --- a/beacon-chain/verification/BUILD.bazel +++ b/beacon-chain/verification/BUILD.bazel @@ -6,6 +6,7 @@ go_library( "batch.go", "blob.go", "cache.go", + "data_column.go", "error.go", "fake.go", "initializer.go", @@ -19,6 +20,7 @@ go_library( deps = [ "//beacon-chain/blockchain/kzg:go_default_library", "//beacon-chain/core/helpers:go_default_library", + "//beacon-chain/core/peerdas:go_default_library", "//beacon-chain/core/signing:go_default_library", "//beacon-chain/core/transition:go_default_library", "//beacon-chain/forkchoice/types:go_default_library", @@ -49,11 +51,14 @@ go_test( "batch_test.go", "blob_test.go", "cache_test.go", + "data_column_test.go", "initializer_test.go", "result_test.go", + "verification_test.go", ], embed = [":go_default_library"], deps = [ + "//beacon-chain/blockchain/kzg:go_default_library", "//beacon-chain/core/signing:go_default_library", "//beacon-chain/db:go_default_library", "//beacon-chain/forkchoice/types:go_default_library", diff --git a/beacon-chain/verification/batch_test.go b/beacon-chain/verification/batch_test.go index f0e987d79739..658a86b369c4 100644 --- a/beacon-chain/verification/batch_test.go +++ b/beacon-chain/verification/batch_test.go @@ -169,7 +169,7 @@ func TestBatchVerifier(t *testing.T) { blk, blbs := c.bandb(t, c.nblobs) reqs := c.reqs if reqs == nil { - reqs = InitsyncSidecarRequirements + reqs = InitsyncBlobSidecarRequirements } bbv := NewBlobBatchVerifier(c.nv(), reqs) if c.cv == nil { diff --git a/beacon-chain/verification/blob.go b/beacon-chain/verification/blob.go index 916ddff3bc31..bf60da279c1b 100644 --- a/beacon-chain/verification/blob.go +++ b/beacon-chain/verification/blob.go @@ -2,6 +2,7 @@ package verification import ( "context" + goError "errors" "github.com/pkg/errors" forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" @@ -17,6 +18,7 @@ import ( const ( RequireBlobIndexInBounds Requirement = iota + RequireDataColumnIndexInBounds RequireNotFromFutureSlot RequireSlotAboveFinalized RequireValidProposerSignature @@ -29,7 +31,7 @@ const ( RequireSidecarProposerExpected ) -var allSidecarRequirements = []Requirement{ +var allBlobSidecarRequirements = []Requirement{ RequireBlobIndexInBounds, RequireNotFromFutureSlot, RequireSlotAboveFinalized, @@ -43,21 +45,21 @@ var allSidecarRequirements = []Requirement{ RequireSidecarProposerExpected, } -// GossipSidecarRequirements defines the set of requirements that BlobSidecars received on gossip +// GossipBlobSidecarRequirements defines the set of requirements that BlobSidecars received on gossip // must satisfy in order to upgrade an ROBlob to a VerifiedROBlob. -var GossipSidecarRequirements = requirementList(allSidecarRequirements).excluding() +var GossipBlobSidecarRequirements = requirementList(allBlobSidecarRequirements).excluding() -// SpectestSidecarRequirements is used by the forkchoice spectests when verifying blobs used in the on_block tests. +// SpectestBlobSidecarRequirements is used by the forkchoice spectests when verifying blobs used in the on_block tests. // The only requirements we exclude for these tests are the parent validity and seen tests, as these are specific to // gossip processing and require the bad block cache that we only use there. -var SpectestSidecarRequirements = requirementList(GossipSidecarRequirements).excluding( +var SpectestBlobSidecarRequirements = requirementList(GossipBlobSidecarRequirements).excluding( RequireSidecarParentSeen, RequireSidecarParentValid) -// InitsyncSidecarRequirements is the list of verification requirements to be used by the init-sync service +// InitsyncBlobSidecarRequirements is the list of verification requirements to be used by the init-sync service // for batch-mode syncing. Because we only perform batch verification as part of the IsDataAvailable method // for blobs after the block has been verified, and the blobs to be verified are keyed in the cache by the // block root, the list of required verifications is much shorter than gossip. -var InitsyncSidecarRequirements = requirementList(GossipSidecarRequirements).excluding( +var InitsyncBlobSidecarRequirements = requirementList(GossipBlobSidecarRequirements).excluding( RequireNotFromFutureSlot, RequireSlotAboveFinalized, RequireSidecarParentSeen, @@ -67,36 +69,16 @@ var InitsyncSidecarRequirements = requirementList(GossipSidecarRequirements).exc RequireSidecarProposerExpected, ) -// BackfillSidecarRequirements is the same as InitsyncSidecarRequirements. -var BackfillSidecarRequirements = requirementList(InitsyncSidecarRequirements).excluding() +// BackfillBlobSidecarRequirements is the same as InitsyncBlobSidecarRequirements. +var BackfillBlobSidecarRequirements = requirementList(InitsyncBlobSidecarRequirements).excluding() -// PendingQueueSidecarRequirements is the same as InitsyncSidecarRequirements, used by the pending blocks queue. -var PendingQueueSidecarRequirements = requirementList(InitsyncSidecarRequirements).excluding() +// PendingQueueBlobSidecarRequirements is the same as InitsyncBlobSidecarRequirements, used by the pending blocks queue. +var PendingQueueBlobSidecarRequirements = requirementList(InitsyncBlobSidecarRequirements).excluding() var ( ErrBlobInvalid = errors.New("blob failed verification") // ErrBlobIndexInvalid means RequireBlobIndexInBounds failed. - ErrBlobIndexInvalid = errors.Wrap(ErrBlobInvalid, "incorrect blob sidecar index") - // ErrFromFutureSlot means RequireSlotNotTooEarly failed. - ErrFromFutureSlot = errors.Wrap(ErrBlobInvalid, "slot is too far in the future") - // ErrSlotNotAfterFinalized means RequireSlotAboveFinalized failed. - ErrSlotNotAfterFinalized = errors.Wrap(ErrBlobInvalid, "slot <= finalized checkpoint") - // ErrInvalidProposerSignature means RequireValidProposerSignature failed. - ErrInvalidProposerSignature = errors.Wrap(ErrBlobInvalid, "proposer signature could not be verified") - // ErrSidecarParentNotSeen means RequireSidecarParentSeen failed. - ErrSidecarParentNotSeen = errors.Wrap(ErrBlobInvalid, "parent root has not been seen") - // ErrSidecarParentInvalid means RequireSidecarParentValid failed. - ErrSidecarParentInvalid = errors.Wrap(ErrBlobInvalid, "parent block is not valid") - // ErrSlotNotAfterParent means RequireSidecarParentSlotLower failed. - ErrSlotNotAfterParent = errors.Wrap(ErrBlobInvalid, "slot <= slot") - // ErrSidecarNotFinalizedDescendent means RequireSidecarDescendsFromFinalized failed. - ErrSidecarNotFinalizedDescendent = errors.Wrap(ErrBlobInvalid, "blob parent is not descended from the finalized block") - // ErrSidecarInclusionProofInvalid means RequireSidecarInclusionProven failed. - ErrSidecarInclusionProofInvalid = errors.Wrap(ErrBlobInvalid, "sidecar inclusion proof verification failed") - // ErrSidecarKzgProofInvalid means RequireSidecarKzgProofVerified failed. - ErrSidecarKzgProofInvalid = errors.Wrap(ErrBlobInvalid, "sidecar kzg commitment proof verification failed") - // ErrSidecarUnexpectedProposer means RequireSidecarProposerExpected failed. - ErrSidecarUnexpectedProposer = errors.Wrap(ErrBlobInvalid, "sidecar was not proposed by the expected proposer_index") + ErrBlobIndexInvalid = errors.New("incorrect blob sidecar index") ) type ROBlobVerifier struct { @@ -145,7 +127,7 @@ func (bv *ROBlobVerifier) BlobIndexInBounds() (err error) { defer bv.recordResult(RequireBlobIndexInBounds, &err) if bv.blob.Index >= fieldparams.MaxBlobsPerBlock { log.WithFields(logging.BlobFields(bv.blob)).Debug("Sidecar index >= MAX_BLOBS_PER_BLOCK") - return ErrBlobIndexInvalid + return blobErrBuilder(ErrBlobIndexInvalid) } return nil } @@ -164,7 +146,7 @@ func (bv *ROBlobVerifier) NotFromFutureSlot() (err error) { // If the system time is still before earliestStart, we consider the blob from a future slot and return an error. if bv.clock.Now().Before(earliestStart) { log.WithFields(logging.BlobFields(bv.blob)).Debug("sidecar slot is too far in the future") - return ErrFromFutureSlot + return blobErrBuilder(ErrFromFutureSlot) } return nil } @@ -177,11 +159,11 @@ func (bv *ROBlobVerifier) SlotAboveFinalized() (err error) { fcp := bv.fc.FinalizedCheckpoint() fSlot, err := slots.EpochStart(fcp.Epoch) if err != nil { - return errors.Wrapf(ErrSlotNotAfterFinalized, "error computing epoch start slot for finalized checkpoint (%d) %s", fcp.Epoch, err.Error()) + return errors.Wrapf(blobErrBuilder(ErrSlotNotAfterFinalized), "error computing epoch start slot for finalized checkpoint (%d) %s", fcp.Epoch, err.Error()) } if bv.blob.Slot() <= fSlot { log.WithFields(logging.BlobFields(bv.blob)).Debug("sidecar slot is not after finalized checkpoint") - return ErrSlotNotAfterFinalized + return blobErrBuilder(ErrSlotNotAfterFinalized) } return nil } @@ -199,7 +181,7 @@ func (bv *ROBlobVerifier) ValidProposerSignature(ctx context.Context) (err error if err != nil { log.WithFields(logging.BlobFields(bv.blob)).WithError(err).Debug("reusing failed proposer signature validation from cache") blobVerificationProposerSignatureCache.WithLabelValues("hit-invalid").Inc() - return ErrInvalidProposerSignature + return blobErrBuilder(ErrInvalidProposerSignature) } return nil } @@ -209,12 +191,12 @@ func (bv *ROBlobVerifier) ValidProposerSignature(ctx context.Context) (err error parent, err := bv.parentState(ctx) if err != nil { log.WithFields(logging.BlobFields(bv.blob)).WithError(err).Debug("could not replay parent state for blob signature verification") - return ErrInvalidProposerSignature + return blobErrBuilder(ErrInvalidProposerSignature) } // Full verification, which will subsequently be cached for anything sharing the signature cache. if err = bv.sc.VerifySignature(sd, parent); err != nil { log.WithFields(logging.BlobFields(bv.blob)).WithError(err).Debug("signature verification failed") - return ErrInvalidProposerSignature + return blobErrBuilder(ErrInvalidProposerSignature) } return nil } @@ -231,7 +213,7 @@ func (bv *ROBlobVerifier) SidecarParentSeen(parentSeen func([32]byte) bool) (err return nil } log.WithFields(logging.BlobFields(bv.blob)).Debug("parent root has not been seen") - return ErrSidecarParentNotSeen + return blobErrBuilder(ErrSidecarParentNotSeen) } // SidecarParentValid represents the spec verification: @@ -240,7 +222,7 @@ func (bv *ROBlobVerifier) SidecarParentValid(badParent func([32]byte) bool) (err defer bv.recordResult(RequireSidecarParentValid, &err) if badParent != nil && badParent(bv.blob.ParentRoot()) { log.WithFields(logging.BlobFields(bv.blob)).Debug("parent root is invalid") - return ErrSidecarParentInvalid + return blobErrBuilder(ErrSidecarParentInvalid) } return nil } @@ -251,10 +233,10 @@ func (bv *ROBlobVerifier) SidecarParentSlotLower() (err error) { defer bv.recordResult(RequireSidecarParentSlotLower, &err) parentSlot, err := bv.fc.Slot(bv.blob.ParentRoot()) if err != nil { - return errors.Wrap(ErrSlotNotAfterParent, "parent root not in forkchoice") + return errors.Wrap(blobErrBuilder(ErrSlotNotAfterParent), "parent root not in forkchoice") } if parentSlot >= bv.blob.Slot() { - return ErrSlotNotAfterParent + return blobErrBuilder(ErrSlotNotAfterParent) } return nil } @@ -266,7 +248,7 @@ func (bv *ROBlobVerifier) SidecarDescendsFromFinalized() (err error) { defer bv.recordResult(RequireSidecarDescendsFromFinalized, &err) if !bv.fc.HasNode(bv.blob.ParentRoot()) { log.WithFields(logging.BlobFields(bv.blob)).Debug("parent root not in forkchoice") - return ErrSidecarNotFinalizedDescendent + return blobErrBuilder(ErrSidecarNotFinalizedDescendent) } return nil } @@ -277,7 +259,7 @@ func (bv *ROBlobVerifier) SidecarInclusionProven() (err error) { defer bv.recordResult(RequireSidecarInclusionProven, &err) if err = blocks.VerifyKZGInclusionProof(bv.blob); err != nil { log.WithError(err).WithFields(logging.BlobFields(bv.blob)).Debug("sidecar inclusion proof verification failed") - return ErrSidecarInclusionProofInvalid + return blobErrBuilder(ErrSidecarInclusionProofInvalid) } return nil } @@ -289,7 +271,7 @@ func (bv *ROBlobVerifier) SidecarKzgProofVerified() (err error) { defer bv.recordResult(RequireSidecarKzgProofVerified, &err) if err = bv.verifyBlobCommitment(bv.blob); err != nil { log.WithError(err).WithFields(logging.BlobFields(bv.blob)).Debug("kzg commitment proof verification failed") - return ErrSidecarKzgProofInvalid + return blobErrBuilder(ErrSidecarKzgProofInvalid) } return nil } @@ -307,7 +289,7 @@ func (bv *ROBlobVerifier) SidecarProposerExpected(ctx context.Context) (err erro } r, err := bv.fc.TargetRootForEpoch(bv.blob.ParentRoot(), e) if err != nil { - return ErrSidecarUnexpectedProposer + return blobErrBuilder(ErrSidecarUnexpectedProposer) } c := &forkchoicetypes.Checkpoint{Root: r, Epoch: e} idx, cached := bv.pc.Proposer(c, bv.blob.Slot()) @@ -315,19 +297,19 @@ func (bv *ROBlobVerifier) SidecarProposerExpected(ctx context.Context) (err erro pst, err := bv.parentState(ctx) if err != nil { log.WithError(err).WithFields(logging.BlobFields(bv.blob)).Debug("state replay to parent_root failed") - return ErrSidecarUnexpectedProposer + return blobErrBuilder(ErrSidecarUnexpectedProposer) } idx, err = bv.pc.ComputeProposer(ctx, bv.blob.ParentRoot(), bv.blob.Slot(), pst) if err != nil { log.WithError(err).WithFields(logging.BlobFields(bv.blob)).Debug("error computing proposer index from parent state") - return ErrSidecarUnexpectedProposer + return blobErrBuilder(ErrSidecarUnexpectedProposer) } } if idx != bv.blob.ProposerIndex() { - log.WithError(ErrSidecarUnexpectedProposer). + log.WithError(blobErrBuilder(ErrSidecarUnexpectedProposer)). WithFields(logging.BlobFields(bv.blob)).WithField("expectedProposer", idx). Debug("unexpected blob proposer") - return ErrSidecarUnexpectedProposer + return blobErrBuilder(ErrSidecarUnexpectedProposer) } return nil } @@ -353,3 +335,7 @@ func blobToSignatureData(b blocks.ROBlob) SignatureData { Slot: b.Slot(), } } + +func blobErrBuilder(baseErr error) error { + return goError.Join(ErrBlobInvalid, baseErr) +} diff --git a/beacon-chain/verification/blob_test.go b/beacon-chain/verification/blob_test.go index e08707de464f..71f9d5408e26 100644 --- a/beacon-chain/verification/blob_test.go +++ b/beacon-chain/verification/blob_test.go @@ -27,13 +27,13 @@ func TestBlobIndexInBounds(t *testing.T) { _, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, 1) b := blobs[0] // set Index to a value that is out of bounds - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.BlobIndexInBounds()) require.Equal(t, true, v.results.executed(RequireBlobIndexInBounds)) require.NoError(t, v.results.result(RequireBlobIndexInBounds)) b.Index = fieldparams.MaxBlobsPerBlock - v = ini.NewBlobVerifier(b, GossipSidecarRequirements) + v = ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.BlobIndexInBounds(), ErrBlobIndexInvalid) require.Equal(t, true, v.results.executed(RequireBlobIndexInBounds)) require.NotNil(t, v.results.result(RequireBlobIndexInBounds)) @@ -52,7 +52,7 @@ func TestSlotNotTooEarly(t *testing.T) { // This clock will give a current slot of 1 on the nose happyClock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return now })) ini := Initializer{shared: &sharedResources{clock: happyClock}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.NotFromFutureSlot()) require.Equal(t, true, v.results.executed(RequireNotFromFutureSlot)) require.NoError(t, v.results.result(RequireNotFromFutureSlot)) @@ -61,7 +61,7 @@ func TestSlotNotTooEarly(t *testing.T) { // but still in the previous slot. closeClock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return now.Add(-1 * params.BeaconConfig().MaximumGossipClockDisparityDuration() / 2) })) ini = Initializer{shared: &sharedResources{clock: closeClock}} - v = ini.NewBlobVerifier(b, GossipSidecarRequirements) + v = ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.NotFromFutureSlot()) // This clock will give a current slot of 0, with now coming more than max clock disparity before slot 1 @@ -69,7 +69,7 @@ func TestSlotNotTooEarly(t *testing.T) { dispClock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return disparate })) // Set up initializer to use the clock that will set now to a little to far before slot 1 ini = Initializer{shared: &sharedResources{clock: dispClock}} - v = ini.NewBlobVerifier(b, GossipSidecarRequirements) + v = ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.NotFromFutureSlot(), ErrFromFutureSlot) require.Equal(t, true, v.results.executed(RequireNotFromFutureSlot)) require.NotNil(t, v.results.result(RequireNotFromFutureSlot)) @@ -114,7 +114,7 @@ func TestSlotAboveFinalized(t *testing.T) { _, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, 1) b := blobs[0] b.SignedBlockHeader.Header.Slot = c.slot - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) err := v.SlotAboveFinalized() require.Equal(t, true, v.results.executed(RequireSlotAboveFinalized)) if c.err == nil { @@ -146,7 +146,7 @@ func TestValidProposerSignature_Cached(t *testing.T) { }, } ini := Initializer{shared: &sharedResources{sc: sc, sr: &mockStateByRooter{sbr: sbrErrorIfCalled(t)}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.ValidProposerSignature(ctx)) require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) require.NoError(t, v.results.result(RequireValidProposerSignature)) @@ -159,7 +159,7 @@ func TestValidProposerSignature_Cached(t *testing.T) { return true, errors.New("derp") } ini = Initializer{shared: &sharedResources{sc: sc, sr: &mockStateByRooter{sbr: sbrErrorIfCalled(t)}}} - v = ini.NewBlobVerifier(b, GossipSidecarRequirements) + v = ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.ValidProposerSignature(ctx), ErrInvalidProposerSignature) require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) require.NotNil(t, v.results.result(RequireValidProposerSignature)) @@ -182,14 +182,14 @@ func TestValidProposerSignature_CacheMiss(t *testing.T) { }, } ini := Initializer{shared: &sharedResources{sc: sc, sr: sbrForValOverride(b.ProposerIndex(), ðpb.Validator{})}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.ValidProposerSignature(ctx)) require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) require.NoError(t, v.results.result(RequireValidProposerSignature)) // simulate state not found ini = Initializer{shared: &sharedResources{sc: sc, sr: sbrNotFound(t, expectedSd.Parent)}} - v = ini.NewBlobVerifier(b, GossipSidecarRequirements) + v = ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.ValidProposerSignature(ctx), ErrInvalidProposerSignature) require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) require.NotNil(t, v.results.result(RequireValidProposerSignature)) @@ -206,7 +206,7 @@ func TestValidProposerSignature_CacheMiss(t *testing.T) { }, } ini = Initializer{shared: &sharedResources{sc: sc, sr: sbr}} - v = ini.NewBlobVerifier(b, GossipSidecarRequirements) + v = ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) // make sure all the histories are clean before calling the method // so we don't get polluted by previous usages @@ -255,14 +255,14 @@ func TestSidecarParentSeen(t *testing.T) { t.Run("happy path", func(t *testing.T) { ini := Initializer{shared: &sharedResources{fc: fcHas}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.SidecarParentSeen(nil)) require.Equal(t, true, v.results.executed(RequireSidecarParentSeen)) require.NoError(t, v.results.result(RequireSidecarParentSeen)) }) t.Run("HasNode false, no badParent cb, expected error", func(t *testing.T) { ini := Initializer{shared: &sharedResources{fc: fcLacks}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarParentSeen(nil), ErrSidecarParentNotSeen) require.Equal(t, true, v.results.executed(RequireSidecarParentSeen)) require.NotNil(t, v.results.result(RequireSidecarParentSeen)) @@ -270,14 +270,14 @@ func TestSidecarParentSeen(t *testing.T) { t.Run("HasNode false, badParent true", func(t *testing.T) { ini := Initializer{shared: &sharedResources{fc: fcLacks}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.SidecarParentSeen(badParentCb(t, b.ParentRoot(), true))) require.Equal(t, true, v.results.executed(RequireSidecarParentSeen)) require.NoError(t, v.results.result(RequireSidecarParentSeen)) }) t.Run("HasNode false, badParent false", func(t *testing.T) { ini := Initializer{shared: &sharedResources{fc: fcLacks}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarParentSeen(badParentCb(t, b.ParentRoot(), false)), ErrSidecarParentNotSeen) require.Equal(t, true, v.results.executed(RequireSidecarParentSeen)) require.NotNil(t, v.results.result(RequireSidecarParentSeen)) @@ -289,14 +289,14 @@ func TestSidecarParentValid(t *testing.T) { b := blobs[0] t.Run("parent valid", func(t *testing.T) { ini := Initializer{shared: &sharedResources{}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.SidecarParentValid(badParentCb(t, b.ParentRoot(), false))) require.Equal(t, true, v.results.executed(RequireSidecarParentValid)) require.NoError(t, v.results.result(RequireSidecarParentValid)) }) t.Run("parent not valid", func(t *testing.T) { ini := Initializer{shared: &sharedResources{}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarParentValid(badParentCb(t, b.ParentRoot(), true)), ErrSidecarParentInvalid) require.Equal(t, true, v.results.executed(RequireSidecarParentValid)) require.NotNil(t, v.results.result(RequireSidecarParentValid)) @@ -340,7 +340,7 @@ func TestSidecarParentSlotLower(t *testing.T) { } return c.fcSlot, c.fcErr }}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) err := v.SidecarParentSlotLower() require.Equal(t, true, v.results.executed(RequireSidecarParentSlotLower)) if c.err == nil { @@ -364,7 +364,7 @@ func TestSidecarDescendsFromFinalized(t *testing.T) { } return false }}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarDescendsFromFinalized(), ErrSidecarNotFinalizedDescendent) require.Equal(t, true, v.results.executed(RequireSidecarDescendsFromFinalized)) require.NotNil(t, v.results.result(RequireSidecarDescendsFromFinalized)) @@ -376,7 +376,7 @@ func TestSidecarDescendsFromFinalized(t *testing.T) { } return true }}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.SidecarDescendsFromFinalized()) require.Equal(t, true, v.results.executed(RequireSidecarDescendsFromFinalized)) require.NoError(t, v.results.result(RequireSidecarDescendsFromFinalized)) @@ -389,7 +389,7 @@ func TestSidecarInclusionProven(t *testing.T) { b := blobs[0] ini := Initializer{} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.SidecarInclusionProven()) require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven)) require.NoError(t, v.results.result(RequireSidecarInclusionProven)) @@ -397,7 +397,7 @@ func TestSidecarInclusionProven(t *testing.T) { // Invert bits of the first byte of the body root to mess up the proof byte0 := b.SignedBlockHeader.Header.BodyRoot[0] b.SignedBlockHeader.Header.BodyRoot[0] = byte0 ^ 255 - v = ini.NewBlobVerifier(b, GossipSidecarRequirements) + v = ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarInclusionProven(), ErrSidecarInclusionProofInvalid) require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven)) require.NotNil(t, v.results.result(RequireSidecarInclusionProven)) @@ -409,7 +409,7 @@ func TestSidecarInclusionProvenElectra(t *testing.T) { b := blobs[0] ini := Initializer{} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.SidecarInclusionProven()) require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven)) require.NoError(t, v.results.result(RequireSidecarInclusionProven)) @@ -417,7 +417,7 @@ func TestSidecarInclusionProvenElectra(t *testing.T) { // Invert bits of the first byte of the body root to mess up the proof byte0 := b.SignedBlockHeader.Header.BodyRoot[0] b.SignedBlockHeader.Header.BodyRoot[0] = byte0 ^ 255 - v = ini.NewBlobVerifier(b, GossipSidecarRequirements) + v = ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarInclusionProven(), ErrSidecarInclusionProofInvalid) require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven)) require.NotNil(t, v.results.result(RequireSidecarInclusionProven)) @@ -452,21 +452,21 @@ func TestSidecarProposerExpected(t *testing.T) { b := blobs[0] t.Run("cached, matches", func(t *testing.T) { ini := Initializer{shared: &sharedResources{pc: &mockProposerCache{ProposerCB: pcReturnsIdx(b.ProposerIndex())}, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.SidecarProposerExpected(ctx)) require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) require.NoError(t, v.results.result(RequireSidecarProposerExpected)) }) t.Run("cached, does not match", func(t *testing.T) { ini := Initializer{shared: &sharedResources{pc: &mockProposerCache{ProposerCB: pcReturnsIdx(b.ProposerIndex() + 1)}, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer) require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) require.NotNil(t, v.results.result(RequireSidecarProposerExpected)) }) t.Run("not cached, state lookup failure", func(t *testing.T) { ini := Initializer{shared: &sharedResources{sr: sbrNotFound(t, b.ParentRoot()), pc: &mockProposerCache{ProposerCB: pcReturnsNotFound()}, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer) require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) require.NotNil(t, v.results.result(RequireSidecarProposerExpected)) @@ -482,7 +482,7 @@ func TestSidecarProposerExpected(t *testing.T) { }, } ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(b.ProposerIndex(), ðpb.Validator{}), pc: pc, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.NoError(t, v.SidecarProposerExpected(ctx)) require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) require.NoError(t, v.results.result(RequireSidecarProposerExpected)) @@ -497,7 +497,7 @@ func TestSidecarProposerExpected(t *testing.T) { }, } ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(b.ProposerIndex(), ðpb.Validator{}), pc: pc, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer) require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) require.NotNil(t, v.results.result(RequireSidecarProposerExpected)) @@ -512,7 +512,7 @@ func TestSidecarProposerExpected(t *testing.T) { }, } ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(b.ProposerIndex(), ðpb.Validator{}), pc: pc, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer) require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) require.NotNil(t, v.results.result(RequireSidecarProposerExpected)) @@ -523,7 +523,7 @@ func TestRequirementSatisfaction(t *testing.T) { _, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 1, 1) b := blobs[0] ini := Initializer{} - v := ini.NewBlobVerifier(b, GossipSidecarRequirements) + v := ini.NewBlobVerifier(b, GossipBlobSidecarRequirements) _, err := v.VerifiedROBlob() require.ErrorIs(t, err, ErrBlobInvalid) @@ -537,7 +537,7 @@ func TestRequirementSatisfaction(t *testing.T) { } // satisfy everything through the backdoor and ensure we get the verified ro blob at the end - for _, r := range GossipSidecarRequirements { + for _, r := range GossipBlobSidecarRequirements { v.results.record(r, nil) } require.Equal(t, true, v.results.allSatisfied()) diff --git a/beacon-chain/verification/data_column.go b/beacon-chain/verification/data_column.go new file mode 100644 index 000000000000..a5e3d8f1077c --- /dev/null +++ b/beacon-chain/verification/data_column.go @@ -0,0 +1,330 @@ +package verification + +import ( + "context" + goErrors "errors" + + "github.com/pkg/errors" + forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v5/runtime/logging" + "github.com/prysmaticlabs/prysm/v5/time/slots" + log "github.com/sirupsen/logrus" +) + +var allColumnSidecarRequirements = []Requirement{ + RequireDataColumnIndexInBounds, + RequireNotFromFutureSlot, + RequireSlotAboveFinalized, + RequireValidProposerSignature, + RequireSidecarParentSeen, + RequireSidecarParentValid, + RequireSidecarParentSlotLower, + RequireSidecarDescendsFromFinalized, + RequireSidecarInclusionProven, + RequireSidecarKzgProofVerified, + RequireSidecarProposerExpected, +} + +// GossipColumnSidecarRequirements defines the set of requirements that DataColumnSidecars received on gossip +// must satisfy in order to upgrade an RODataColumn to a VerifiedRODataColumn. +var GossipColumnSidecarRequirements = requirementList(allColumnSidecarRequirements).excluding() + +// SpectestColumnSidecarRequirements is used by the forkchoice spectests when verifying blobs used in the on_block tests. +// The only requirements we exclude for these tests are the parent validity and seen tests, as these are specific to +// gossip processing and require the bad block cache that we only use there. +var SpectestColumnSidecarRequirements = requirementList(GossipColumnSidecarRequirements).excluding( + RequireSidecarParentSeen, RequireSidecarParentValid) + +// InitsyncColumnSidecarRequirements is the list of verification requirements to be used by the init-sync service +// for batch-mode syncing. Because we only perform batch verification as part of the IsDataAvailable method +// for data columns after the block has been verified, and the blobs to be verified are keyed in the cache by the +// block root, the list of required verifications is much shorter than gossip. +var InitsyncColumnSidecarRequirements = requirementList(GossipColumnSidecarRequirements).excluding( + RequireNotFromFutureSlot, + RequireSlotAboveFinalized, + RequireSidecarParentSeen, + RequireSidecarParentValid, + RequireSidecarParentSlotLower, + RequireSidecarDescendsFromFinalized, + RequireSidecarProposerExpected, +) + +// BackfillColumnSidecarRequirements is the same as InitsyncColumnSidecarRequirements. +var BackfillColumnSidecarRequirements = requirementList(InitsyncColumnSidecarRequirements).excluding() + +// PendingQueueColumnSidecarRequirements is the same as InitsyncColumnSidecarRequirements, used by the pending blocks queue. +var PendingQueueColumnSidecarRequirements = requirementList(InitsyncColumnSidecarRequirements).excluding() + +var ( + // ErrColumnIndexInvalid means the column failed verification. + ErrColumnInvalid = errors.New("data column failed verification") + // ErrColumnIndexInvalid means RequireDataColumnIndexInBounds failed. + ErrColumnIndexInvalid = errors.New("incorrect column sidecar index") +) + +type RODataColumnVerifier struct { + *sharedResources + results *results + dataColumn blocks.RODataColumn + parent state.BeaconState + verifyDataColumnCommitment rodataColumnCommitmentVerifier +} + +type rodataColumnCommitmentVerifier func(blocks.RODataColumn) (bool, error) + +var _ DataColumnVerifier = &RODataColumnVerifier{} + +// VerifiedRODataColumn "upgrades" the wrapped ROBlob to a VerifiedROBlob. +// If any of the verifications ran against the blob failed, or some required verifications +// were not run, an error will be returned. +func (dv *RODataColumnVerifier) VerifiedRODataColumn() (blocks.VerifiedRODataColumn, error) { + if dv.results.allSatisfied() { + return blocks.NewVerifiedRODataColumn(dv.dataColumn), nil + } + return blocks.VerifiedRODataColumn{}, dv.results.errors(ErrColumnInvalid) +} + +// SatisfyRequirement allows the caller to assert that a requirement has been satisfied. +// This gives us a way to tick the box for a requirement where the usual method would be impractical. +// For example, when batch syncing, forkchoice is only updated at the end of the batch. So the checks that use +// forkchoice, like descends from finalized or parent seen, would necessarily fail. Allowing the caller to +// assert the requirement has been satisfied ensures we have an easy way to audit which piece of code is satisfying +// a requirement outside of this package. +func (dv *RODataColumnVerifier) SatisfyRequirement(req Requirement) { + dv.recordResult(req, nil) +} + +func (dv *RODataColumnVerifier) recordResult(req Requirement, err *error) { + if err == nil || *err == nil { + dv.results.record(req, nil) + return + } + dv.results.record(req, *err) +} + +// DataColumnIndexInBounds represents the follow spec verification: +// [REJECT] The sidecar's index is consistent with NUMBER_OF_COLUMNS -- i.e. data_column_sidecar.index < NUMBER_OF_COLUMNS. +func (dv *RODataColumnVerifier) DataColumnIndexInBounds() (err error) { + defer dv.recordResult(RequireDataColumnIndexInBounds, &err) + if dv.dataColumn.ColumnIndex >= fieldparams.NumberOfColumns { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("Sidecar index >= NUMBER_OF_COLUMNS") + return columnErrBuilder(ErrColumnIndexInvalid) + } + return nil +} + +// NotFromFutureSlot represents the spec verification: +// [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) +// -- i.e. validate that block_header.slot <= current_slot +func (dv *RODataColumnVerifier) NotFromFutureSlot() (err error) { + defer dv.recordResult(RequireNotFromFutureSlot, &err) + if dv.clock.CurrentSlot() == dv.dataColumn.Slot() { + return nil + } + // earliestStart represents the time the slot starts, lowered by MAXIMUM_GOSSIP_CLOCK_DISPARITY. + // We lower the time by MAXIMUM_GOSSIP_CLOCK_DISPARITY in case system time is running slightly behind real time. + earliestStart := dv.clock.SlotStart(dv.dataColumn.Slot()).Add(-1 * params.BeaconConfig().MaximumGossipClockDisparityDuration()) + // If the system time is still before earliestStart, we consider the column from a future slot and return an error. + if dv.clock.Now().Before(earliestStart) { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("sidecar slot is too far in the future") + return columnErrBuilder(ErrFromFutureSlot) + } + return nil +} + +// SlotAboveFinalized represents the spec verification: +// [IGNORE] The sidecar is from a slot greater than the latest finalized slot +// -- i.e. validate that block_header.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) +func (dv *RODataColumnVerifier) SlotAboveFinalized() (err error) { + defer dv.recordResult(RequireSlotAboveFinalized, &err) + fcp := dv.fc.FinalizedCheckpoint() + fSlot, err := slots.EpochStart(fcp.Epoch) + if err != nil { + return errors.Wrapf(columnErrBuilder(ErrSlotNotAfterFinalized), "error computing epoch start slot for finalized checkpoint (%d) %s", fcp.Epoch, err.Error()) + } + if dv.dataColumn.Slot() <= fSlot { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("sidecar slot is not after finalized checkpoint") + return columnErrBuilder(ErrSlotNotAfterFinalized) + } + return nil +} + +// ValidProposerSignature represents the spec verification: +// [REJECT] The proposer signature of sidecar.signed_block_header, is valid with respect to the block_header.proposer_index pubkey. +func (dv *RODataColumnVerifier) ValidProposerSignature(ctx context.Context) (err error) { + defer dv.recordResult(RequireValidProposerSignature, &err) + sd := columnToSignatureData(dv.dataColumn) + // First check if there is a cached verification that can be reused. + seen, err := dv.sc.SignatureVerified(sd) + if seen { + columnVerificationProposerSignatureCache.WithLabelValues("hit-valid").Inc() + if err != nil { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).WithError(err).Debug("reusing failed proposer signature validation from cache") + blobVerificationProposerSignatureCache.WithLabelValues("hit-invalid").Inc() + return columnErrBuilder(ErrInvalidProposerSignature) + } + return nil + } + columnVerificationProposerSignatureCache.WithLabelValues("miss").Inc() + + // Retrieve the parent state to fallback to full verification. + parent, err := dv.parentState(ctx) + if err != nil { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).WithError(err).Debug("could not replay parent state for column signature verification") + return columnErrBuilder(ErrInvalidProposerSignature) + } + // Full verification, which will subsequently be cached for anything sharing the signature cache. + if err = dv.sc.VerifySignature(sd, parent); err != nil { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).WithError(err).Debug("signature verification failed") + return columnErrBuilder(ErrInvalidProposerSignature) + } + return nil +} + +// SidecarParentSeen represents the spec verification: +// [IGNORE] The sidecar's block's parent (defined by block_header.parent_root) has been seen +// (via both gossip and non-gossip sources) (a client MAY queue sidecars for processing once the parent block is retrieved). +func (dv *RODataColumnVerifier) SidecarParentSeen(parentSeen func([32]byte) bool) (err error) { + defer dv.recordResult(RequireSidecarParentSeen, &err) + if parentSeen != nil && parentSeen(dv.dataColumn.ParentRoot()) { + return nil + } + if dv.fc.HasNode(dv.dataColumn.ParentRoot()) { + return nil + } + log.WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("parent root has not been seen") + return columnErrBuilder(ErrSidecarParentNotSeen) +} + +// SidecarParentValid represents the spec verification: +// [REJECT] The sidecar's block's parent (defined by block_header.parent_root) passes validation. +func (dv *RODataColumnVerifier) SidecarParentValid(badParent func([32]byte) bool) (err error) { + defer dv.recordResult(RequireSidecarParentValid, &err) + if badParent != nil && badParent(dv.dataColumn.ParentRoot()) { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("parent root is invalid") + return columnErrBuilder(ErrSidecarParentInvalid) + } + return nil +} + +// SidecarParentSlotLower represents the spec verification: +// [REJECT] The sidecar is from a higher slot than the sidecar's block's parent (defined by block_header.parent_root). +func (dv *RODataColumnVerifier) SidecarParentSlotLower() (err error) { + defer dv.recordResult(RequireSidecarParentSlotLower, &err) + parentSlot, err := dv.fc.Slot(dv.dataColumn.ParentRoot()) + if err != nil { + return errors.Wrap(columnErrBuilder(ErrSlotNotAfterParent), "parent root not in forkchoice") + } + if parentSlot >= dv.dataColumn.Slot() { + return ErrSlotNotAfterParent + } + return nil +} + +// SidecarDescendsFromFinalized represents the spec verification: +// [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block +// -- i.e. get_checkpoint_block(store, block_header.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root. +func (dv *RODataColumnVerifier) SidecarDescendsFromFinalized() (err error) { + defer dv.recordResult(RequireSidecarDescendsFromFinalized, &err) + if !dv.fc.HasNode(dv.dataColumn.ParentRoot()) { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("parent root not in forkchoice") + return columnErrBuilder(ErrSidecarNotFinalizedDescendent) + } + return nil +} + +// SidecarInclusionProven represents the spec verification: +// [REJECT] The sidecar's kzg_commitments field inclusion proof is valid as verified by verify_data_column_sidecar_inclusion_proof(sidecar). +func (dv *RODataColumnVerifier) SidecarInclusionProven() (err error) { + defer dv.recordResult(RequireSidecarInclusionProven, &err) + if err = blocks.VerifyKZGInclusionProofColumn(dv.dataColumn); err != nil { + log.WithError(err).WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("sidecar inclusion proof verification failed") + return columnErrBuilder(ErrSidecarInclusionProofInvalid) + } + return nil +} + +// SidecarKzgProofVerified represents the spec verification: +// [REJECT] The sidecar's column data is valid as verified by verify_data_column_sidecar_kzg_proofs(sidecar). +func (dv *RODataColumnVerifier) SidecarKzgProofVerified() (err error) { + defer dv.recordResult(RequireSidecarKzgProofVerified, &err) + ok, err := dv.verifyDataColumnCommitment(dv.dataColumn) + if err != nil { + log.WithError(err).WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("kzg commitment proof verification failed") + return columnErrBuilder(ErrSidecarKzgProofInvalid) + } + if !ok { + log.WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("kzg commitment proof verification failed") + return columnErrBuilder(ErrSidecarKzgProofInvalid) + } + return nil +} + +// SidecarProposerExpected represents the spec verification: +// [REJECT] The sidecar is proposed by the expected proposer_index for the block's slot +// in the context of the current shuffling (defined by block_header.parent_root/block_header.slot). +// If the proposer_index cannot immediately be verified against the expected shuffling, the sidecar MAY be queued +// for later processing while proposers for the block's branch are calculated -- in such a case do not REJECT, instead IGNORE this message. +func (dv *RODataColumnVerifier) SidecarProposerExpected(ctx context.Context) (err error) { + defer dv.recordResult(RequireSidecarProposerExpected, &err) + e := slots.ToEpoch(dv.dataColumn.Slot()) + if e > 0 { + e = e - 1 + } + r, err := dv.fc.TargetRootForEpoch(dv.dataColumn.ParentRoot(), e) + if err != nil { + return columnErrBuilder(ErrSidecarUnexpectedProposer) + } + c := &forkchoicetypes.Checkpoint{Root: r, Epoch: e} + idx, cached := dv.pc.Proposer(c, dv.dataColumn.Slot()) + if !cached { + pst, err := dv.parentState(ctx) + if err != nil { + log.WithError(err).WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("state replay to parent_root failed") + return columnErrBuilder(ErrSidecarUnexpectedProposer) + } + idx, err = dv.pc.ComputeProposer(ctx, dv.dataColumn.ParentRoot(), dv.dataColumn.Slot(), pst) + if err != nil { + log.WithError(err).WithFields(logging.DataColumnFields(dv.dataColumn)).Debug("error computing proposer index from parent state") + return columnErrBuilder(ErrSidecarUnexpectedProposer) + } + } + if idx != dv.dataColumn.ProposerIndex() { + log.WithError(columnErrBuilder(ErrSidecarUnexpectedProposer)). + WithFields(logging.DataColumnFields(dv.dataColumn)).WithField("expectedProposer", idx). + Debug("unexpected column proposer") + return columnErrBuilder(ErrSidecarUnexpectedProposer) + } + return nil +} + +func (dv *RODataColumnVerifier) parentState(ctx context.Context) (state.BeaconState, error) { + if dv.parent != nil { + return dv.parent, nil + } + st, err := dv.sr.StateByRoot(ctx, dv.dataColumn.ParentRoot()) + if err != nil { + return nil, err + } + dv.parent = st + return dv.parent, nil +} + +func columnToSignatureData(d blocks.RODataColumn) SignatureData { + return SignatureData{ + Root: d.BlockRoot(), + Parent: d.ParentRoot(), + Signature: bytesutil.ToBytes96(d.SignedBlockHeader.Signature), + Proposer: d.ProposerIndex(), + Slot: d.Slot(), + } +} + +func columnErrBuilder(baseErr error) error { + return goErrors.Join(ErrColumnInvalid, baseErr) +} diff --git a/beacon-chain/verification/data_column_test.go b/beacon-chain/verification/data_column_test.go new file mode 100644 index 000000000000..4433d3f8830c --- /dev/null +++ b/beacon-chain/verification/data_column_test.go @@ -0,0 +1,576 @@ +package verification + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/pkg/errors" + forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/startup" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/util" + "github.com/prysmaticlabs/prysm/v5/time/slots" +) + +func TestColumnIndexInBounds(t *testing.T) { + ini := &Initializer{} + _, cols := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + b := cols[0] + // set Index to a value that is out of bounds + v := ini.NewColumnVerifier(b, GossipColumnSidecarRequirements) + require.NoError(t, v.DataColumnIndexInBounds()) + require.Equal(t, true, v.results.executed(RequireDataColumnIndexInBounds)) + require.NoError(t, v.results.result(RequireDataColumnIndexInBounds)) + + b.ColumnIndex = fieldparams.NumberOfColumns + v = ini.NewColumnVerifier(b, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.DataColumnIndexInBounds(), ErrColumnIndexInvalid) + require.Equal(t, true, v.results.executed(RequireDataColumnIndexInBounds)) + require.NotNil(t, v.results.result(RequireDataColumnIndexInBounds)) +} + +func TestColumnSlotNotTooEarly(t *testing.T) { + now := time.Now() + // make genesis 1 slot in the past + genesis := now.Add(-1 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second) + + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + c := columns[0] + // slot 1 should be 12 seconds after genesis + c.SignedBlockHeader.Header.Slot = 1 + + // This clock will give a current slot of 1 on the nose + happyClock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return now })) + ini := Initializer{shared: &sharedResources{clock: happyClock}} + v := ini.NewColumnVerifier(c, GossipColumnSidecarRequirements) + require.NoError(t, v.NotFromFutureSlot()) + require.Equal(t, true, v.results.executed(RequireNotFromFutureSlot)) + require.NoError(t, v.results.result(RequireNotFromFutureSlot)) + + // Since we have an early return for slots that are directly equal, give a time that is less than max disparity + // but still in the previous slot. + closeClock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return now.Add(-1 * params.BeaconConfig().MaximumGossipClockDisparityDuration() / 2) })) + ini = Initializer{shared: &sharedResources{clock: closeClock}} + v = ini.NewColumnVerifier(c, GossipColumnSidecarRequirements) + require.NoError(t, v.NotFromFutureSlot()) + + // This clock will give a current slot of 0, with now coming more than max clock disparity before slot 1 + disparate := now.Add(-2 * params.BeaconConfig().MaximumGossipClockDisparityDuration()) + dispClock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return disparate })) + // Set up initializer to use the clock that will set now to a little to far before slot 1 + ini = Initializer{shared: &sharedResources{clock: dispClock}} + v = ini.NewColumnVerifier(c, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.NotFromFutureSlot(), ErrFromFutureSlot) + require.Equal(t, true, v.results.executed(RequireNotFromFutureSlot)) + require.NotNil(t, v.results.result(RequireNotFromFutureSlot)) +} + +func TestColumnSlotAboveFinalized(t *testing.T) { + ini := &Initializer{shared: &sharedResources{}} + cases := []struct { + name string + slot primitives.Slot + finalizedSlot primitives.Slot + err error + }{ + { + name: "finalized epoch < column epoch", + slot: 32, + }, + { + name: "finalized slot < column slot (same epoch)", + slot: 31, + }, + { + name: "finalized epoch > column epoch", + finalizedSlot: 32, + err: ErrSlotNotAfterFinalized, + }, + { + name: "finalized slot == column slot", + slot: 35, + finalizedSlot: 35, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + finalizedCB := func() *forkchoicetypes.Checkpoint { + return &forkchoicetypes.Checkpoint{ + Epoch: slots.ToEpoch(c.finalizedSlot), + Root: [32]byte{}, + } + } + ini.shared.fc = &mockForkchoicer{FinalizedCheckpointCB: finalizedCB} + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + col.SignedBlockHeader.Header.Slot = c.slot + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + err := v.SlotAboveFinalized() + require.Equal(t, true, v.results.executed(RequireSlotAboveFinalized)) + if c.err == nil { + require.NoError(t, err) + require.NoError(t, v.results.result(RequireSlotAboveFinalized)) + } else { + require.ErrorIs(t, err, c.err) + require.NotNil(t, v.results.result(RequireSlotAboveFinalized)) + } + }) + } +} + +func TestDataColumnValidProposerSignature_Cached(t *testing.T) { + ctx := context.Background() + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + expectedSd := columnToSignatureData(col) + sc := &mockSignatureCache{ + svcb: func(sig SignatureData) (bool, error) { + if sig != expectedSd { + t.Error("Did not see expected SignatureData") + } + return true, nil + }, + vscb: func(sig SignatureData, v ValidatorAtIndexer) (err error) { + t.Error("VerifySignature should not be called if the result is cached") + return nil + }, + } + ini := Initializer{shared: &sharedResources{sc: sc, sr: &mockStateByRooter{sbr: sbrErrorIfCalled(t)}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.ValidProposerSignature(ctx)) + require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) + require.NoError(t, v.results.result(RequireValidProposerSignature)) + + // simulate an error in the cache - indicating the previous verification failed + sc.svcb = func(sig SignatureData) (bool, error) { + if sig != expectedSd { + t.Error("Did not see expected SignatureData") + } + return true, errors.New("derp") + } + ini = Initializer{shared: &sharedResources{sc: sc, sr: &mockStateByRooter{sbr: sbrErrorIfCalled(t)}}} + v = ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.ValidProposerSignature(ctx), ErrInvalidProposerSignature) + require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) + require.NotNil(t, v.results.result(RequireValidProposerSignature)) +} + +func TestColumnValidProposerSignature_CacheMiss(t *testing.T) { + ctx := context.Background() + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + expectedSd := columnToSignatureData(col) + sc := &mockSignatureCache{ + svcb: func(sig SignatureData) (bool, error) { + return false, nil + }, + vscb: func(sig SignatureData, v ValidatorAtIndexer) (err error) { + if expectedSd != sig { + t.Error("unexpected signature data") + } + return nil + }, + } + ini := Initializer{shared: &sharedResources{sc: sc, sr: sbrForValOverride(col.ProposerIndex(), ðpb.Validator{})}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.ValidProposerSignature(ctx)) + require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) + require.NoError(t, v.results.result(RequireValidProposerSignature)) + + // simulate state not found + ini = Initializer{shared: &sharedResources{sc: sc, sr: sbrNotFound(t, expectedSd.Parent)}} + v = ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.ValidProposerSignature(ctx), ErrInvalidProposerSignature) + require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) + require.NotNil(t, v.results.result(RequireValidProposerSignature)) + + // simulate successful state lookup, but sig failure + sbr := sbrForValOverride(col.ProposerIndex(), ðpb.Validator{}) + sc = &mockSignatureCache{ + svcb: sc.svcb, + vscb: func(sig SignatureData, v ValidatorAtIndexer) (err error) { + if expectedSd != sig { + t.Error("unexpected signature data") + } + return errors.New("signature, not so good!") + }, + } + ini = Initializer{shared: &sharedResources{sc: sc, sr: sbr}} + v = ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + + // make sure all the histories are clean before calling the method + // so we don't get polluted by previous usages + require.Equal(t, false, sbr.calledForRoot[expectedSd.Parent]) + require.Equal(t, false, sc.svCalledForSig[expectedSd]) + require.Equal(t, false, sc.vsCalledForSig[expectedSd]) + + // Here we're mainly checking that all the right interfaces get used in the unhappy path + require.ErrorIs(t, v.ValidProposerSignature(ctx), ErrInvalidProposerSignature) + require.Equal(t, true, sbr.calledForRoot[expectedSd.Parent]) + require.Equal(t, true, sc.svCalledForSig[expectedSd]) + require.Equal(t, true, sc.vsCalledForSig[expectedSd]) + require.Equal(t, true, v.results.executed(RequireValidProposerSignature)) + require.NotNil(t, v.results.result(RequireValidProposerSignature)) +} + +func TestColumnSidecarParentSeen(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + + fcHas := &mockForkchoicer{ + HasNodeCB: func(parent [32]byte) bool { + if parent != col.ParentRoot() { + t.Error("forkchoice.HasNode called with unexpected parent root") + } + return true + }, + } + fcLacks := &mockForkchoicer{ + HasNodeCB: func(parent [32]byte) bool { + if parent != col.ParentRoot() { + t.Error("forkchoice.HasNode called with unexpected parent root") + } + return false + }, + } + + t.Run("happy path", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{fc: fcHas}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarParentSeen(nil)) + require.Equal(t, true, v.results.executed(RequireSidecarParentSeen)) + require.NoError(t, v.results.result(RequireSidecarParentSeen)) + }) + t.Run("HasNode false, no badParent cb, expected error", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{fc: fcLacks}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarParentSeen(nil), ErrSidecarParentNotSeen) + require.Equal(t, true, v.results.executed(RequireSidecarParentSeen)) + require.NotNil(t, v.results.result(RequireSidecarParentSeen)) + }) + + t.Run("HasNode false, badParent true", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{fc: fcLacks}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarParentSeen(badParentCb(t, col.ParentRoot(), true))) + require.Equal(t, true, v.results.executed(RequireSidecarParentSeen)) + require.NoError(t, v.results.result(RequireSidecarParentSeen)) + }) + t.Run("HasNode false, badParent false", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{fc: fcLacks}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarParentSeen(badParentCb(t, col.ParentRoot(), false)), ErrSidecarParentNotSeen) + require.Equal(t, true, v.results.executed(RequireSidecarParentSeen)) + require.NotNil(t, v.results.result(RequireSidecarParentSeen)) + }) +} + +func TestColumnSidecarParentValid(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + t.Run("parent valid", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarParentValid(badParentCb(t, col.ParentRoot(), false))) + require.Equal(t, true, v.results.executed(RequireSidecarParentValid)) + require.NoError(t, v.results.result(RequireSidecarParentValid)) + }) + t.Run("parent not valid", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarParentValid(badParentCb(t, col.ParentRoot(), true)), ErrSidecarParentInvalid) + require.Equal(t, true, v.results.executed(RequireSidecarParentValid)) + require.NotNil(t, v.results.result(RequireSidecarParentValid)) + }) +} + +func TestColumnSidecarParentSlotLower(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 1, 1) + col := columns[0] + cases := []struct { + name string + fcSlot primitives.Slot + fcErr error + err error + }{ + { + name: "not in fc", + fcErr: errors.New("not in forkchoice"), + err: ErrSlotNotAfterParent, + }, + { + name: "in fc, slot lower", + fcSlot: col.Slot() - 1, + }, + { + name: "in fc, slot equal", + fcSlot: col.Slot(), + err: ErrSlotNotAfterParent, + }, + { + name: "in fc, slot higher", + fcSlot: col.Slot() + 1, + err: ErrSlotNotAfterParent, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + ini := Initializer{shared: &sharedResources{fc: &mockForkchoicer{SlotCB: func(r [32]byte) (primitives.Slot, error) { + if col.ParentRoot() != r { + t.Error("forkchoice.Slot called with unexpected parent root") + } + return c.fcSlot, c.fcErr + }}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + err := v.SidecarParentSlotLower() + require.Equal(t, true, v.results.executed(RequireSidecarParentSlotLower)) + if c.err == nil { + require.NoError(t, err) + require.NoError(t, v.results.result(RequireSidecarParentSlotLower)) + } else { + require.ErrorIs(t, err, c.err) + require.NotNil(t, v.results.result(RequireSidecarParentSlotLower)) + } + }) + } +} + +func TestColumnSidecarDescendsFromFinalized(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + t.Run("not canonical", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{fc: &mockForkchoicer{HasNodeCB: func(r [32]byte) bool { + if col.ParentRoot() != r { + t.Error("forkchoice.Slot called with unexpected parent root") + } + return false + }}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarDescendsFromFinalized(), ErrSidecarNotFinalizedDescendent) + require.Equal(t, true, v.results.executed(RequireSidecarDescendsFromFinalized)) + require.NotNil(t, v.results.result(RequireSidecarDescendsFromFinalized)) + }) + t.Run("canonical", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{fc: &mockForkchoicer{HasNodeCB: func(r [32]byte) bool { + if col.ParentRoot() != r { + t.Error("forkchoice.Slot called with unexpected parent root") + } + return true + }}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarDescendsFromFinalized()) + require.Equal(t, true, v.results.executed(RequireSidecarDescendsFromFinalized)) + require.NoError(t, v.results.result(RequireSidecarDescendsFromFinalized)) + }) +} + +func TestColumnSidecarInclusionProven(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + + ini := Initializer{} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarInclusionProven()) + require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven)) + require.NoError(t, v.results.result(RequireSidecarInclusionProven)) + + // Invert bits of the first byte of the body root to mess up the proof + byte0 := col.SignedBlockHeader.Header.BodyRoot[0] + col.SignedBlockHeader.Header.BodyRoot[0] = byte0 ^ 255 + v = ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarInclusionProven(), ErrSidecarInclusionProofInvalid) + require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven)) + require.NotNil(t, v.results.result(RequireSidecarInclusionProven)) +} + +func TestColumnSidecarInclusionProvenElectra(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + + ini := Initializer{} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarInclusionProven()) + require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven)) + require.NoError(t, v.results.result(RequireSidecarInclusionProven)) + + // Invert bits of the first byte of the body root to mess up the proof + byte0 := col.SignedBlockHeader.Header.BodyRoot[0] + col.SignedBlockHeader.Header.BodyRoot[0] = byte0 ^ 255 + v = ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarInclusionProven(), ErrSidecarInclusionProofInvalid) + require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven)) + require.NotNil(t, v.results.result(RequireSidecarInclusionProven)) +} + +func TestColumnSidecarKzgProofVerified(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 0, 1) + col := columns[0] + passes := func(vb blocks.RODataColumn) (bool, error) { + require.Equal(t, true, reflect.DeepEqual(col.KzgCommitments, vb.KzgCommitments)) + return true, nil + } + v := &RODataColumnVerifier{verifyDataColumnCommitment: passes, results: newResults(), dataColumn: col} + require.NoError(t, v.SidecarKzgProofVerified()) + require.Equal(t, true, v.results.executed(RequireSidecarKzgProofVerified)) + require.NoError(t, v.results.result(RequireSidecarKzgProofVerified)) + + fails := func(vb blocks.RODataColumn) (bool, error) { + require.Equal(t, true, reflect.DeepEqual(col.KzgCommitments, vb.KzgCommitments)) + return false, errors.New("bad blob") + } + v = &RODataColumnVerifier{results: newResults(), dataColumn: col, verifyDataColumnCommitment: fails} + require.ErrorIs(t, v.SidecarKzgProofVerified(), ErrSidecarKzgProofInvalid) + require.Equal(t, true, v.results.executed(RequireSidecarKzgProofVerified)) + require.NotNil(t, v.results.result(RequireSidecarKzgProofVerified)) +} + +func TestColumnSidecarProposerExpected(t *testing.T) { + ctx := context.Background() + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 1, 1) + col := columns[0] + t.Run("cached, matches", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{pc: &mockProposerCache{ProposerCB: pcReturnsIdx(col.ProposerIndex())}, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarProposerExpected(ctx)) + require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) + require.NoError(t, v.results.result(RequireSidecarProposerExpected)) + }) + t.Run("cached, does not match", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{pc: &mockProposerCache{ProposerCB: pcReturnsIdx(col.ProposerIndex() + 1)}, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer) + require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) + require.NotNil(t, v.results.result(RequireSidecarProposerExpected)) + }) + t.Run("not cached, state lookup failure", func(t *testing.T) { + ini := Initializer{shared: &sharedResources{sr: sbrNotFound(t, col.ParentRoot()), pc: &mockProposerCache{ProposerCB: pcReturnsNotFound()}, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer) + require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) + require.NotNil(t, v.results.result(RequireSidecarProposerExpected)) + }) + + t.Run("not cached, proposer matches", func(t *testing.T) { + pc := &mockProposerCache{ + ProposerCB: pcReturnsNotFound(), + ComputeProposerCB: func(ctx context.Context, root [32]byte, slot primitives.Slot, pst state.BeaconState) (primitives.ValidatorIndex, error) { + require.Equal(t, col.ParentRoot(), root) + require.Equal(t, col.Slot(), slot) + return col.ProposerIndex(), nil + }, + } + ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(col.ProposerIndex(), ðpb.Validator{}), pc: pc, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarProposerExpected(ctx)) + require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) + require.NoError(t, v.results.result(RequireSidecarProposerExpected)) + }) + + t.Run("not cached, proposer matches for next epoch", func(t *testing.T) { + _, newCols := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 2*params.BeaconConfig().SlotsPerEpoch, 1) + + newCol := newCols[0] + pc := &mockProposerCache{ + ProposerCB: pcReturnsNotFound(), + ComputeProposerCB: func(ctx context.Context, root [32]byte, slot primitives.Slot, pst state.BeaconState) (primitives.ValidatorIndex, error) { + require.Equal(t, newCol.ParentRoot(), root) + require.Equal(t, newCol.Slot(), slot) + return col.ProposerIndex(), nil + }, + } + ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(newCol.ProposerIndex(), ðpb.Validator{}), pc: pc, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} + v := ini.NewColumnVerifier(newCol, GossipColumnSidecarRequirements) + require.NoError(t, v.SidecarProposerExpected(ctx)) + require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) + require.NoError(t, v.results.result(RequireSidecarProposerExpected)) + }) + t.Run("not cached, proposer does not match", func(t *testing.T) { + pc := &mockProposerCache{ + ProposerCB: pcReturnsNotFound(), + ComputeProposerCB: func(ctx context.Context, root [32]byte, slot primitives.Slot, pst state.BeaconState) (primitives.ValidatorIndex, error) { + require.Equal(t, col.ParentRoot(), root) + require.Equal(t, col.Slot(), slot) + return col.ProposerIndex() + 1, nil + }, + } + ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(col.ProposerIndex(), ðpb.Validator{}), pc: pc, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer) + require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) + require.NotNil(t, v.results.result(RequireSidecarProposerExpected)) + }) + t.Run("not cached, ComputeProposer fails", func(t *testing.T) { + pc := &mockProposerCache{ + ProposerCB: pcReturnsNotFound(), + ComputeProposerCB: func(ctx context.Context, root [32]byte, slot primitives.Slot, pst state.BeaconState) (primitives.ValidatorIndex, error) { + require.Equal(t, col.ParentRoot(), root) + require.Equal(t, col.Slot(), slot) + return 0, errors.New("ComputeProposer failed") + }, + } + ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(col.ProposerIndex(), ðpb.Validator{}), pc: pc, fc: &mockForkchoicer{TargetRootForEpochCB: fcReturnsTargetRoot([32]byte{})}}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer) + require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected)) + require.NotNil(t, v.results.result(RequireSidecarProposerExpected)) + }) +} + +func TestColumnRequirementSatisfaction(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 1, 1) + col := columns[0] + ini := Initializer{} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + + _, err := v.VerifiedRODataColumn() + require.ErrorIs(t, err, ErrColumnInvalid) + var me VerificationMultiError + ok := errors.As(err, &me) + require.Equal(t, true, ok) + fails := me.Failures() + // we haven't performed any verification, so all the results should be this type + for _, v := range fails { + require.ErrorIs(t, v, ErrMissingVerification) + } + + // satisfy everything through the backdoor and ensure we get the verified ro blob at the end + for _, r := range GossipColumnSidecarRequirements { + v.results.record(r, nil) + } + require.Equal(t, true, v.results.allSatisfied()) + _, err = v.VerifiedRODataColumn() + require.NoError(t, err) +} + +func TestStateCaching(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 1, 1) + col := columns[0] + ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(col.ProposerIndex(), ðpb.Validator{})}} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + _, err := v.parentState(context.Background()) + require.NoError(t, err) + + // Utilize the cached state. + v.sr = nil + _, err = v.parentState(context.Background()) + require.NoError(t, err) +} + +func TestColumnSatisfyRequirement(t *testing.T) { + _, columns := util.GenerateTestDenebBlockWithColumns(t, [32]byte{}, 1, 1) + col := columns[0] + ini := Initializer{} + v := ini.NewColumnVerifier(col, GossipColumnSidecarRequirements) + require.Equal(t, false, v.results.executed(RequireDataColumnIndexInBounds)) + + v.SatisfyRequirement(RequireDataColumnIndexInBounds) + require.Equal(t, true, v.results.executed(RequireDataColumnIndexInBounds)) +} diff --git a/beacon-chain/verification/error.go b/beacon-chain/verification/error.go index 9260184e54f0..e67659e68022 100644 --- a/beacon-chain/verification/error.go +++ b/beacon-chain/verification/error.go @@ -2,8 +2,30 @@ package verification import "github.com/pkg/errors" -// ErrMissingVerification indicates that the given verification function was never performed on the value. -var ErrMissingVerification = errors.New("verification was not performed for requirement") +var ( + // ErrFromFutureSlot means RequireSlotNotTooEarly failed. + ErrFromFutureSlot = errors.New("slot is too far in the future") + // ErrSlotNotAfterFinalized means RequireSlotAboveFinalized failed. + ErrSlotNotAfterFinalized = errors.New("slot <= finalized checkpoint") + // ErrInvalidProposerSignature means RequireValidProposerSignature failed. + ErrInvalidProposerSignature = errors.New("proposer signature could not be verified") + // ErrSidecarParentNotSeen means RequireSidecarParentSeen failed. + ErrSidecarParentNotSeen = errors.New("parent root has not been seen") + // ErrSidecarParentInvalid means RequireSidecarParentValid failed. + ErrSidecarParentInvalid = errors.New("parent block is not valid") + // ErrSlotNotAfterParent means RequireSidecarParentSlotLower failed. + ErrSlotNotAfterParent = errors.New("slot <= slot") + // ErrSidecarNotFinalizedDescendent means RequireSidecarDescendsFromFinalized failed. + ErrSidecarNotFinalizedDescendent = errors.New("parent is not descended from the finalized block") + // ErrSidecarInclusionProofInvalid means RequireSidecarInclusionProven failed. + ErrSidecarInclusionProofInvalid = errors.New("sidecar inclusion proof verification failed") + // ErrSidecarKzgProofInvalid means RequireSidecarKzgProofVerified failed. + ErrSidecarKzgProofInvalid = errors.New("sidecar kzg commitment proof verification failed") + // ErrSidecarUnexpectedProposer means RequireSidecarProposerExpected failed. + ErrSidecarUnexpectedProposer = errors.New("sidecar was not proposed by the expected proposer_index") + // ErrMissingVerification indicates that the given verification function was never performed on the value. + ErrMissingVerification = errors.New("verification was not performed for requirement") +) // VerificationMultiError is a custom error that can be used to access individual verification failures. type VerificationMultiError struct { diff --git a/beacon-chain/verification/initializer.go b/beacon-chain/verification/initializer.go index ebdfecfe8a8f..782fad14c444 100644 --- a/beacon-chain/verification/initializer.go +++ b/beacon-chain/verification/initializer.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/kzg" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/peerdas" forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" "github.com/prysmaticlabs/prysm/v5/beacon-chain/startup" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" @@ -59,6 +60,16 @@ func (ini *Initializer) NewBlobVerifier(b blocks.ROBlob, reqs []Requirement) *RO } } +// NewColumnVerifier creates a DataColumnVerifier for a single data column, with the given set of requirements. +func (ini *Initializer) NewColumnVerifier(d blocks.RODataColumn, reqs []Requirement) *RODataColumnVerifier { + return &RODataColumnVerifier{ + sharedResources: ini.shared, + dataColumn: d, + results: newResults(reqs...), + verifyDataColumnCommitment: peerdas.VerifyDataColumnSidecarKZGProofs, + } +} + func (ini *Initializer) VerifyProposer(ctx context.Context, dc blocks.RODataColumn) error { e := slots.ToEpoch(dc.Slot()) if e > 0 { diff --git a/beacon-chain/verification/interface.go b/beacon-chain/verification/interface.go index 52a4d13ae780..19a7607ce67f 100644 --- a/beacon-chain/verification/interface.go +++ b/beacon-chain/verification/interface.go @@ -30,6 +30,24 @@ type BlobVerifier interface { // able to mock Initializer.NewBlobVerifier without complex setup. type NewBlobVerifier func(b blocks.ROBlob, reqs []Requirement) BlobVerifier +// DataColumnVerifier defines the methods implemented by the RODataColumnVerifier. +// It serves a very similar purpose as the blob verifier interface for data columns. +type DataColumnVerifier interface { + VerifiedRODataColumn() (blocks.VerifiedRODataColumn, error) + DataColumnIndexInBounds() (err error) + NotFromFutureSlot() (err error) + SlotAboveFinalized() (err error) + ValidProposerSignature(ctx context.Context) (err error) + SidecarParentSeen(parentSeen func([32]byte) bool) (err error) + SidecarParentValid(badParent func([32]byte) bool) (err error) + SidecarParentSlotLower() (err error) + SidecarDescendsFromFinalized() (err error) + SidecarInclusionProven() (err error) + SidecarKzgProofVerified() (err error) + SidecarProposerExpected(ctx context.Context) (err error) + SatisfyRequirement(Requirement) +} + // NewColumnVerifier is a function signature that can be used to mock a setup where a // column verifier can be easily initialized. -type NewColumnVerifier func(ctx context.Context, dc blocks.RODataColumn) error +type NewColumnVerifier func(dc blocks.RODataColumn, reqs []Requirement) DataColumnVerifier diff --git a/beacon-chain/verification/metrics.go b/beacon-chain/verification/metrics.go index 85e86b9df1a7..699fdbdae0df 100644 --- a/beacon-chain/verification/metrics.go +++ b/beacon-chain/verification/metrics.go @@ -13,4 +13,11 @@ var ( }, []string{"result"}, ) + columnVerificationProposerSignatureCache = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "data_column_verification_proposer_signature_cache", + Help: "DataColumnSidecar proposer signature cache result.", + }, + []string{"result"}, + ) ) diff --git a/beacon-chain/verification/result_test.go b/beacon-chain/verification/result_test.go index 5f4f7f9664f9..036177ecbbeb 100644 --- a/beacon-chain/verification/result_test.go +++ b/beacon-chain/verification/result_test.go @@ -39,7 +39,7 @@ func TestResultList(t *testing.T) { func TestExportedBlobSanityCheck(t *testing.T) { // make sure all requirement lists contain the bare minimum checks sanity := []Requirement{RequireValidProposerSignature, RequireSidecarKzgProofVerified, RequireBlobIndexInBounds, RequireSidecarInclusionProven} - reqs := [][]Requirement{GossipSidecarRequirements, SpectestSidecarRequirements, InitsyncSidecarRequirements, BackfillSidecarRequirements, PendingQueueSidecarRequirements} + reqs := [][]Requirement{GossipBlobSidecarRequirements, SpectestBlobSidecarRequirements, InitsyncBlobSidecarRequirements, BackfillBlobSidecarRequirements, PendingQueueBlobSidecarRequirements} for i := range reqs { r := reqs[i] reqMap := make(map[Requirement]struct{}) @@ -51,13 +51,13 @@ func TestExportedBlobSanityCheck(t *testing.T) { require.Equal(t, true, ok) } } - require.DeepEqual(t, allSidecarRequirements, GossipSidecarRequirements) + require.DeepEqual(t, allBlobSidecarRequirements, GossipBlobSidecarRequirements) } func TestAllBlobRequirementsHaveStrings(t *testing.T) { var derp Requirement = math.MaxInt require.Equal(t, unknownRequirementName, derp.String()) - for i := range allSidecarRequirements { - require.NotEqual(t, unknownRequirementName, allSidecarRequirements[i].String()) + for i := range allBlobSidecarRequirements { + require.NotEqual(t, unknownRequirementName, allBlobSidecarRequirements[i].String()) } } diff --git a/beacon-chain/verification/verification_test.go b/beacon-chain/verification/verification_test.go new file mode 100644 index 000000000000..44eb3e64a980 --- /dev/null +++ b/beacon-chain/verification/verification_test.go @@ -0,0 +1,15 @@ +package verification + +import ( + "os" + "testing" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/kzg" +) + +func TestMain(t *testing.M) { + if err := kzg.Start(); err != nil { + os.Exit(1) + } + t.Run() +} diff --git a/consensus-types/blocks/kzg.go b/consensus-types/blocks/kzg.go index c2f14f019ce5..cbc9c44161b5 100644 --- a/consensus-types/blocks/kzg.go +++ b/consensus-types/blocks/kzg.go @@ -8,7 +8,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v5/container/trie" "github.com/prysmaticlabs/prysm/v5/encoding/ssz" - ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/runtime/version" ) @@ -50,7 +49,7 @@ func VerifyKZGInclusionProof(blob ROBlob) error { // VerifyKZGInclusionProofColumn verifies the Merkle proof in a data column sidecar against // the beacon block body root. -func VerifyKZGInclusionProofColumn(sc *ethpb.DataColumnSidecar) error { +func VerifyKZGInclusionProofColumn(sc RODataColumn) error { if sc.SignedBlockHeader == nil { return errNilBlockHeader } diff --git a/consensus-types/blocks/kzg_test.go b/consensus-types/blocks/kzg_test.go index e0fb3e8557ee..b64e06ebd37f 100644 --- a/consensus-types/blocks/kzg_test.go +++ b/consensus-types/blocks/kzg_test.go @@ -365,7 +365,7 @@ func Test_VerifyKZGInclusionProofColumn(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := VerifyKZGInclusionProofColumn(tc.dataColumnSidecar) + err = VerifyKZGInclusionProofColumn(RODataColumn{DataColumnSidecar: tc.dataColumnSidecar}) if tc.expectedError == nil { require.NoError(t, err) return diff --git a/runtime/logging/BUILD.bazel b/runtime/logging/BUILD.bazel index 5bdf03adc3ef..058bf45e020c 100644 --- a/runtime/logging/BUILD.bazel +++ b/runtime/logging/BUILD.bazel @@ -2,7 +2,10 @@ load("@prysm//tools/go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["blob.go"], + srcs = [ + "blob.go", + "data_column.go", + ], importpath = "github.com/prysmaticlabs/prysm/v5/runtime/logging", visibility = ["//visibility:public"], deps = [ diff --git a/runtime/logging/data_column.go b/runtime/logging/data_column.go new file mode 100644 index 000000000000..31bce28c2a02 --- /dev/null +++ b/runtime/logging/data_column.go @@ -0,0 +1,32 @@ +package logging + +import ( + "fmt" + + "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + "github.com/sirupsen/logrus" +) + +// DataColumnFields extracts a standard set of fields from a DataColumnSidecar into a logrus.Fields struct +// which can be passed to log.WithFields. +func DataColumnFields(column blocks.RODataColumn) logrus.Fields { + return logrus.Fields{ + "slot": column.Slot(), + "proposerIndex": column.ProposerIndex(), + "blockRoot": fmt.Sprintf("%#x", column.BlockRoot()), + "parentRoot": fmt.Sprintf("%#x", column.ParentRoot()), + "kzgCommitments": fmt.Sprintf("%#x", column.KzgCommitments), + "index": column.ColumnIndex, + } +} + +// BlockFieldsFromColumn extracts the set of fields from a given DataColumnSidecar which are shared by the block and +// all other sidecars for the block. +func BlockFieldsFromColumn(column blocks.RODataColumn) logrus.Fields { + return logrus.Fields{ + "slot": column.Slot(), + "proposerIndex": column.ProposerIndex(), + "blockRoot": fmt.Sprintf("%#x", column.BlockRoot()), + "parentRoot": fmt.Sprintf("%#x", column.ParentRoot()), + } +} diff --git a/testing/spectest/shared/common/forkchoice/runner.go b/testing/spectest/shared/common/forkchoice/runner.go index c999888c80db..0808727ddd97 100644 --- a/testing/spectest/shared/common/forkchoice/runner.go +++ b/testing/spectest/shared/common/forkchoice/runner.go @@ -372,7 +372,7 @@ func runBlobStep(t *testing.T, require.NoError(t, err) ini, err := builder.vwait.WaitForInitializer(context.Background()) require.NoError(t, err) - bv := ini.NewBlobVerifier(ro, verification.SpectestSidecarRequirements) + bv := ini.NewBlobVerifier(ro, verification.SpectestBlobSidecarRequirements) ctx := context.Background() if err := bv.BlobIndexInBounds(); err != nil { t.Logf("BlobIndexInBounds error: %s", err.Error()) diff --git a/testing/util/BUILD.bazel b/testing/util/BUILD.bazel index 8456b8d17824..dec7a8ef8d7a 100644 --- a/testing/util/BUILD.bazel +++ b/testing/util/BUILD.bazel @@ -29,9 +29,11 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/v5/testing/util", visibility = ["//visibility:public"], deps = [ + "//beacon-chain/blockchain/kzg:go_default_library", "//beacon-chain/core/altair:go_default_library", "//beacon-chain/core/blocks:go_default_library", "//beacon-chain/core/helpers:go_default_library", + "//beacon-chain/core/peerdas:go_default_library", "//beacon-chain/core/signing:go_default_library", "//beacon-chain/core/time:go_default_library", "//beacon-chain/core/transition:go_default_library", diff --git a/testing/util/deneb.go b/testing/util/deneb.go index 12a888bf9d11..bf482d88fed9 100644 --- a/testing/util/deneb.go +++ b/testing/util/deneb.go @@ -7,6 +7,8 @@ import ( "github.com/ethereum/go-ethereum/common" gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/kzg" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/peerdas" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" @@ -172,6 +174,122 @@ func GenerateTestDenebBlobSidecar(t *testing.T, root [32]byte, header *ethpb.Sig return r } +func GenerateTestDenebBlockWithColumns(t *testing.T, parent [32]byte, slot primitives.Slot, nblobs int, opts ...DenebBlockGeneratorOption) (blocks.ROBlock, []blocks.RODataColumn) { + g := &denebBlockGenerator{ + parent: parent, + slot: slot, + nblobs: nblobs, + } + for _, o := range opts { + o(g) + } + + if g.payload == nil { + stateRoot := bytesutil.PadTo([]byte("stateRoot"), fieldparams.RootLength) + ads := common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87") + tx := gethTypes.NewTx(&gethTypes.LegacyTx{ + Nonce: 0, + To: &ads, + Value: big.NewInt(0), + Gas: 0, + GasPrice: big.NewInt(0), + Data: nil, + }) + + txs := []*gethTypes.Transaction{tx} + encodedBinaryTxs := make([][]byte, 1) + var err error + encodedBinaryTxs[0], err = txs[0].MarshalBinary() + require.NoError(t, err) + blockHash := bytesutil.ToBytes32([]byte("foo")) + logsBloom := bytesutil.PadTo([]byte("logs"), fieldparams.LogsBloomLength) + receiptsRoot := bytesutil.PadTo([]byte("receiptsRoot"), fieldparams.RootLength) + parentHash := bytesutil.PadTo([]byte("parentHash"), fieldparams.RootLength) + g.payload = &enginev1.ExecutionPayloadDeneb{ + ParentHash: parentHash, + FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), + StateRoot: stateRoot, + ReceiptsRoot: receiptsRoot, + LogsBloom: logsBloom, + PrevRandao: blockHash[:], + BlockNumber: 0, + GasLimit: 0, + GasUsed: 0, + Timestamp: 0, + ExtraData: make([]byte, 0), + BaseFeePerGas: bytesutil.PadTo([]byte("baseFeePerGas"), fieldparams.RootLength), + BlockHash: blockHash[:], + Transactions: encodedBinaryTxs, + Withdrawals: make([]*enginev1.Withdrawal, 0), + BlobGasUsed: 0, + ExcessBlobGas: 0, + } + } + + block := NewBeaconBlockDeneb() + block.Block.Body.ExecutionPayload = g.payload + block.Block.Slot = g.slot + block.Block.ParentRoot = g.parent[:] + block.Block.ProposerIndex = g.proposer + commitments := make([][48]byte, g.nblobs) + block.Block.Body.BlobKzgCommitments = make([][]byte, g.nblobs) + for i := range commitments { + binary.LittleEndian.PutUint16(commitments[i][0:16], uint16(i)) + binary.LittleEndian.PutUint16(commitments[i][16:32], uint16(g.slot)) + block.Block.Body.BlobKzgCommitments[i] = commitments[i][:] + } + + body, err := blocks.NewBeaconBlockBody(block.Block.Body) + require.NoError(t, err) + inclusion := make([][][]byte, len(commitments)) + for i := range commitments { + proof, err := blocks.MerkleProofKZGCommitment(body, i) + require.NoError(t, err) + inclusion[i] = proof + } + if g.sign { + epoch := slots.ToEpoch(block.Block.Slot) + schedule := forks.NewOrderedSchedule(params.BeaconConfig()) + version, err := schedule.VersionForEpoch(epoch) + require.NoError(t, err) + fork, err := schedule.ForkFromVersion(version) + require.NoError(t, err) + domain := params.BeaconConfig().DomainBeaconProposer + sig, err := signing.ComputeDomainAndSignWithoutState(fork, epoch, domain, g.valRoot, block.Block, g.sk) + require.NoError(t, err) + block.Signature = sig + } + + root, err := block.Block.HashTreeRoot() + require.NoError(t, err) + + sidecars := make([]blocks.ROBlob, len(commitments)) + blobs := make([]kzg.Blob, len(commitments)) + sbb, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + + sh, err := sbb.Header() + require.NoError(t, err) + for i, c := range block.Block.Body.BlobKzgCommitments { + sidecars[i] = GenerateTestDenebBlobSidecar(t, root, sh, i, c, inclusion[i]) + blobs[i] = kzg.Blob(sidecars[i].BlobSidecar.Blob) + } + + rob, err := blocks.NewROBlock(sbb) + require.NoError(t, err) + + columns, err := peerdas.DataColumnSidecars(rob, blobs) + require.NoError(t, err) + roColumns := make([]blocks.RODataColumn, len(columns)) + for i, c := range columns { + roCol, err := blocks.NewRODataColumn(c) + require.NoError(t, err) + roColumns[i] = roCol + } + + return rob, roColumns +} + func fakeEmptyProof(_ *testing.T, _ *ethpb.BlobSidecar) [][]byte { r := make([][]byte, fieldparams.KzgCommitmentInclusionProofDepth) for i := range r {