Verify roblobs (#13245)

* scaffolding for verification package

* WIP blob verification methods

* lock wrapper for safer forkchoice sharing

* more solid cache and verification designs; adding tests

* more test coverage, adding missing cache files

* clearer func name

* remove forkchoice borrower (it's in another PR)

* revert temporary interface experiment

* lint

* nishant feedback

* add comments with spec text to all verifications

* some comments on public methods

* invert confusing verification name

* deep source

* remove cache from ProposerCache + gaz

* more consistently early return on error paths

* messed up the test with the wrong config value

* terence naming feedback

* tests on BeginsAt

* lint

* deep source...

* name errors after failure, not expectation

* deep sooource

* check len()==0 instead of nil so empty lists work

* update test for EIP-7044

---------

Co-authored-by: Kasey Kirkham <kasey@users.noreply.github.com>
This commit is contained in:
kasey 2023-12-06 20:36:25 -06:00 committed by GitHub
parent 4e4fb9ad52
commit 4008ea736f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1723 additions and 66 deletions

View File

@ -10,6 +10,7 @@ go_library(
importpath = "github.com/prysmaticlabs/prysm/v4/beacon-chain/blockchain/kzg", importpath = "github.com/prysmaticlabs/prysm/v4/beacon-chain/blockchain/kzg",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//consensus-types/blocks:go_default_library",
"//proto/prysm/v1alpha1:go_default_library", "//proto/prysm/v1alpha1:go_default_library",
"@com_github_crate_crypto_go_kzg_4844//:go_default_library", "@com_github_crate_crypto_go_kzg_4844//:go_default_library",
"@com_github_pkg_errors//:go_default_library", "@com_github_pkg_errors//:go_default_library",

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
GoKZG "github.com/crate-crypto/go-kzg-4844" GoKZG "github.com/crate-crypto/go-kzg-4844"
"github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
) )
@ -31,6 +32,11 @@ func IsDataAvailable(commitments [][]byte, sidecars []*ethpb.DeprecatedBlobSidec
return kzgContext.VerifyBlobKZGProofBatch(blobs, cmts, proofs) return kzgContext.VerifyBlobKZGProofBatch(blobs, cmts, proofs)
} }
// VerifyROBlobCommitment is a helper that massages the fields of an ROBlob into the types needed to call VerifyBlobKZGProof.
func VerifyROBlobCommitment(sc blocks.ROBlob) error {
return kzgContext.VerifyBlobKZGProof(bytesToBlob(sc.Blob), bytesToCommitment(sc.KzgCommitment), bytesToKZGProof(sc.KzgProof))
}
func bytesToBlob(blob []byte) (ret GoKZG.Blob) { func bytesToBlob(blob []byte) (ret GoKZG.Blob) {
copy(ret[:], blob) copy(ret[:], blob)
return return

View File

@ -17,6 +17,7 @@ import (
"github.com/prysmaticlabs/prysm/v4/testing/assert" "github.com/prysmaticlabs/prysm/v4/testing/assert"
"github.com/prysmaticlabs/prysm/v4/testing/require" "github.com/prysmaticlabs/prysm/v4/testing/require"
"github.com/prysmaticlabs/prysm/v4/testing/util" "github.com/prysmaticlabs/prysm/v4/testing/util"
"github.com/prysmaticlabs/prysm/v4/time/slots"
) )
func TestProcessVoluntaryExits_NotActiveLongEnoughToExit(t *testing.T) { func TestProcessVoluntaryExits_NotActiveLongEnoughToExit(t *testing.T) {
@ -134,6 +135,10 @@ func TestProcessVoluntaryExits_AppliesCorrectStatus(t *testing.T) {
} }
func TestVerifyExitAndSignature(t *testing.T) { func TestVerifyExitAndSignature(t *testing.T) {
undo := util.HackDenebMaxuint(t)
defer undo()
denebSlot, err := slots.EpochStart(params.BeaconConfig().DenebForkEpoch)
require.NoError(t, err)
tests := []struct { tests := []struct {
name string name string
setup func() (*ethpb.Validator, *ethpb.SignedVoluntaryExit, state.ReadOnlyBeaconState, error) setup func() (*ethpb.Validator, *ethpb.SignedVoluntaryExit, state.ReadOnlyBeaconState, error)
@ -241,11 +246,11 @@ func TestVerifyExitAndSignature(t *testing.T) {
fork := &ethpb.Fork{ fork := &ethpb.Fork{
PreviousVersion: params.BeaconConfig().CapellaForkVersion, PreviousVersion: params.BeaconConfig().CapellaForkVersion,
CurrentVersion: params.BeaconConfig().DenebForkVersion, CurrentVersion: params.BeaconConfig().DenebForkVersion,
Epoch: primitives.Epoch(2), Epoch: params.BeaconConfig().DenebForkEpoch,
} }
signedExit := &ethpb.SignedVoluntaryExit{ signedExit := &ethpb.SignedVoluntaryExit{
Exit: &ethpb.VoluntaryExit{ Exit: &ethpb.VoluntaryExit{
Epoch: 2, Epoch: params.BeaconConfig().CapellaForkEpoch,
ValidatorIndex: 0, ValidatorIndex: 0,
}, },
} }
@ -253,7 +258,7 @@ func TestVerifyExitAndSignature(t *testing.T) {
bs, err := state_native.InitializeFromProtoUnsafeDeneb(&ethpb.BeaconStateDeneb{ bs, err := state_native.InitializeFromProtoUnsafeDeneb(&ethpb.BeaconStateDeneb{
GenesisValidatorsRoot: bs.GenesisValidatorsRoot(), GenesisValidatorsRoot: bs.GenesisValidatorsRoot(),
Fork: fork, Fork: fork,
Slot: (params.BeaconConfig().SlotsPerEpoch * 2) + 1, Slot: denebSlot,
Validators: bs.Validators(), Validators: bs.Validators(),
}) })
if err != nil { if err != nil {

View File

@ -16,7 +16,6 @@ go_library(
"//crypto/bls:go_default_library", "//crypto/bls:go_default_library",
"//encoding/bytesutil:go_default_library", "//encoding/bytesutil:go_default_library",
"//proto/prysm/v1alpha1:go_default_library", "//proto/prysm/v1alpha1:go_default_library",
"//runtime/version:go_default_library",
"@com_github_pkg_errors//:go_default_library", "@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_fastssz//:go_default_library", "@com_github_prysmaticlabs_fastssz//:go_default_library",
], ],

View File

@ -11,7 +11,6 @@ import (
"github.com/prysmaticlabs/prysm/v4/crypto/bls" "github.com/prysmaticlabs/prysm/v4/crypto/bls"
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/runtime/version"
) )
// ForkVersionByteLength length of fork version byte array. // ForkVersionByteLength length of fork version byte array.
@ -57,18 +56,22 @@ const (
// ComputeDomainAndSign computes the domain and signing root and sign it using the passed in private key. // ComputeDomainAndSign computes the domain and signing root and sign it using the passed in private key.
func ComputeDomainAndSign(st state.ReadOnlyBeaconState, epoch primitives.Epoch, obj fssz.HashRoot, domain [4]byte, key bls.SecretKey) ([]byte, error) { func ComputeDomainAndSign(st state.ReadOnlyBeaconState, epoch primitives.Epoch, obj fssz.HashRoot, domain [4]byte, key bls.SecretKey) ([]byte, error) {
fork := st.Fork() return ComputeDomainAndSignWithoutState(st.Fork(), epoch, domain, st.GenesisValidatorsRoot(), obj, key)
}
// ComputeDomainAndSignWithoutState offers the same functionalit as ComputeDomainAndSign without the need to provide a BeaconState.
// This is particularly helpful for signing values in tests.
func ComputeDomainAndSignWithoutState(fork *ethpb.Fork, epoch primitives.Epoch, domain [4]byte, vr []byte, obj fssz.HashRoot, key bls.SecretKey) ([]byte, error) {
// EIP-7044: Beginning in Deneb, fix the fork version to Capella for signed exits. // EIP-7044: Beginning in Deneb, fix the fork version to Capella for signed exits.
// This allows for signed validator exits to be valid forever. // This allows for signed validator exits to be valid forever.
if st.Version() >= version.Deneb && domain == params.BeaconConfig().DomainVoluntaryExit { if domain == params.BeaconConfig().DomainVoluntaryExit && epoch >= params.BeaconConfig().DenebForkEpoch {
fork = &ethpb.Fork{ fork = &ethpb.Fork{
PreviousVersion: params.BeaconConfig().CapellaForkVersion, PreviousVersion: params.BeaconConfig().CapellaForkVersion,
CurrentVersion: params.BeaconConfig().CapellaForkVersion, CurrentVersion: params.BeaconConfig().CapellaForkVersion,
Epoch: params.BeaconConfig().CapellaForkEpoch, Epoch: params.BeaconConfig().CapellaForkEpoch,
} }
} }
d, err := Domain(fork, epoch, domain, vr)
d, err := Domain(fork, epoch, domain, st.GenesisValidatorsRoot())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -102,8 +105,14 @@ func Data(rootFunc func() ([32]byte, error), domain []byte) ([32]byte, error) {
if err != nil { if err != nil {
return [32]byte{}, err return [32]byte{}, err
} }
return ComputeSigningRootForRoot(objRoot, domain)
}
// ComputeSigningRootForRoot works the same as ComputeSigningRoot,
// except that gets the root from an argument instead of a callback.
func ComputeSigningRootForRoot(root [32]byte, domain []byte) ([32]byte, error) {
container := &ethpb.SigningData{ container := &ethpb.SigningData{
ObjectRoot: objRoot[:], ObjectRoot: root[:],
Domain: domain, Domain: domain,
} }
return container.HashTreeRoot() return container.HashTreeRoot()

View File

@ -41,6 +41,11 @@ func (g *Clock) CurrentSlot() types.Slot {
return slots.Duration(g.t, now) return slots.Duration(g.t, now)
} }
// SlotStart computes the time the given slot begins.
func (g *Clock) SlotStart(slot types.Slot) time.Time {
return slots.BeginsAt(slot, g.t)
}
// Now provides a value for time.Now() that can be overridden in tests. // Now provides a value for time.Now() that can be overridden in tests.
func (g *Clock) Now() time.Time { func (g *Clock) Now() time.Time {
return g.now() return g.now()

View File

@ -437,8 +437,8 @@ func TestConstructPendingBlobsRequest(t *testing.T) {
Signature: bytesutil.PadTo([]byte{}, 96), Signature: bytesutil.PadTo([]byte{}, 96),
} }
blobSidecars := []blocks.ROBlob{ blobSidecars := []blocks.ROBlob{
util.GenerateTestDenebBlobSidecar(t, root, header, 0, bytesutil.PadTo([]byte{}, 48)), util.GenerateTestDenebBlobSidecar(t, root, header, 0, bytesutil.PadTo([]byte{}, 48), make([][]byte, 0)),
util.GenerateTestDenebBlobSidecar(t, root, header, 2, bytesutil.PadTo([]byte{}, 48)), util.GenerateTestDenebBlobSidecar(t, root, header, 2, bytesutil.PadTo([]byte{}, 48), make([][]byte, 0)),
} }
vscs, err := verification.BlobSidecarSliceNoop(blobSidecars) vscs, err := verification.BlobSidecarSliceNoop(blobSidecars)
require.NoError(t, err) require.NoError(t, err)

View File

@ -482,8 +482,8 @@ func TestBlobValidatorFromRootReq(t *testing.T) {
validRoot := bytesutil.PadTo([]byte("valid"), 32) validRoot := bytesutil.PadTo([]byte("valid"), 32)
invalidRoot := bytesutil.PadTo([]byte("invalid"), 32) invalidRoot := bytesutil.PadTo([]byte("invalid"), 32)
header := &ethpb.SignedBeaconBlockHeader{} header := &ethpb.SignedBeaconBlockHeader{}
validb := util.GenerateTestDenebBlobSidecar(t, bytesutil.ToBytes32(validRoot), header, 0, []byte{}) validb := util.GenerateTestDenebBlobSidecar(t, bytesutil.ToBytes32(validRoot), header, 0, []byte{}, make([][]byte, 0))
invalidb := util.GenerateTestDenebBlobSidecar(t, bytesutil.ToBytes32(invalidRoot), header, 0, []byte{}) invalidb := util.GenerateTestDenebBlobSidecar(t, bytesutil.ToBytes32(invalidRoot), header, 0, []byte{}, make([][]byte, 0))
cases := []struct { cases := []struct {
name string name string
ids []*ethpb.BlobIdentifier ids []*ethpb.BlobIdentifier
@ -584,7 +584,7 @@ func TestBlobValidatorFromRangeReq(t *testing.T) {
header := &ethpb.SignedBeaconBlockHeader{ header := &ethpb.SignedBeaconBlockHeader{
Header: &ethpb.BeaconBlockHeader{Slot: c.responseSlot}, Header: &ethpb.BeaconBlockHeader{Slot: c.responseSlot},
} }
sc := util.GenerateTestDenebBlobSidecar(t, [32]byte{}, header, 0, []byte{}) sc := util.GenerateTestDenebBlobSidecar(t, [32]byte{}, header, 0, []byte{}, make([][]byte, 0))
err := vf(sc) err := vf(sc)
if c.err != nil { if c.err != nil {
require.ErrorIs(t, err, c.err) require.ErrorIs(t, err, c.err)

View File

@ -239,7 +239,7 @@ func TestValidateBlob_AlreadySeenInCache(t *testing.T) {
//_, scs := util.GenerateTestDenebBlockWithSidecar(t, r, chainService.CurrentSlot()+1, 1) //_, scs := util.GenerateTestDenebBlockWithSidecar(t, r, chainService.CurrentSlot()+1, 1)
header, err := signedBb.Header() header, err := signedBb.Header()
require.NoError(t, err) require.NoError(t, err)
sc := util.GenerateTestDenebBlobSidecar(t, r, header, 0, make([]byte, 48)) sc := util.GenerateTestDenebBlobSidecar(t, r, header, 0, make([]byte, 48), make([][]byte, 0))
b := sc.BlobSidecar b := sc.BlobSidecar
buf := new(bytes.Buffer) buf := new(bytes.Buffer)

View File

@ -1,9 +1,66 @@
load("@prysm//tools/go:def.bzl", "go_library") load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = ["fake.go"], srcs = [
"blob.go",
"cache.go",
"error.go",
"fake.go",
"initializer.go",
"result.go",
],
importpath = "github.com/prysmaticlabs/prysm/v4/beacon-chain/verification", importpath = "github.com/prysmaticlabs/prysm/v4/beacon-chain/verification",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = ["//consensus-types/blocks:go_default_library"], deps = [
"//beacon-chain/blockchain/kzg:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/core/transition:go_default_library",
"//beacon-chain/forkchoice/types:go_default_library",
"//beacon-chain/startup:go_default_library",
"//beacon-chain/state:go_default_library",
"//cache/lru:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/blocks:go_default_library",
"//consensus-types/interfaces:go_default_library",
"//consensus-types/primitives:go_default_library",
"//crypto/bls:go_default_library",
"//encoding/bytesutil:go_default_library",
"//network/forks:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//runtime/logging:go_default_library",
"//time/slots:go_default_library",
"@com_github_hashicorp_golang_lru//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"blob_test.go",
"cache_test.go",
],
embed = [":go_default_library"],
deps = [
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/db:go_default_library",
"//beacon-chain/forkchoice/types:go_default_library",
"//beacon-chain/startup:go_default_library",
"//beacon-chain/state:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/blocks:go_default_library",
"//consensus-types/primitives:go_default_library",
"//crypto/bls:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//runtime/interop:go_default_library",
"//testing/require:go_default_library",
"//testing/util:go_default_library",
"//time/slots:go_default_library",
"@com_github_pkg_errors//:go_default_library",
],
) )

View File

@ -0,0 +1,296 @@
package verification
import (
"context"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state"
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
"github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
"github.com/prysmaticlabs/prysm/v4/runtime/logging"
"github.com/prysmaticlabs/prysm/v4/time/slots"
log "github.com/sirupsen/logrus"
)
const (
RequireBlobIndexInBounds Requirement = iota
RequireSlotNotTooEarly
RequireSlotAboveFinalized
RequireValidProposerSignature
RequireSidecarParentSeen
RequireSidecarParentValid
RequireSidecarParentSlotLower
RequireSidecarDescendsFromFinalized
RequireSidecarInclusionProven
RequireSidecarKzgProofVerified
RequireSidecarProposerExpected
)
// GossipSidecarRequirements defines the set of requirements that BlobSidecars received on gossip
// must satisfy in order to upgrade an ROBlob to a VerifiedROBlob.
var GossipSidecarRequirements = []Requirement{
RequireBlobIndexInBounds,
RequireSlotNotTooEarly,
RequireSlotAboveFinalized,
RequireValidProposerSignature,
RequireSidecarParentSeen,
RequireSidecarParentValid,
RequireSidecarParentSlotLower,
RequireSidecarDescendsFromFinalized,
RequireSidecarInclusionProven,
RequireSidecarKzgProofVerified,
RequireSidecarProposerExpected,
}
var (
ErrBlobInvalid = errors.New("blob failed verification")
// ErrBlobIndexInvalid means RequireBlobIndexInBounds failed.
ErrBlobIndexInvalid = errors.Wrap(ErrBlobInvalid, "incorrect blob sidecar index")
// ErrSlotTooEarly means RequireSlotNotTooEarly failed.
ErrSlotTooEarly = 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")
)
type BlobVerifier struct {
*sharedResources
results *results
blob blocks.ROBlob
parent state.BeaconState
verifyBlobCommitment roblobCommitmentVerifier
}
type roblobCommitmentVerifier func(blocks.ROBlob) error
// VerifiedROBlob "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 (bv *BlobVerifier) VerifiedROBlob() (blocks.VerifiedROBlob, error) {
if bv.results.allSatisfied() {
return blocks.NewVerifiedROBlob(bv.blob), nil
}
return blocks.VerifiedROBlob{}, bv.results.errors(ErrBlobInvalid)
}
func (bv *BlobVerifier) recordResult(req Requirement, err *error) {
if err == nil || *err == nil {
bv.results.record(req, nil)
return
}
bv.results.record(req, *err)
}
// BlobIndexInBounds represents the follow spec verification:
// [REJECT] The sidecar's index is consistent with MAX_BLOBS_PER_BLOCK -- i.e. blob_sidecar.index < MAX_BLOBS_PER_BLOCK.
func (bv *BlobVerifier) 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 nil
}
// SlotNotTooEarly 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 (bv *BlobVerifier) SlotNotTooEarly() (err error) {
defer bv.recordResult(RequireSlotNotTooEarly, &err)
if bv.clock.CurrentSlot() == bv.blob.Slot() {
return nil
}
// subtract the max clock disparity from the start slot time
validAfter := bv.clock.SlotStart(bv.blob.Slot()).Add(-1 * params.BeaconNetworkConfig().MaximumGossipClockDisparity)
// If the difference between now and gt is greater than maximum clock disparity, the block is too far in the future.
if bv.clock.Now().Before(validAfter) {
return ErrSlotTooEarly
}
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 (bv *BlobVerifier) SlotAboveFinalized() (err error) {
defer bv.recordResult(RequireSlotAboveFinalized, &err)
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())
}
if bv.blob.Slot() <= fSlot {
return ErrSlotNotAfterFinalized
}
return nil
}
// ValidProposerSignature represents the spec verification:
// [REJECT] The proposer signature of blob_sidecar.signed_block_header,
// is valid with respect to the block_header.proposer_index pubkey.
func (bv *BlobVerifier) ValidProposerSignature(ctx context.Context) (err error) {
defer bv.recordResult(RequireValidProposerSignature, &err)
sd := blobToSignatureData(bv.blob)
// First check if there is a cached verification that can be reused.
seen, err := bv.sc.SignatureVerified(sd)
if seen {
if err != nil {
log.WithFields(logging.BlobFields(bv.blob)).WithError(err).Debug("reusing failed proposer signature validation from cache")
return ErrInvalidProposerSignature
}
return nil
}
// retrieve the parent state to fallback to full verification
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
}
// 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 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 (bv *BlobVerifier) SidecarParentSeen(badParent func([32]byte) bool) (err error) {
defer bv.recordResult(RequireSidecarParentSeen, &err)
if bv.fc.HasNode(bv.blob.ParentRoot()) {
return nil
}
if badParent != nil && badParent(bv.blob.ParentRoot()) {
return nil
}
return ErrSidecarParentNotSeen
}
// SidecarParentValid represents the spec verification:
// [REJECT] The sidecar's block's parent (defined by block_header.parent_root) passes validation.
func (bv *BlobVerifier) SidecarParentValid(badParent func([32]byte) bool) (err error) {
defer bv.recordResult(RequireSidecarParentValid, &err)
if badParent != nil && badParent(bv.blob.ParentRoot()) {
return 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 (bv *BlobVerifier) 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")
}
if parentSlot >= bv.blob.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 (bv *BlobVerifier) SidecarDescendsFromFinalized() (err error) {
defer bv.recordResult(RequireSidecarDescendsFromFinalized, &err)
if !bv.fc.IsCanonical(bv.blob.ParentRoot()) {
return ErrSidecarNotFinalizedDescendent
}
return nil
}
// SidecarInclusionProven represents the spec verification:
// [REJECT] The sidecar's inclusion proof is valid as verified by verify_blob_sidecar_inclusion_proof(blob_sidecar).
func (bv *BlobVerifier) 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 nil
}
// SidecarKzgProofVerified represents the spec verification:
// [REJECT] The sidecar's blob is valid as verified by
// verify_blob_kzg_proof(blob_sidecar.blob, blob_sidecar.kzg_commitment, blob_sidecar.kzg_proof).
func (bv *BlobVerifier) 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 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 (bv *BlobVerifier) SidecarProposerExpected(ctx context.Context) (err error) {
defer bv.recordResult(RequireSidecarProposerExpected, &err)
idx, cached := bv.pc.Proposer(bv.blob.ParentRoot(), bv.blob.Slot())
if !cached {
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
}
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
}
}
if idx != bv.blob.ProposerIndex() {
log.WithError(ErrSidecarUnexpectedProposer).
WithFields(logging.BlobFields(bv.blob)).WithField("expected_proposer", idx).
Debug("unexpected blob proposer")
return ErrSidecarUnexpectedProposer
}
return nil
}
func (bv *BlobVerifier) parentState(ctx context.Context) (state.BeaconState, error) {
if bv.parent != nil {
return bv.parent, nil
}
st, err := bv.sr.StateByRoot(ctx, bv.blob.ParentRoot())
if err != nil {
return nil, err
}
bv.parent = st
return bv.parent, nil
}
func blobToSignatureData(b blocks.ROBlob) SignatureData {
return SignatureData{
Root: b.BlockRoot(),
Parent: b.ParentRoot(),
Signature: bytesutil.ToBytes96(b.SignedBlockHeader.Signature),
Proposer: b.ProposerIndex(),
Slot: b.Slot(),
}
}

View File

@ -0,0 +1,660 @@
package verification
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/db"
forkchoicetypes "github.com/prysmaticlabs/prysm/v4/beacon-chain/forkchoice/types"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/startup"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state"
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
"github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/testing/require"
"github.com/prysmaticlabs/prysm/v4/testing/util"
"github.com/prysmaticlabs/prysm/v4/time/slots"
)
func TestBlobIndexInBounds(t *testing.T) {
ini := &Initializer{}
_, 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...)
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...)
require.ErrorIs(t, v.BlobIndexInBounds(), ErrBlobIndexInvalid)
require.Equal(t, true, v.results.executed(RequireBlobIndexInBounds))
require.NotNil(t, v.results.result(RequireBlobIndexInBounds))
}
func TestSlotNotTooEarly(t *testing.T) {
now := time.Now()
// make genesis 1 slot in the past
genesis := now.Add(-1 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second)
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, 1)
b := blobs[0]
// slot 1 should be 12 seconds after genesis
b.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.NewBlobVerifier(b, GossipSidecarRequirements...)
require.NoError(t, v.SlotNotTooEarly())
require.Equal(t, true, v.results.executed(RequireSlotNotTooEarly))
require.NoError(t, v.results.result(RequireSlotNotTooEarly))
// 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.BeaconNetworkConfig().MaximumGossipClockDisparity / 2) }))
ini = Initializer{shared: &sharedResources{clock: closeClock}}
v = ini.NewBlobVerifier(b, GossipSidecarRequirements...)
require.NoError(t, v.SlotNotTooEarly())
// 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.BeaconNetworkConfig().MaximumGossipClockDisparity)
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...)
require.ErrorIs(t, v.SlotNotTooEarly(), ErrSlotTooEarly)
require.Equal(t, true, v.results.executed(RequireSlotNotTooEarly))
require.NotNil(t, v.results.result(RequireSlotNotTooEarly))
}
func TestSlotAboveFinalized(t *testing.T) {
ini := &Initializer{shared: &sharedResources{}}
cases := []struct {
name string
slot primitives.Slot
finalizedSlot primitives.Slot
err error
}{
{
name: "finalized epoch < blob epoch",
slot: 32,
},
{
name: "finalized slot < blob slot (same epoch)",
slot: 31,
},
{
name: "finalized epoch > blob epoch",
finalizedSlot: 32,
err: ErrSlotNotAfterFinalized,
},
{
name: "finalized slot == blob 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}
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, 1)
b := blobs[0]
b.SignedBlockHeader.Header.Slot = c.slot
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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 TestValidProposerSignature_Cached(t *testing.T) {
ctx := context.Background()
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, 1)
b := blobs[0]
expectedSd := blobToSignatureData(b)
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.NewBlobVerifier(b, GossipSidecarRequirements...)
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.NewBlobVerifier(b, GossipSidecarRequirements...)
require.ErrorIs(t, v.ValidProposerSignature(ctx), ErrInvalidProposerSignature)
require.Equal(t, true, v.results.executed(RequireValidProposerSignature))
require.NotNil(t, v.results.result(RequireValidProposerSignature))
}
func TestValidProposerSignature_CacheMiss(t *testing.T) {
ctx := context.Background()
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, 1)
b := blobs[0]
expectedSd := blobToSignatureData(b)
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(b.ProposerIndex(), &ethpb.Validator{})}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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...)
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(b.ProposerIndex(), &ethpb.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.NewBlobVerifier(b, GossipSidecarRequirements...)
// 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 badParentCb(t *testing.T, expected [32]byte, e bool) func([32]byte) bool {
return func(r [32]byte) bool {
if expected != r {
t.Error("badParent callback did not receive expected root")
}
return e
}
}
func TestSidecarParentSeen(t *testing.T) {
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, 1)
b := blobs[0]
fcHas := &mockForkchoicer{
HasNodeCB: func(parent [32]byte) bool {
if parent != b.ParentRoot() {
t.Error("forkchoice.HasNode called with unexpected parent root")
}
return true
},
}
fcLacks := &mockForkchoicer{
HasNodeCB: func(parent [32]byte) bool {
if parent != b.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.NewBlobVerifier(b, GossipSidecarRequirements...)
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...)
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.NewBlobVerifier(b, GossipSidecarRequirements...)
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...)
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))
})
}
func TestSidecarParentValid(t *testing.T) {
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, 1)
b := blobs[0]
t.Run("parent valid", func(t *testing.T) {
ini := Initializer{shared: &sharedResources{}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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...)
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))
})
}
func TestSidecarParentSlotLower(t *testing.T) {
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 1, 1)
b := blobs[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: b.Slot() - 1,
},
{
name: "in fc, slot equal",
fcSlot: b.Slot(),
err: ErrSlotNotAfterParent,
},
{
name: "in fc, slot higher",
fcSlot: b.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 b.ParentRoot() != r {
t.Error("forkchoice.Slot called with unexpected parent root")
}
return c.fcSlot, c.fcErr
}}}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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 TestSidecarDescendsFromFinalized(t *testing.T) {
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 1, 1)
b := blobs[0]
t.Run("not canonical", func(t *testing.T) {
ini := Initializer{shared: &sharedResources{fc: &mockForkchoicer{IsCanonicalCB: func(r [32]byte) bool {
if b.ParentRoot() != r {
t.Error("forkchoice.Slot called with unexpected parent root")
}
return false
}}}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
require.ErrorIs(t, v.SidecarDescendsFromFinalized(), ErrSidecarNotFinalizedDescendent)
require.Equal(t, true, v.results.executed(RequireSidecarDescendsFromFinalized))
require.NotNil(t, v.results.result(RequireSidecarDescendsFromFinalized))
})
t.Run("not canonical", func(t *testing.T) {
ini := Initializer{shared: &sharedResources{fc: &mockForkchoicer{IsCanonicalCB: func(r [32]byte) bool {
if b.ParentRoot() != r {
t.Error("forkchoice.Slot called with unexpected parent root")
}
return true
}}}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
require.NoError(t, v.SidecarDescendsFromFinalized())
require.Equal(t, true, v.results.executed(RequireSidecarDescendsFromFinalized))
require.NoError(t, v.results.result(RequireSidecarDescendsFromFinalized))
})
}
func TestSidecarInclusionProven(t *testing.T) {
// GenerateTestDenebBlockWithSidecar is supposed to generate valid inclusion proofs
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 1, 1)
b := blobs[0]
ini := Initializer{}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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 := b.SignedBlockHeader.Header.BodyRoot[0]
b.SignedBlockHeader.Header.BodyRoot[0] = byte0 ^ 255
v = ini.NewBlobVerifier(b, GossipSidecarRequirements...)
require.ErrorIs(t, v.SidecarInclusionProven(), ErrSidecarInclusionProofInvalid)
require.Equal(t, true, v.results.executed(RequireSidecarInclusionProven))
require.NotNil(t, v.results.result(RequireSidecarInclusionProven))
}
func TestSidecarKzgProofVerified(t *testing.T) {
// GenerateTestDenebBlockWithSidecar is supposed to generate valid commitments
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 1, 1)
b := blobs[0]
passes := func(vb blocks.ROBlob) error {
require.Equal(t, true, bytes.Equal(b.KzgCommitment, vb.KzgCommitment))
return nil
}
v := &BlobVerifier{verifyBlobCommitment: passes, results: newResults(), blob: b}
require.NoError(t, v.SidecarKzgProofVerified())
require.Equal(t, true, v.results.executed(RequireSidecarKzgProofVerified))
require.NoError(t, v.results.result(RequireSidecarKzgProofVerified))
fails := func(vb blocks.ROBlob) error {
require.Equal(t, true, bytes.Equal(b.KzgCommitment, vb.KzgCommitment))
return errors.New("bad blob")
}
v = &BlobVerifier{verifyBlobCommitment: fails, results: newResults(), blob: b}
require.ErrorIs(t, v.SidecarKzgProofVerified(), ErrSidecarKzgProofInvalid)
require.Equal(t, true, v.results.executed(RequireSidecarKzgProofVerified))
require.NotNil(t, v.results.result(RequireSidecarKzgProofVerified))
}
func TestSidecarProposerExpected(t *testing.T) {
ctx := context.Background()
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 1, 1)
b := blobs[0]
t.Run("cached, matches", func(t *testing.T) {
ini := Initializer{shared: &sharedResources{pc: &mockProposerCache{ProposerCB: pcReturnsIdx(b.ProposerIndex())}}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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)}}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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()}}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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, b.ParentRoot(), root)
require.Equal(t, b.Slot(), slot)
return b.ProposerIndex(), nil
},
}
ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(b.ProposerIndex(), &ethpb.Validator{}), pc: pc}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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, b.ParentRoot(), root)
require.Equal(t, b.Slot(), slot)
return b.ProposerIndex() + 1, nil
},
}
ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(b.ProposerIndex(), &ethpb.Validator{}), pc: pc}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
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, b.ParentRoot(), root)
require.Equal(t, b.Slot(), slot)
return 0, errors.New("ComputeProposer failed")
},
}
ini := Initializer{shared: &sharedResources{sr: sbrForValOverride(b.ProposerIndex(), &ethpb.Validator{}), pc: pc}}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
require.ErrorIs(t, v.SidecarProposerExpected(ctx), ErrSidecarUnexpectedProposer)
require.Equal(t, true, v.results.executed(RequireSidecarProposerExpected))
require.NotNil(t, v.results.result(RequireSidecarProposerExpected))
})
}
func TestRequirementSatisfaction(t *testing.T) {
_, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 1, 1)
b := blobs[0]
ini := Initializer{}
v := ini.NewBlobVerifier(b, GossipSidecarRequirements...)
_, err := v.VerifiedROBlob()
require.ErrorIs(t, err, ErrBlobInvalid)
me, ok := err.(VerificationMultiError)
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 GossipSidecarRequirements {
v.results.record(r, nil)
}
require.Equal(t, true, v.results.allSatisfied())
_, err = v.VerifiedROBlob()
require.NoError(t, err)
}
type mockForkchoicer struct {
FinalizedCheckpointCB func() *forkchoicetypes.Checkpoint
HasNodeCB func([32]byte) bool
IsCanonicalCB func(root [32]byte) bool
SlotCB func([32]byte) (primitives.Slot, error)
}
var _ Forkchoicer = &mockForkchoicer{}
func (m *mockForkchoicer) FinalizedCheckpoint() *forkchoicetypes.Checkpoint {
return m.FinalizedCheckpointCB()
}
func (m *mockForkchoicer) HasNode(root [32]byte) bool {
return m.HasNodeCB(root)
}
func (m *mockForkchoicer) IsCanonical(root [32]byte) bool {
return m.IsCanonicalCB(root)
}
func (m *mockForkchoicer) Slot(root [32]byte) (primitives.Slot, error) {
return m.SlotCB(root)
}
type mockSignatureCache struct {
svCalledForSig map[SignatureData]bool
svcb func(sig SignatureData) (bool, error)
vsCalledForSig map[SignatureData]bool
vscb func(sig SignatureData, v ValidatorAtIndexer) (err error)
}
// SignatureVerified implements SignatureCache.
func (m *mockSignatureCache) SignatureVerified(sig SignatureData) (bool, error) {
if m.svCalledForSig == nil {
m.svCalledForSig = make(map[SignatureData]bool)
}
m.svCalledForSig[sig] = true
return m.svcb(sig)
}
// VerifySignature implements SignatureCache.
func (m *mockSignatureCache) VerifySignature(sig SignatureData, v ValidatorAtIndexer) (err error) {
if m.vsCalledForSig == nil {
m.vsCalledForSig = make(map[SignatureData]bool)
}
m.vsCalledForSig[sig] = true
return m.vscb(sig, v)
}
var _ SignatureCache = &mockSignatureCache{}
type sbrfunc func(context.Context, [32]byte) (state.BeaconState, error)
type mockStateByRooter struct {
sbr sbrfunc
calledForRoot map[[32]byte]bool
}
func (sbr *mockStateByRooter) StateByRoot(ctx context.Context, root [32]byte) (state.BeaconState, error) {
if sbr.calledForRoot == nil {
sbr.calledForRoot = make(map[[32]byte]bool)
}
sbr.calledForRoot[root] = true
return sbr.sbr(ctx, root)
}
var _ StateByRooter = &mockStateByRooter{}
func sbrErrorIfCalled(t *testing.T) sbrfunc {
return func(_ context.Context, _ [32]byte) (state.BeaconState, error) {
t.Error("StateByRoot should not have been called")
return nil, nil
}
}
func sbrNotFound(t *testing.T, expectedRoot [32]byte) *mockStateByRooter {
return &mockStateByRooter{sbr: func(_ context.Context, parent [32]byte) (state.BeaconState, error) {
if parent != expectedRoot {
t.Errorf("did not receive expected root in StateByRootCall, want %#x got %#x", expectedRoot, parent)
}
return nil, db.ErrNotFound
}}
}
func sbrForValOverride(idx primitives.ValidatorIndex, val *ethpb.Validator) *mockStateByRooter {
return &mockStateByRooter{sbr: func(_ context.Context, root [32]byte) (state.BeaconState, error) {
return &validxStateOverride{vals: map[primitives.ValidatorIndex]*ethpb.Validator{
idx: val,
}}, nil
}}
}
type validxStateOverride struct {
state.BeaconState
vals map[primitives.ValidatorIndex]*ethpb.Validator
}
var _ state.BeaconState = &validxStateOverride{}
func (v *validxStateOverride) ValidatorAtIndex(idx primitives.ValidatorIndex) (*ethpb.Validator, error) {
val, ok := v.vals[idx]
if !ok {
return nil, fmt.Errorf("validxStateOverride does not know index %d", idx)
}
return val, nil
}
type mockProposerCache struct {
ComputeProposerCB func(ctx context.Context, root [32]byte, slot primitives.Slot, pst state.BeaconState) (primitives.ValidatorIndex, error)
ProposerCB func(root [32]byte, slot primitives.Slot) (primitives.ValidatorIndex, bool)
}
func (p *mockProposerCache) ComputeProposer(ctx context.Context, root [32]byte, slot primitives.Slot, pst state.BeaconState) (primitives.ValidatorIndex, error) {
return p.ComputeProposerCB(ctx, root, slot, pst)
}
func (p *mockProposerCache) Proposer(root [32]byte, slot primitives.Slot) (primitives.ValidatorIndex, bool) {
return p.ProposerCB(root, slot)
}
var _ ProposerCache = &mockProposerCache{}
func pcReturnsIdx(idx primitives.ValidatorIndex) func(root [32]byte, slot primitives.Slot) (primitives.ValidatorIndex, bool) {
return func(root [32]byte, slot primitives.Slot) (primitives.ValidatorIndex, bool) {
return idx, true
}
}
func pcReturnsNotFound() func(root [32]byte, slot primitives.Slot) (primitives.ValidatorIndex, bool) {
return func(root [32]byte, slot primitives.Slot) (primitives.ValidatorIndex, bool) {
return 0, false
}
}

View File

@ -0,0 +1,169 @@
package verification
import (
"context"
"fmt"
lru "github.com/hashicorp/golang-lru"
lruwrpr "github.com/prysmaticlabs/prysm/v4/cache/lru"
log "github.com/sirupsen/logrus"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/signing"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/transition"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state"
"github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v4/crypto/bls"
"github.com/prysmaticlabs/prysm/v4/network/forks"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/time/slots"
)
const (
DefaultSignatureCacheSize = 256
)
// ValidatorAtIndexer defines the method needed to retrieve a validator by its index.
// This interface is satisfied by state.BeaconState, but can also be satisfied by a cache.
type ValidatorAtIndexer interface {
ValidatorAtIndex(idx primitives.ValidatorIndex) (*ethpb.Validator, error)
}
// SignatureCache represents a type that can perform signature verification and cache the result so that it
// can be used when the same signature is seen in multiple places, like a SignedBeaconBlockHeader
// found in multiple BlobSidecars.
type SignatureCache interface {
// VerifySignature perform signature verification and caches the result.
VerifySignature(sig SignatureData, v ValidatorAtIndexer) (err error)
// SignatureVerified accesses the result of a previous signature verification.
SignatureVerified(sig SignatureData) (bool, error)
}
// SignatureData represents the set of parameters that together uniquely identify a signature observed on
// a beacon block. This is used as the key for the signature cache.
type SignatureData struct {
Root [32]byte
Parent [32]byte
Signature [96]byte
Proposer primitives.ValidatorIndex
Slot primitives.Slot
}
func (d SignatureData) logFields() log.Fields {
return log.Fields{
"root": fmt.Sprintf("%#x", d.Root),
"parent_root": fmt.Sprintf("%#x", d.Parent),
"signature": fmt.Sprintf("%#x", d.Signature),
"proposer": d.Proposer,
"slot": d.Slot,
}
}
func newSigCache(vr []byte, size int) *sigCache {
return &sigCache{Cache: lruwrpr.New(size), valRoot: vr}
}
type sigCache struct {
*lru.Cache
valRoot []byte
}
// VerifySignature verifies the given signature data against the key obtained via ValidatorAtIndexer.
func (c *sigCache) VerifySignature(sig SignatureData, v ValidatorAtIndexer) (err error) {
defer func() {
if err == nil {
c.Add(sig, true)
} else {
log.WithError(err).WithFields(sig.logFields()).Debug("caching failed signature verification result")
c.Add(sig, false)
}
}()
e := slots.ToEpoch(sig.Slot)
fork, err := forks.Fork(e)
if err != nil {
return err
}
domain, err := signing.Domain(fork, e, params.BeaconConfig().DomainBeaconProposer, c.valRoot)
if err != nil {
return err
}
pv, err := v.ValidatorAtIndex(sig.Proposer)
if err != nil {
return err
}
pb, err := bls.PublicKeyFromBytes(pv.PublicKey)
if err != nil {
return err
}
s, err := bls.SignatureFromBytes(sig.Signature[:])
if err != nil {
return err
}
sr, err := signing.ComputeSigningRootForRoot(sig.Root, domain)
if err != nil {
return err
}
if !s.Verify(pb, sr[:]) {
return signing.ErrSigFailedToVerify
}
return nil
}
// SignatureVerified checks the signature cache for the given key, and returns a boolean value of true
// if it has been seen before, and an error value indicating whether the signature verification succeeded.
// ie only a result of (true, nil) means a previous signature check passed.
func (c *sigCache) SignatureVerified(sig SignatureData) (bool, error) {
val, seen := c.Get(sig)
if !seen {
return false, nil
}
verified, ok := val.(bool)
if !ok {
log.WithFields(sig.logFields()).Debug("ignoring invalid value found in signature cache")
// This shouldn't happen, and if it does, the caller should treat it as a cache miss and run verification
// again to correctly populate the cache key.
return false, nil
}
if verified {
return true, nil
}
return true, signing.ErrSigFailedToVerify
}
// ProposerCache represents a type that can compute the proposer for a given slot + parent root,
// and cache the result so that it can be reused when the same verification needs to be performed
// across multiple values.
type ProposerCache interface {
ComputeProposer(ctx context.Context, root [32]byte, slot primitives.Slot, pst state.BeaconState) (primitives.ValidatorIndex, error)
Proposer(root [32]byte, slot primitives.Slot) (primitives.ValidatorIndex, bool)
}
func newPropCache() *propCache {
return &propCache{}
}
type propCache struct {
}
// ComputeProposer takes the state for the given parent root and slot and computes the proposer index, updating the
// proposer index cache when successful.
func (*propCache) ComputeProposer(ctx context.Context, parent [32]byte, slot primitives.Slot, pst state.BeaconState) (primitives.ValidatorIndex, error) {
pst, err := transition.ProcessSlotsUsingNextSlotCache(ctx, pst, parent[:], slot)
if err != nil {
return 0, err
}
idx, err := helpers.BeaconProposerIndex(ctx, pst)
if err != nil {
return 0, err
}
return idx, nil
}
// Proposer returns the validator index if it is found in the cache, along with a boolean indicating
// whether the value was present, similar to accessing an lru or go map.
func (*propCache) Proposer(_ [32]byte, _ primitives.Slot) (primitives.ValidatorIndex, bool) {
// TODO: replace with potuz' proposer id cache
return 0, false
}

View File

@ -0,0 +1,119 @@
package verification
import (
"context"
"testing"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/signing"
"github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v4/crypto/bls"
eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/runtime/interop"
"github.com/prysmaticlabs/prysm/v4/testing/require"
"github.com/prysmaticlabs/prysm/v4/testing/util"
)
func testSignedBlockBlobKeys(t *testing.T, valRoot []byte, slot primitives.Slot, nblobs int) (blocks.ROBlock, []blocks.ROBlob, bls.SecretKey, bls.PublicKey) {
sks, pks, err := interop.DeterministicallyGenerateKeys(0, 1)
require.NoError(t, err)
block, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, slot, nblobs, util.WithProposerSigning(0, sks[0], pks[0], valRoot))
return block, blobs, sks[0], pks[0]
}
func TestVerifySignature(t *testing.T) {
valRoot := [32]byte{}
_, blobs, _, pk := testSignedBlockBlobKeys(t, valRoot[:], 0, 1)
b := blobs[0]
sc := newSigCache(valRoot[:], 1)
cb := func(idx primitives.ValidatorIndex) (*eth.Validator, error) {
return &eth.Validator{PublicKey: pk.Marshal()}, nil
}
mv := &mockValidatorAtIndexer{cb: cb}
sd := blobToSignatureData(b)
require.NoError(t, sc.VerifySignature(sd, mv))
}
func TestSignatureCacheMissThenHit(t *testing.T) {
valRoot := [32]byte{}
_, blobs, _, pk := testSignedBlockBlobKeys(t, valRoot[:], 0, 1)
b := blobs[0]
sc := newSigCache(valRoot[:], 1)
cb := func(idx primitives.ValidatorIndex) (*eth.Validator, error) {
return &eth.Validator{PublicKey: pk.Marshal()}, nil
}
sd := blobToSignatureData(b)
cached, err := sc.SignatureVerified(sd)
// Should not be cached yet.
require.Equal(t, false, cached)
require.NoError(t, err)
mv := &mockValidatorAtIndexer{cb: cb}
require.NoError(t, sc.VerifySignature(sd, mv))
// Now it should be cached.
cached, err = sc.SignatureVerified(sd)
require.Equal(t, true, cached)
require.NoError(t, err)
// note the changed slot, which will give this blob a different cache key
_, blobs, _, _ = testSignedBlockBlobKeys(t, valRoot[:], 1, 1)
badSd := blobToSignatureData(blobs[0])
// new value, should not be cached
cached, err = sc.SignatureVerified(badSd)
require.Equal(t, false, cached)
require.NoError(t, err)
// note that the first argument is incremented, so it will be a different deterministic key
_, pks, err := interop.DeterministicallyGenerateKeys(1, 1)
require.NoError(t, err)
wrongKey := pks[0]
cb = func(idx primitives.ValidatorIndex) (*eth.Validator, error) {
return &eth.Validator{PublicKey: wrongKey.Marshal()}, nil
}
mv = &mockValidatorAtIndexer{cb: cb}
require.ErrorIs(t, sc.VerifySignature(badSd, mv), signing.ErrSigFailedToVerify)
// we should now get the failure error from the cache
cached, err = sc.SignatureVerified(badSd)
require.Equal(t, true, cached)
require.ErrorIs(t, err, signing.ErrSigFailedToVerify)
}
type mockValidatorAtIndexer struct {
cb func(idx primitives.ValidatorIndex) (*eth.Validator, error)
}
// ValidatorAtIndex implements ValidatorAtIndexer.
func (m *mockValidatorAtIndexer) ValidatorAtIndex(idx primitives.ValidatorIndex) (*eth.Validator, error) {
return m.cb(idx)
}
var _ ValidatorAtIndexer = &mockValidatorAtIndexer{}
func TestProposerCache(t *testing.T) {
ctx := context.Background()
// 3 validators because that was the first number that produced a non-zero proposer index by default
st, _ := util.DeterministicGenesisStateDeneb(t, 3)
pc := newPropCache()
_, cached := pc.Proposer([32]byte{}, 1)
// should not be cached yet
require.Equal(t, false, cached)
// If this test breaks due to changes in the determinstic state gen, just replace '2' with whatever the right index is.
expectedIdx := 2
idx, err := pc.ComputeProposer(ctx, [32]byte{}, 1, st)
require.NoError(t, err)
require.Equal(t, primitives.ValidatorIndex(expectedIdx), idx)
idx, cached = pc.Proposer([32]byte{}, 1)
// TODO: update this test when we integrate a proposer id cache
require.Equal(t, false, cached)
require.Equal(t, primitives.ValidatorIndex(0), idx)
}

View File

@ -0,0 +1,32 @@
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")
// VerificationMultiError is a custom error that can be used to access individual verification failures.
type VerificationMultiError struct {
r *results
err error
}
// Unwrap is used by errors.Is to unwrap errors.
func (ve VerificationMultiError) Unwrap() error {
return ve.err
}
// Error satisfies the standard error interface.
func (ve VerificationMultiError) Error() string {
return ve.err.Error()
}
// Failures provides access to map of Requirements->error messages
// so that calling code can introspect on what went wrong.
func (ve VerificationMultiError) Failures() map[Requirement]error {
return ve.r.failures()
}
func newVerificationMultiError(r *results, err error) VerificationMultiError {
return VerificationMultiError{r: r, err: err}
}

View File

@ -0,0 +1,108 @@
package verification
import (
"context"
"sync"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/blockchain/kzg"
forkchoicetypes "github.com/prysmaticlabs/prysm/v4/beacon-chain/forkchoice/types"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/startup"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state"
"github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
"github.com/prysmaticlabs/prysm/v4/consensus-types/interfaces"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
)
// Database represents the db methods that the verifiers need.
type Database interface {
Block(ctx context.Context, blockRoot [32]byte) (interfaces.ReadOnlySignedBeaconBlock, error)
}
// Forkchoicer represents the forkchoice methods that the verifiers need.
// Note that forkchoice is used here in a lock-free fashion, assuming that a version of forkchoice
// is given that internally handles the details of locking the underlying store.
type Forkchoicer interface {
FinalizedCheckpoint() *forkchoicetypes.Checkpoint
HasNode([32]byte) bool
IsCanonical(root [32]byte) bool
Slot([32]byte) (primitives.Slot, error)
}
// StateByRooter describes a stategen-ish type that can produce arbitrary states by their root
type StateByRooter interface {
StateByRoot(ctx context.Context, blockRoot [32]byte) (state.BeaconState, error)
}
// sharedResources provides access to resources that are required by different verification types.
// for example, sidecar verification and block verification share the block signature verification cache.
type sharedResources struct {
clock *startup.Clock
fc Forkchoicer
sc SignatureCache
pc ProposerCache
db Database
sr StateByRooter
}
// Initializer is used to create different Verifiers.
// Verifiers require access to stateful data structures, like caches,
// and it is Initializer's job to provides access to those.
type Initializer struct {
shared *sharedResources
}
// NewBlobVerifier creates a BlobVerifier for a single blob, with the given set of requirements.
func (ini *Initializer) NewBlobVerifier(b blocks.ROBlob, reqs ...Requirement) *BlobVerifier {
return &BlobVerifier{
sharedResources: ini.shared,
blob: b,
results: newResults(reqs...),
verifyBlobCommitment: kzg.VerifyROBlobCommitment,
}
}
// InitializerWaiter provides an Initializer once all dependent resources are ready
// via the WaitForInitializer method.
type InitializerWaiter struct {
sync.RWMutex
ready bool
cw startup.ClockWaiter
ini *Initializer
}
// NewInitializerWaiter creates an InitializerWaiter which can be used to obtain an Initializer once async dependencies are ready.
func NewInitializerWaiter(cw startup.ClockWaiter, fc Forkchoicer, sc SignatureCache, pc ProposerCache, db Database, sr StateByRooter) *InitializerWaiter {
shared := &sharedResources{
fc: fc,
sc: sc,
pc: pc,
db: db,
sr: sr,
}
return &InitializerWaiter{cw: cw, ini: &Initializer{shared: shared}}
}
// WaitForInitializer ensures that asynchronous initialization of the shared resources the initializer
// depends on has completed before the underlying Initializer is accessible by client code.
func (w *InitializerWaiter) WaitForInitializer(ctx context.Context) (*Initializer, error) {
if err := w.waitForReady(ctx); err != nil {
return nil, err
}
return w.ini, nil
}
func (w *InitializerWaiter) waitForReady(ctx context.Context) error {
w.Lock()
defer w.Unlock()
if w.ready {
return nil
}
clock, err := w.cw.WaitForClock(ctx)
if err != nil {
return err
}
w.ini.shared.clock = clock
w.ready = true
return nil
}

View File

@ -0,0 +1,63 @@
package verification
// Requirement represents a validation check that needs to pass in order for a Verified form a consensus type to be issued.
type Requirement int
// results collects positive verification results.
// This bitmap can be used to test which verifications have been successfully completed in order to
// decide whether it is safe to issue a "Verified" type variant.
type results struct {
done map[Requirement]error
reqs []Requirement
}
func newResults(reqs ...Requirement) *results {
return &results{done: make(map[Requirement]error, len(reqs)), reqs: reqs}
}
func (r *results) record(req Requirement, err error) {
r.done[req] = err
}
// allSatisfied returns true if there is a nil error result for every Requirement.
func (r *results) allSatisfied() bool {
if len(r.done) != len(r.reqs) {
return false
}
for i := range r.reqs {
err, ok := r.done[r.reqs[i]]
if !ok || err != nil {
return false
}
}
return true
}
func (r *results) executed(req Requirement) bool {
_, ok := r.done[req]
return ok
}
func (r *results) result(req Requirement) error {
return r.done[req]
}
func (r *results) errors(err error) error {
return newVerificationMultiError(r, err)
}
func (r *results) failures() map[Requirement]error {
fail := make(map[Requirement]error, len(r.done))
for i := range r.reqs {
req := r.reqs[i]
err, ok := r.done[req]
if !ok {
fail[req] = ErrMissingVerification
continue
}
if err != nil {
fail[req] = err
}
}
return fail
}

View File

@ -53,6 +53,11 @@ func (b *ROBlob) ParentRoot() [32]byte {
return bytesutil.ToBytes32(b.SignedBlockHeader.Header.ParentRoot) return bytesutil.ToBytes32(b.SignedBlockHeader.Header.ParentRoot)
} }
// ParentRootSlice returns the parent root as a byte slice.
func (b *ROBlob) ParentRootSlice() []byte {
return b.SignedBlockHeader.Header.ParentRoot
}
// BodyRoot returns the body root of the blob sidecar. // BodyRoot returns the body root of the blob sidecar.
func (b *ROBlob) BodyRoot() [32]byte { func (b *ROBlob) BodyRoot() [32]byte {
return bytesutil.ToBytes32(b.SignedBlockHeader.Header.BodyRoot) return bytesutil.ToBytes32(b.SignedBlockHeader.Header.BodyRoot)

View File

@ -3,7 +3,6 @@ package detect
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"testing" "testing"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state" "github.com/prysmaticlabs/prysm/v4/beacon-chain/state"
@ -48,11 +47,8 @@ func TestSlotFromBlock(t *testing.T) {
} }
func TestByState(t *testing.T) { func TestByState(t *testing.T) {
undo, err := hackDenebMaxuint() undo := util.HackDenebMaxuint(t)
require.NoError(t, err) defer undo()
defer func() {
require.NoError(t, undo())
}()
bc := params.BeaconConfig() bc := params.BeaconConfig()
altairSlot, err := slots.EpochStart(bc.AltairForkEpoch) altairSlot, err := slots.EpochStart(bc.AltairForkEpoch)
require.NoError(t, err) require.NoError(t, err)
@ -137,11 +133,8 @@ func stateForVersion(v int) (state.BeaconState, error) {
func TestUnmarshalState(t *testing.T) { func TestUnmarshalState(t *testing.T) {
ctx := context.Background() ctx := context.Background()
undo, err := hackDenebMaxuint() undo := util.HackDenebMaxuint(t)
require.NoError(t, err) defer undo()
defer func() {
require.NoError(t, undo())
}()
bc := params.BeaconConfig() bc := params.BeaconConfig()
altairSlot, err := slots.EpochStart(bc.AltairForkEpoch) altairSlot, err := slots.EpochStart(bc.AltairForkEpoch)
require.NoError(t, err) require.NoError(t, err)
@ -211,23 +204,9 @@ func TestUnmarshalState(t *testing.T) {
} }
} }
func hackDenebMaxuint() (func() error, error) {
// We monkey patch the config to use a smaller value for the next fork epoch (which is always set to maxint).
// Upstream configs use MaxUint64, which leads to a multiplication overflow when converting epoch->slot.
// Unfortunately we have unit tests that assert our config matches the upstream config, so we have to choose between
// breaking conformance, adding a special case to the conformance unit test, or patch it here.
bc := params.MainnetConfig().Copy()
bc.DenebForkEpoch = math.MaxUint32
undo, err := params.SetActiveWithUndo(bc)
return undo, err
}
func TestUnmarshalBlock(t *testing.T) { func TestUnmarshalBlock(t *testing.T) {
undo, err := hackDenebMaxuint() undo := util.HackDenebMaxuint(t)
require.NoError(t, err) defer undo()
defer func() {
require.NoError(t, undo())
}()
genv := bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion) genv := bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion)
altairv := bytesutil.ToBytes4(params.BeaconConfig().AltairForkVersion) altairv := bytesutil.ToBytes4(params.BeaconConfig().AltairForkVersion)
bellav := bytesutil.ToBytes4(params.BeaconConfig().BellatrixForkVersion) bellav := bytesutil.ToBytes4(params.BeaconConfig().BellatrixForkVersion)
@ -339,11 +318,8 @@ func TestUnmarshalBlock(t *testing.T) {
} }
func TestUnmarshalBlindedBlock(t *testing.T) { func TestUnmarshalBlindedBlock(t *testing.T) {
undo, err := hackDenebMaxuint() undo := util.HackDenebMaxuint(t)
require.NoError(t, err) defer undo()
defer func() {
require.NoError(t, undo())
}()
genv := bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion) genv := bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion)
altairv := bytesutil.ToBytes4(params.BeaconConfig().AltairForkVersion) altairv := bytesutil.ToBytes4(params.BeaconConfig().AltairForkVersion)
bellav := bytesutil.ToBytes4(params.BeaconConfig().BellatrixForkVersion) bellav := bytesutil.ToBytes4(params.BeaconConfig().BellatrixForkVersion)

View File

@ -9,6 +9,7 @@ import (
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
"github.com/prysmaticlabs/prysm/v4/config/params" "github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
) )
// ForkScheduleEntry is a Version+Epoch tuple for sorted storage in an OrderedSchedule // ForkScheduleEntry is a Version+Epoch tuple for sorted storage in an OrderedSchedule
@ -57,6 +58,20 @@ func (o OrderedSchedule) VersionForName(name string) ([fieldparams.VersionLength
return [4]byte{}, errors.Wrapf(ErrVersionNotFound, "no version with name %s", lower) return [4]byte{}, errors.Wrapf(ErrVersionNotFound, "no version with name %s", lower)
} }
func (o OrderedSchedule) ForkFromVersion(version [fieldparams.VersionLength]byte) (*ethpb.Fork, error) {
for i := range o {
e := o[i]
if e.Version == version {
f := &ethpb.Fork{Epoch: e.Epoch, CurrentVersion: version[:], PreviousVersion: version[:]}
if i > 0 {
f.PreviousVersion = o[i-1].Version[:]
}
return f, nil
}
}
return nil, errors.Wrapf(ErrVersionNotFound, "could not determine fork for version %#x", version)
}
func (o OrderedSchedule) Previous(version [fieldparams.VersionLength]byte) ([fieldparams.VersionLength]byte, error) { func (o OrderedSchedule) Previous(version [fieldparams.VersionLength]byte) ([fieldparams.VersionLength]byte, error) {
for i := len(o) - 1; i >= 0; i-- { for i := len(o) - 1; i >= 0; i-- {
if o[i].Version == version { if o[i].Version == version {

View File

@ -11,10 +11,11 @@ import (
// which can be passed to log.WithFields. // which can be passed to log.WithFields.
func BlobFields(blob blocks.ROBlob) logrus.Fields { func BlobFields(blob blocks.ROBlob) logrus.Fields {
return logrus.Fields{ return logrus.Fields{
"slot": blob.Slot(), "slot": blob.Slot(),
"proposerIndex": blob.ProposerIndex(), "proposer_index": blob.ProposerIndex(),
"blockRoot": fmt.Sprintf("%#x", blob.BlockRoot()), "block_root": fmt.Sprintf("%#x", blob.BlockRoot()),
"kzgCommitment": fmt.Sprintf("%#x", blob.KzgCommitment), "parent_root": fmt.Sprintf("%#x", blob.ParentRoot()),
"index": blob.Index, "kzg_commitment": fmt.Sprintf("%#x", blob.KzgCommitment),
"index": blob.Index,
} }
} }

View File

@ -47,6 +47,7 @@ go_library(
"//crypto/hash:go_default_library", "//crypto/hash:go_default_library",
"//crypto/rand:go_default_library", "//crypto/rand:go_default_library",
"//encoding/bytesutil:go_default_library", "//encoding/bytesutil:go_default_library",
"//network/forks:go_default_library",
"//proto/engine/v1:go_default_library", "//proto/engine/v1:go_default_library",
"//proto/eth/v1:go_default_library", "//proto/eth/v1:go_default_library",
"//proto/eth/v2:go_default_library", "//proto/eth/v2:go_default_library",
@ -73,6 +74,7 @@ go_test(
"bellatrix_state_test.go", "bellatrix_state_test.go",
"block_test.go", "block_test.go",
"capella_block_test.go", "capella_block_test.go",
"deneb_test.go",
"deposits_test.go", "deposits_test.go",
"helpers_test.go", "helpers_test.go",
"state_test.go", "state_test.go",

View File

@ -2,22 +2,58 @@ package util
import ( import (
"encoding/binary" "encoding/binary"
"math"
"math/big" "math/big"
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
gethTypes "github.com/ethereum/go-ethereum/core/types" gethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/signing"
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
"github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v4/crypto/bls"
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
"github.com/prysmaticlabs/prysm/v4/network/forks"
enginev1 "github.com/prysmaticlabs/prysm/v4/proto/engine/v1" enginev1 "github.com/prysmaticlabs/prysm/v4/proto/engine/v1"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/testing/require" "github.com/prysmaticlabs/prysm/v4/testing/require"
"github.com/prysmaticlabs/prysm/v4/time/slots"
) )
func GenerateTestDenebBlockWithSidecar(t *testing.T, parent [32]byte, slot primitives.Slot, nblobs int) (blocks.ROBlock, []blocks.ROBlob) { type DenebBlockGeneratorOption func(*denebBlockGenerator)
// Start service with 160 as allowed blocks capacity (and almost zero capacity recovery).
type denebBlockGenerator struct {
parent [32]byte
slot primitives.Slot
nblobs int
sign bool
sk bls.SecretKey
pk bls.PublicKey
proposer primitives.ValidatorIndex
valRoot []byte
}
func WithProposerSigning(idx primitives.ValidatorIndex, sk bls.SecretKey, pk bls.PublicKey, valRoot []byte) DenebBlockGeneratorOption {
return func(g *denebBlockGenerator) {
g.sign = true
g.proposer = idx
g.sk = sk
g.pk = pk
g.valRoot = valRoot
}
}
func GenerateTestDenebBlockWithSidecar(t *testing.T, parent [32]byte, slot primitives.Slot, nblobs int, opts ...DenebBlockGeneratorOption) (blocks.ROBlock, []blocks.ROBlob) {
g := &denebBlockGenerator{
parent: parent,
slot: slot,
nblobs: nblobs,
}
for _, o := range opts {
o(g)
}
stateRoot := bytesutil.PadTo([]byte("stateRoot"), fieldparams.RootLength) stateRoot := bytesutil.PadTo([]byte("stateRoot"), fieldparams.RootLength)
receiptsRoot := bytesutil.PadTo([]byte("receiptsRoot"), fieldparams.RootLength) receiptsRoot := bytesutil.PadTo([]byte("receiptsRoot"), fieldparams.RootLength)
logsBloom := bytesutil.PadTo([]byte("logs"), fieldparams.LogsBloomLength) logsBloom := bytesutil.PadTo([]byte("logs"), fieldparams.LogsBloomLength)
@ -58,16 +94,37 @@ func GenerateTestDenebBlockWithSidecar(t *testing.T, parent [32]byte, slot primi
} }
block := NewBeaconBlockDeneb() block := NewBeaconBlockDeneb()
block.Block.Body.ExecutionPayload = payload block.Block.Body.ExecutionPayload = payload
block.Block.Slot = slot block.Block.Slot = g.slot
block.Block.ParentRoot = parent[:] block.Block.ParentRoot = g.parent[:]
commitments := make([][48]byte, nblobs) commitments := make([][48]byte, g.nblobs)
block.Block.Body.BlobKzgCommitments = make([][]byte, nblobs) block.Block.Body.BlobKzgCommitments = make([][]byte, g.nblobs)
for i := range commitments { for i := range commitments {
binary.LittleEndian.PutUint16(commitments[i][0:16], uint16(i)) binary.LittleEndian.PutUint16(commitments[i][0:16], uint16(i))
binary.LittleEndian.PutUint16(commitments[i][16:32], uint16(slot)) binary.LittleEndian.PutUint16(commitments[i][16:32], uint16(g.slot))
block.Block.Body.BlobKzgCommitments[i] = commitments[i][:] 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() root, err := block.Block.HashTreeRoot()
require.NoError(t, err) require.NoError(t, err)
@ -78,7 +135,7 @@ func GenerateTestDenebBlockWithSidecar(t *testing.T, parent [32]byte, slot primi
sh, err := sbb.Header() sh, err := sbb.Header()
require.NoError(t, err) require.NoError(t, err)
for i, c := range block.Block.Body.BlobKzgCommitments { for i, c := range block.Block.Body.BlobKzgCommitments {
sidecars[i] = GenerateTestDenebBlobSidecar(t, root, sh, i, c) sidecars[i] = GenerateTestDenebBlobSidecar(t, root, sh, i, c, inclusion[i])
} }
rob, err := blocks.NewROBlock(sbb) rob, err := blocks.NewROBlock(sbb)
@ -86,7 +143,7 @@ func GenerateTestDenebBlockWithSidecar(t *testing.T, parent [32]byte, slot primi
return rob, sidecars return rob, sidecars
} }
func GenerateTestDenebBlobSidecar(t *testing.T, root [32]byte, header *ethpb.SignedBeaconBlockHeader, index int, commitment []byte) blocks.ROBlob { func GenerateTestDenebBlobSidecar(t *testing.T, root [32]byte, header *ethpb.SignedBeaconBlockHeader, index int, commitment []byte, incProof [][]byte) blocks.ROBlob {
blob := make([]byte, fieldparams.BlobSize) blob := make([]byte, fieldparams.BlobSize)
binary.LittleEndian.PutUint64(blob, uint64(index)) binary.LittleEndian.PutUint64(blob, uint64(index))
pb := &ethpb.BlobSidecar{ pb := &ethpb.BlobSidecar{
@ -96,7 +153,10 @@ func GenerateTestDenebBlobSidecar(t *testing.T, root [32]byte, header *ethpb.Sig
KzgCommitment: commitment, KzgCommitment: commitment,
KzgProof: commitment, KzgProof: commitment,
} }
pb.CommitmentInclusionProof = fakeEmptyProof(t, pb) if len(incProof) == 0 {
incProof = fakeEmptyProof(t, pb)
}
pb.CommitmentInclusionProof = incProof
r, err := blocks.NewROBlobWithRoot(pb, root) r, err := blocks.NewROBlobWithRoot(pb, root)
require.NoError(t, err) require.NoError(t, err)
return r return r
@ -127,3 +187,19 @@ func ExtendBlocksPlusBlobs(t *testing.T, blks []blocks.ROBlock, size int) ([]blo
return blks, blobs return blks, blobs
} }
// HackDenebForkEpoch is helpful for tests that need to set up cases where the deneb fork has passed.
// We have unit tests that assert our config matches the upstream config, where the next fork is always
// set to MaxUint64 until the fork epoch is formally set. This creates an issue for tests that want to
// work with slots that are defined to be after deneb because converting the max epoch to a slot leads
// to multiplication overflow.
// Monkey patching tests with this function is the simplest workaround in these cases.
func HackDenebMaxuint(t *testing.T) func() {
bc := params.MainnetConfig().Copy()
bc.DenebForkEpoch = math.MaxUint32
undo, err := params.SetActiveWithUndo(bc)
require.NoError(t, err)
return func() {
require.NoError(t, undo())
}
}

View File

@ -0,0 +1,16 @@
package util
import (
"testing"
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
"github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
"github.com/prysmaticlabs/prysm/v4/testing/require"
)
func TestInclusionProofs(t *testing.T) {
_, blobs := GenerateTestDenebBlockWithSidecar(t, [32]byte{}, 0, fieldparams.MaxBlobsPerBlock)
for i := range blobs {
require.NoError(t, blocks.VerifyKZGInclusionProof(blobs[i]))
}
}

View File

@ -163,6 +163,12 @@ func ToTime(genesisTimeSec uint64, slot primitives.Slot) (time.Time, error) {
return time.Unix(int64(sTime), 0), nil // lint:ignore uintcast -- A timestamp will not exceed int64 in your lifetime. return time.Unix(int64(sTime), 0), nil // lint:ignore uintcast -- A timestamp will not exceed int64 in your lifetime.
} }
// BeginsAt computes the timestamp where the given slot begins, relative to the genesis timestamp.
func BeginsAt(slot primitives.Slot, genesis time.Time) time.Time {
sd := time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(slot)
return genesis.Add(sd)
}
// Since computes the number of time slots that have occurred since the given timestamp. // Since computes the number of time slots that have occurred since the given timestamp.
func Since(time time.Time) primitives.Slot { func Since(time time.Time) primitives.Slot {
return CurrentSlot(uint64(time.Unix())) return CurrentSlot(uint64(time.Unix()))

View File

@ -153,6 +153,37 @@ func TestEpochStartSlot_OK(t *testing.T) {
} }
} }
func TestBeginsAtOK(t *testing.T) {
cases := []struct {
name string
genesis int64
slot primitives.Slot
slotTime time.Time
}{
{
name: "genesis",
slotTime: time.Unix(0, 0),
},
{
name: "slot 1",
slot: 1,
slotTime: time.Unix(int64(params.BeaconConfig().SecondsPerSlot), 0),
},
{
name: "slot 1",
slot: 32,
slotTime: time.Unix(int64(params.BeaconConfig().SecondsPerSlot)*32, 0),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
genesis := time.Unix(c.genesis, 0)
st := BeginsAt(c.slot, genesis)
require.Equal(t, c.slotTime, st)
})
}
}
func TestEpochEndSlot_OK(t *testing.T) { func TestEpochEndSlot_OK(t *testing.T) {
tests := []struct { tests := []struct {
epoch primitives.Epoch epoch primitives.Epoch