diff --git a/beacon-chain/verification/BUILD.bazel b/beacon-chain/verification/BUILD.bazel index fa95e5451e65..a3d01c805955 100644 --- a/beacon-chain/verification/BUILD.bazel +++ b/beacon-chain/verification/BUILD.bazel @@ -9,9 +9,12 @@ go_library( "error.go", "fake.go", "initializer.go", + "initializer_epbs.go", "interface.go", "metrics.go", "mock.go", + "payload_attestation.go", + "payload_attestation_mock.go", "result.go", ], importpath = "github.com/prysmaticlabs/prysm/v5/beacon-chain/verification", @@ -28,6 +31,7 @@ go_library( "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/blocks:go_default_library", + "//consensus-types/epbs/payload-attestation:go_default_library", "//consensus-types/primitives:go_default_library", "//crypto/bls:go_default_library", "//encoding/bytesutil:go_default_library", @@ -50,18 +54,22 @@ go_test( "blob_test.go", "cache_test.go", "initializer_test.go", + "payload_attestation_test.go", "result_test.go", ], embed = [":go_default_library"], deps = [ + "//beacon-chain/core/helpers:go_default_library", "//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", + "//beacon-chain/state/state-native:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/blocks:go_default_library", + "//consensus-types/epbs/payload-attestation:go_default_library", "//consensus-types/primitives:go_default_library", "//crypto/bls:go_default_library", "//encoding/bytesutil:go_default_library", diff --git a/beacon-chain/verification/initializer_epbs.go b/beacon-chain/verification/initializer_epbs.go new file mode 100644 index 000000000000..b6886ba423ae --- /dev/null +++ b/beacon-chain/verification/initializer_epbs.go @@ -0,0 +1,15 @@ +package verification + +import ( + payloadattestation "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs/payload-attestation" +) + +// NewPayloadAttestationMsgVerifier creates a PayloadAttestationMsgVerifier for a single payload attestation message, +// with the given set of requirements. +func (ini *Initializer) NewPayloadAttestationMsgVerifier(pa payloadattestation.ROMessage, reqs []Requirement) *PayloadAttMsgVerifier { + return &PayloadAttMsgVerifier{ + sharedResources: ini.shared, + results: newResults(reqs...), + pa: pa, + } +} diff --git a/beacon-chain/verification/interface.go b/beacon-chain/verification/interface.go index dea830511cdb..e87a5c55bbf8 100644 --- a/beacon-chain/verification/interface.go +++ b/beacon-chain/verification/interface.go @@ -3,7 +3,9 @@ package verification import ( "context" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + payloadattestation "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs/payload-attestation" ) // BlobVerifier defines the methods implemented by the ROBlobVerifier. @@ -26,6 +28,19 @@ type BlobVerifier interface { SatisfyRequirement(Requirement) } +// PayloadAttestationMsgVerifier defines the methods implemented by the ROPayloadAttestation. +// It is similar to BlobVerifier, but for payload attestation messages. +type PayloadAttestationMsgVerifier interface { + VerifyCurrentSlot() error + VerifyPayloadStatus() error + VerifyBlockRootSeen(func([32]byte) bool) error + VerifyBlockRootValid(func([32]byte) bool) error + VerifyValidatorInPTC(context.Context, state.BeaconState) error + VerifySignature(state.BeaconState) error + VerifiedPayloadAttestation() (payloadattestation.VerifiedROMessage, error) + SatisfyRequirement(Requirement) +} + // NewBlobVerifier is a function signature that can be used by code that needs to be // able to mock Initializer.NewBlobVerifier without complex setup. type NewBlobVerifier func(b blocks.ROBlob, reqs []Requirement) BlobVerifier diff --git a/beacon-chain/verification/payload_attestation.go b/beacon-chain/verification/payload_attestation.go new file mode 100644 index 000000000000..4d6fa7f9a41d --- /dev/null +++ b/beacon-chain/verification/payload_attestation.go @@ -0,0 +1,231 @@ +package verification + +import ( + "context" + "fmt" + "slices" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/config/params" + payloadattestation "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs/payload-attestation" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/crypto/bls" + "github.com/prysmaticlabs/prysm/v5/time/slots" + log "github.com/sirupsen/logrus" +) + +// RequirementList defines a list of requirements. +type RequirementList []Requirement + +const ( + RequireCurrentSlot Requirement = iota + RequireMessageNotSeen + RequireKnownPayloadStatus + RequireValidatorInPTC + RequireBlockRootSeen + RequireBlockRootValid + RequireSignatureValid +) + +// PayloadAttGossipRequirements defines the list of requirements for gossip payload attestation messages. +var PayloadAttGossipRequirements = []Requirement{ + RequireCurrentSlot, + RequireMessageNotSeen, + RequireKnownPayloadStatus, + RequireValidatorInPTC, + RequireBlockRootSeen, + RequireBlockRootValid, + RequireSignatureValid, +} + +// GossipPayloadAttestationMessageRequirements is a requirement list for gossip payload attestation messages. +var GossipPayloadAttestationMessageRequirements = RequirementList(PayloadAttGossipRequirements) + +var ( + ErrIncorrectPayloadAttSlot = errors.New("payload att slot does not match the current slot") + ErrIncorrectPayloadAttStatus = errors.New("unknown payload att status") + ErrPayloadAttBlockRootNotSeen = errors.New("block root not seen") + ErrPayloadAttBlockRootInvalid = errors.New("block root invalid") + ErrIncorrectPayloadAttValidator = errors.New("validator not present in payload timeliness committee") + ErrInvalidPayloadAttMessage = errors.New("invalid payload attestation message") +) + +var _ PayloadAttestationMsgVerifier = &PayloadAttMsgVerifier{} + +// PayloadAttMsgVerifier is a read-only verifier for payload attestation messages. +type PayloadAttMsgVerifier struct { + *sharedResources + results *results + pa payloadattestation.ROMessage +} + +// VerifyCurrentSlot verifies if the current slot matches the expected slot. +// Represents the following spec verification: +// [IGNORE] data.slot is the current slot. +func (v *PayloadAttMsgVerifier) VerifyCurrentSlot() (err error) { + defer v.record(RequireCurrentSlot, &err) + + if v.pa.Slot() != v.clock.CurrentSlot() { + log.WithFields(logFields(v.pa)).Errorf("does not match current slot %d", v.clock.CurrentSlot()) + return ErrIncorrectPayloadAttSlot + } + + return nil +} + +// VerifyPayloadStatus verifies if the payload status is known. +// Represents the following spec verification: +// [REJECT] data.payload_status < PAYLOAD_INVALID_STATUS. +func (v *PayloadAttMsgVerifier) VerifyPayloadStatus() (err error) { + defer v.record(RequireKnownPayloadStatus, &err) + + if v.pa.PayloadStatus() >= primitives.PAYLOAD_INVALID_STATUS { + log.WithFields(logFields(v.pa)).Error(ErrIncorrectPayloadAttStatus.Error()) + return ErrIncorrectPayloadAttStatus + } + + return nil +} + +// VerifyBlockRootSeen verifies if the block root has been seen before. +// Represents the following spec verification: +// [IGNORE] The attestation's data.beacon_block_root has been seen (via both gossip and non-gossip sources). +func (v *PayloadAttMsgVerifier) VerifyBlockRootSeen(parentSeen func([32]byte) bool) (err error) { + defer v.record(RequireBlockRootSeen, &err) + + if parentSeen != nil && parentSeen(v.pa.BeaconBlockRoot()) { + return nil + } + + if v.fc.HasNode(v.pa.BeaconBlockRoot()) { + return nil + } + + log.WithFields(logFields(v.pa)).Error(ErrPayloadAttBlockRootNotSeen.Error()) + return ErrPayloadAttBlockRootNotSeen +} + +// VerifyBlockRootValid verifies if the block root is valid. +// Represents the following spec verification: +// [REJECT] The beacon block with root data.beacon_block_root passes validation. +func (v *PayloadAttMsgVerifier) VerifyBlockRootValid(badBlock func([32]byte) bool) (err error) { + defer v.record(RequireBlockRootValid, &err) + + if badBlock != nil && badBlock(v.pa.BeaconBlockRoot()) { + log.WithFields(logFields(v.pa)).Error(ErrPayloadAttBlockRootInvalid.Error()) + return ErrPayloadAttBlockRootInvalid + } + + return nil +} + +// VerifyValidatorInPTC verifies if the validator is present. +// Represents the following spec verification: +// [REJECT] The validator index is within the payload committee in get_ptc(state, data.slot). For the current's slot head state. +func (v *PayloadAttMsgVerifier) VerifyValidatorInPTC(ctx context.Context, st state.BeaconState) (err error) { + defer v.record(RequireValidatorInPTC, &err) + + ptc, err := helpers.GetPayloadTimelinessCommittee(ctx, st, v.pa.Slot()) + if err != nil { + return err + } + + idx := slices.Index(ptc, v.pa.ValidatorIndex()) + if idx == -1 { + log.WithFields(logFields(v.pa)).Error(ErrIncorrectPayloadAttValidator.Error()) + return ErrIncorrectPayloadAttValidator + } + + return nil +} + +// VerifySignature verifies the signature of the payload attestation message. +// Represents the following spec verification: +// [REJECT] The signature of payload_attestation_message.signature is valid with respect to the validator index. +func (v *PayloadAttMsgVerifier) VerifySignature(st state.BeaconState) (err error) { + defer v.record(RequireSignatureValid, &err) + + err = validatePayloadAttestationMessageSignature(st, v.pa) + if err != nil { + if errors.Is(err, signing.ErrSigFailedToVerify) { + log.WithFields(logFields(v.pa)).Error("signature failed to validate") + } else { + log.WithFields(logFields(v.pa)).WithError(err).Error("could not validate signature") + } + return err + } + + return nil +} + +// VerifiedPayloadAttestation returns a verified payload attestation message by checking all requirements. +func (v *PayloadAttMsgVerifier) VerifiedPayloadAttestation() (payloadattestation.VerifiedROMessage, error) { + if v.results.allSatisfied() { + return payloadattestation.NewVerifiedROMessage(v.pa), nil + } + return payloadattestation.VerifiedROMessage{}, ErrInvalidPayloadAttMessage +} + +// SatisfyRequirement allows the caller to manually mark a requirement as satisfied. +func (v *PayloadAttMsgVerifier) SatisfyRequirement(req Requirement) { + v.record(req, nil) +} + +// ValidatePayloadAttestationMessageSignature verifies the signature of a payload attestation message. +func validatePayloadAttestationMessageSignature(st state.BeaconState, payloadAtt payloadattestation.ROMessage) error { + val, err := st.ValidatorAtIndex(payloadAtt.ValidatorIndex()) + if err != nil { + return err + } + + pub, err := bls.PublicKeyFromBytes(val.PublicKey) + if err != nil { + return err + } + + s := payloadAtt.Signature() + sig, err := bls.SignatureFromBytes(s[:]) + if err != nil { + return err + } + + currentEpoch := slots.ToEpoch(st.Slot()) + domain, err := signing.Domain(st.Fork(), currentEpoch, params.BeaconConfig().DomainPTCAttester, st.GenesisValidatorsRoot()) + if err != nil { + return err + } + + root, err := payloadAtt.SigningRoot(domain) + if err != nil { + return err + } + + if !sig.Verify(pub, root[:]) { + return signing.ErrSigFailedToVerify + } + return nil +} + +// record records the result of a requirement verification. +func (v *PayloadAttMsgVerifier) record(req Requirement, err *error) { + if err == nil || *err == nil { + v.results.record(req, nil) + return + } + + v.results.record(req, *err) +} + +// logFields returns log fields for a ROMessage instance. +func logFields(payload payloadattestation.ROMessage) log.Fields { + return log.Fields{ + "slot": payload.Slot(), + "validatorIndex": payload.ValidatorIndex(), + "signature": fmt.Sprintf("%#x", payload.Signature()), + "beaconBlockRoot": fmt.Sprintf("%#x", payload.BeaconBlockRoot()), + "payloadStatus": payload.PayloadStatus(), + } +} diff --git a/beacon-chain/verification/payload_attestation_mock.go b/beacon-chain/verification/payload_attestation_mock.go new file mode 100644 index 000000000000..8881879972be --- /dev/null +++ b/beacon-chain/verification/payload_attestation_mock.go @@ -0,0 +1,51 @@ +package verification + +import ( + "context" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + payloadattestation "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs/payload-attestation" +) + +type MockPayloadAttestation struct { + ErrIncorrectPayloadAttSlot error + ErrIncorrectPayloadAttStatus error + ErrIncorrectPayloadAttValidator error + ErrPayloadAttBlockRootNotSeen error + ErrPayloadAttBlockRootInvalid error + ErrInvalidPayloadAttMessage error + ErrInvalidMessageSignature error + ErrUnsatisfiedRequirement error +} + +var _ PayloadAttestationMsgVerifier = &MockPayloadAttestation{} + +func (m *MockPayloadAttestation) VerifyCurrentSlot() error { + return m.ErrIncorrectPayloadAttSlot +} + +func (m *MockPayloadAttestation) VerifyPayloadStatus() error { + return m.ErrIncorrectPayloadAttStatus +} + +func (m *MockPayloadAttestation) VerifyValidatorInPTC(ctx context.Context, st state.BeaconState) error { + return m.ErrIncorrectPayloadAttValidator +} + +func (m *MockPayloadAttestation) VerifyBlockRootSeen(func([32]byte) bool) error { + return m.ErrPayloadAttBlockRootNotSeen +} + +func (m *MockPayloadAttestation) VerifyBlockRootValid(func([32]byte) bool) error { + return m.ErrPayloadAttBlockRootInvalid +} + +func (m *MockPayloadAttestation) VerifySignature(st state.BeaconState) (err error) { + return m.ErrInvalidMessageSignature +} + +func (m *MockPayloadAttestation) VerifiedPayloadAttestation() (payloadattestation.VerifiedROMessage, error) { + return payloadattestation.VerifiedROMessage{}, nil +} + +func (m *MockPayloadAttestation) SatisfyRequirement(req Requirement) {} diff --git a/beacon-chain/verification/payload_attestation_test.go b/beacon-chain/verification/payload_attestation_test.go new file mode 100644 index 000000000000..ed70cbc2723e --- /dev/null +++ b/beacon-chain/verification/payload_attestation_test.go @@ -0,0 +1,321 @@ +package verification + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/startup" + state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native" + "github.com/prysmaticlabs/prysm/v5/config/params" + payloadattestation "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs/payload-attestation" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/crypto/bls" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + 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 TestVerifyCurrentSlot(t *testing.T) { + now := time.Now() + // make genesis 1 slot in the past + genesis := now.Add(-1 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second) + clock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return now })) + + init := Initializer{shared: &sharedResources{clock: clock}} + + t.Run("incorrect slot", func(t *testing.T) { + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{}, + Signature: make([]byte, 96), + }, init) + require.ErrorIs(t, pa.VerifyCurrentSlot(), ErrIncorrectPayloadAttSlot) + require.Equal(t, true, pa.results.executed(RequireCurrentSlot)) + require.Equal(t, ErrIncorrectPayloadAttSlot, pa.results.result(RequireCurrentSlot)) + }) + + t.Run("current slot", func(t *testing.T) { + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + Slot: 1, + }, + Signature: make([]byte, 96), + }, init) + require.NoError(t, pa.VerifyCurrentSlot()) + require.Equal(t, true, pa.results.executed(RequireCurrentSlot)) + require.NoError(t, pa.results.result(RequireCurrentSlot)) + }) +} + +func TestVerifyKnownPayloadStatus(t *testing.T) { + init := Initializer{shared: &sharedResources{clock: &startup.Clock{}}} + + t.Run("unknown status", func(t *testing.T) { + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + PayloadStatus: primitives.PAYLOAD_INVALID_STATUS, + }, + Signature: make([]byte, 96), + }, init) + require.ErrorIs(t, pa.VerifyPayloadStatus(), ErrIncorrectPayloadAttStatus) + require.Equal(t, true, pa.results.executed(RequireKnownPayloadStatus)) + require.Equal(t, ErrIncorrectPayloadAttStatus, pa.results.result(RequireKnownPayloadStatus)) + }) + + t.Run("known status", func(t *testing.T) { + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{}, + Signature: make([]byte, 96), + }, init) + require.NoError(t, pa.VerifyPayloadStatus()) + require.Equal(t, true, pa.results.executed(RequireKnownPayloadStatus)) + require.NoError(t, pa.results.result(RequireKnownPayloadStatus)) + }) +} + +func TestVerifyBlockRootSeen(t *testing.T) { + blockRoot := [32]byte{1} + + fc := &mockForkchoicer{ + HasNodeCB: func(parent [32]byte) bool { + return parent == blockRoot + }, + } + + t.Run("happy path", func(t *testing.T) { + init := Initializer{shared: &sharedResources{fc: fc}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: blockRoot[:], + }, + Signature: make([]byte, 96), + }, init) + require.NoError(t, pa.VerifyBlockRootSeen(nil)) + require.Equal(t, true, pa.results.executed(RequireBlockRootSeen)) + require.NoError(t, pa.results.result(RequireBlockRootSeen)) + }) + + t.Run("unknown block", func(t *testing.T) { + init := Initializer{shared: &sharedResources{fc: fc}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{}, + Signature: make([]byte, 96), + }, init) + require.ErrorIs(t, pa.VerifyBlockRootSeen(nil), ErrPayloadAttBlockRootNotSeen) + require.Equal(t, true, pa.results.executed(RequireBlockRootSeen)) + require.Equal(t, ErrPayloadAttBlockRootNotSeen, pa.results.result(RequireBlockRootSeen)) + }) + + t.Run("bad parent true", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: blockRoot[:], + }, + Signature: make([]byte, 96), + }, init) + require.NoError(t, pa.VerifyBlockRootSeen(badParentCb(t, blockRoot, true))) + require.Equal(t, true, pa.results.executed(RequireBlockRootSeen)) + require.NoError(t, pa.results.result(RequireBlockRootSeen)) + }) + + t.Run("bad parent false, unknown block", func(t *testing.T) { + init := Initializer{shared: &sharedResources{fc: fc}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: []byte{2}, + }, + Signature: make([]byte, 96), + }, init) + require.ErrorIs(t, pa.VerifyBlockRootSeen(badParentCb(t, [32]byte{2}, false)), ErrPayloadAttBlockRootNotSeen) + require.Equal(t, true, pa.results.executed(RequireBlockRootSeen)) + require.Equal(t, ErrPayloadAttBlockRootNotSeen, pa.results.result(RequireBlockRootSeen)) + }) +} + +func TestVerifyBlockRootValid(t *testing.T) { + blockRoot := [32]byte{1} + + t.Run("good block", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: blockRoot[:], + }, + Signature: make([]byte, 96), + }, init) + require.NoError(t, pa.VerifyBlockRootValid(badParentCb(t, blockRoot, false))) + require.Equal(t, true, pa.results.executed(RequireBlockRootValid)) + require.NoError(t, pa.results.result(RequireBlockRootValid)) + }) + + t.Run("bad block", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: blockRoot[:], + }, + Signature: make([]byte, 96), + }, init) + require.ErrorIs(t, pa.VerifyBlockRootValid(badParentCb(t, blockRoot, true)), ErrPayloadAttBlockRootInvalid) + require.Equal(t, true, pa.results.executed(RequireBlockRootValid)) + require.Equal(t, ErrPayloadAttBlockRootInvalid, pa.results.result(RequireBlockRootValid)) + }) +} + +func TestGetPayloadTimelinessCommittee(t *testing.T) { + validators := make([]*ethpb.Validator, 4*params.BeaconConfig().TargetCommitteeSize*uint64(params.BeaconConfig().SlotsPerEpoch)) + validatorIndices := make([]primitives.ValidatorIndex, len(validators)) + + for i := 0; i < len(validators); i++ { + k := make([]byte, 48) + copy(k, strconv.Itoa(i)) + validators[i] = ðpb.Validator{ + PublicKey: k, + WithdrawalCredentials: make([]byte, 32), + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + } + validatorIndices[i] = primitives.ValidatorIndex(i) + } + + st, err := state_native.InitializeFromProtoEpbs(ðpb.BeaconStateEPBS{ + Validators: validators, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + }) + require.NoError(t, err) + + slot := primitives.Slot(1) + ctx := context.Background() + ptc, err := helpers.GetPayloadTimelinessCommittee(ctx, st, slot) + require.NoError(t, err) + + t.Run("in committee", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + ValidatorIndex: ptc[0], + Data: ðpb.PayloadAttestationData{ + Slot: slot, + }, + Signature: make([]byte, 96), + }, init) + require.NoError(t, pa.VerifyValidatorInPTC(ctx, st)) + require.Equal(t, true, pa.results.executed(RequireValidatorInPTC)) + require.NoError(t, pa.results.result(RequireValidatorInPTC)) + }) + + t.Run("not in committee", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + Slot: slot, + }, + Signature: make([]byte, 96), + }, init) + require.ErrorIs(t, pa.VerifyValidatorInPTC(ctx, st), ErrIncorrectPayloadAttValidator) + require.Equal(t, true, pa.results.executed(RequireValidatorInPTC)) + require.Equal(t, ErrIncorrectPayloadAttValidator, pa.results.result(RequireValidatorInPTC)) + }) +} + +func TestPayloadAttestationVerifySignature(t *testing.T) { + _, secretKeys, err := util.DeterministicDepositsAndKeys(2) + require.NoError(t, err) + + st, err := state_native.InitializeFromProtoEpbs(ðpb.BeaconStateEPBS{ + Validators: []*ethpb.Validator{{PublicKey: secretKeys[0].PublicKey().Marshal()}, + {PublicKey: secretKeys[1].PublicKey().Marshal()}}, + Fork: ðpb.Fork{ + CurrentVersion: params.BeaconConfig().EPBSForkVersion, + PreviousVersion: params.BeaconConfig().ElectraForkVersion, + }, + }) + require.NoError(t, err) + + t.Run("valid signature", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + d := ðpb.PayloadAttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte{'a'}, 32), + Slot: 1, + PayloadStatus: primitives.PAYLOAD_WITHHELD, + } + signedBytes, err := signing.ComputeDomainAndSign( + st, + slots.ToEpoch(d.Slot), + d, + params.BeaconConfig().DomainPTCAttester, + secretKeys[0], + ) + require.NoError(t, err) + sig, err := bls.SignatureFromBytes(signedBytes) + require.NoError(t, err) + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: d, + Signature: sig.Marshal(), + }, init) + require.NoError(t, pa.VerifySignature(st)) + require.Equal(t, true, pa.results.executed(RequireSignatureValid)) + require.NoError(t, pa.results.result(RequireSignatureValid)) + }) + + t.Run("invalid signature", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + d := ðpb.PayloadAttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte{'a'}, 32), + Slot: 1, + PayloadStatus: primitives.PAYLOAD_WITHHELD, + } + signedBytes, err := signing.ComputeDomainAndSign( + st, + slots.ToEpoch(d.Slot), + d, + params.BeaconConfig().DomainPTCAttester, + secretKeys[0], + ) + require.NoError(t, err) + sig, err := bls.SignatureFromBytes(signedBytes) + require.NoError(t, err) + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + ValidatorIndex: 1, + Data: d, + Signature: sig.Marshal(), + }, init) + require.ErrorIs(t, pa.VerifySignature(st), signing.ErrSigFailedToVerify) + require.Equal(t, true, pa.results.executed(RequireSignatureValid)) + require.Equal(t, signing.ErrSigFailedToVerify, pa.results.result(RequireSignatureValid)) + }) +} + +func TestVerifiedPayloadAttestation(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + pa := newPayloadAttestation(t, ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{}, + Signature: make([]byte, 96), + }, init) + + t.Run("missing last requirement", func(t *testing.T) { + for _, requirement := range GossipPayloadAttestationMessageRequirements[:len(GossipPayloadAttestationMessageRequirements)-1] { + pa.SatisfyRequirement(requirement) + } + _, err := pa.VerifiedPayloadAttestation() + require.ErrorIs(t, err, ErrInvalidPayloadAttMessage) + }) + + t.Run("satisfy all the requirements", func(t *testing.T) { + for _, requirement := range GossipPayloadAttestationMessageRequirements { + pa.SatisfyRequirement(requirement) + } + _, err := pa.VerifiedPayloadAttestation() + require.NoError(t, err) + }) +} + +func newPayloadAttestation(t *testing.T, m *ethpb.PayloadAttestationMessage, init Initializer) *PayloadAttMsgVerifier { + ro, err := payloadattestation.NewReadOnly(m) + require.NoError(t, err) + return init.NewPayloadAttestationMsgVerifier(ro, GossipPayloadAttestationMessageRequirements) +} diff --git a/consensus-types/epbs/payload-attestation/BUILD.bazel b/consensus-types/epbs/payload-attestation/BUILD.bazel new file mode 100644 index 000000000000..85a897625478 --- /dev/null +++ b/consensus-types/epbs/payload-attestation/BUILD.bazel @@ -0,0 +1,27 @@ +load("@prysm//tools/go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["readonly_message.go"], + importpath = "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs/payload-attestation", + visibility = ["//visibility:public"], + deps = [ + "//beacon-chain/core/signing:go_default_library", + "//consensus-types/primitives:go_default_library", + "//encoding/bytesutil:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "@com_github_pkg_errors//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["readonly_message_test.go"], + embed = [":go_default_library"], + deps = [ + "//config/fieldparams:go_default_library", + "//consensus-types/primitives:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//testing/require:go_default_library", + ], +) diff --git a/consensus-types/epbs/payload-attestation/readonly_message.go b/consensus-types/epbs/payload-attestation/readonly_message.go new file mode 100644 index 000000000000..9e9488cca062 --- /dev/null +++ b/consensus-types/epbs/payload-attestation/readonly_message.go @@ -0,0 +1,82 @@ +package payloadattestation + +import ( + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" +) + +var ( + errNilPayloadAttMessage = errors.New("received nil payload attestation message") + errNilPayloadAttData = errors.New("received nil payload attestation data") + errNilPayloadAttSignature = errors.New("received nil payload attestation signature") +) + +// ROMessage represents a read-only payload attestation message. +type ROMessage struct { + m *ethpb.PayloadAttestationMessage +} + +// validatePayloadAtt checks if the given payload attestation message is valid. +func validatePayloadAtt(m *ethpb.PayloadAttestationMessage) error { + if m == nil { + return errNilPayloadAttMessage + } + if m.Data == nil { + return errNilPayloadAttData + } + if len(m.Signature) == 0 { + return errNilPayloadAttSignature + } + return nil +} + +// NewReadOnly creates a new ReadOnly instance after validating the message. +func NewReadOnly(m *ethpb.PayloadAttestationMessage) (ROMessage, error) { + if err := validatePayloadAtt(m); err != nil { + return ROMessage{}, err + } + return ROMessage{m}, nil +} + +// ValidatorIndex returns the validator index from the payload attestation message. +func (r *ROMessage) ValidatorIndex() primitives.ValidatorIndex { + return r.m.ValidatorIndex +} + +// Signature returns the signature from the payload attestation message. +func (r *ROMessage) Signature() [96]byte { + return bytesutil.ToBytes96(r.m.Signature) +} + +// BeaconBlockRoot returns the beacon block root from the payload attestation message. +func (r *ROMessage) BeaconBlockRoot() [32]byte { + return bytesutil.ToBytes32(r.m.Data.BeaconBlockRoot) +} + +// Slot returns the slot from the payload attestation message. +func (r *ROMessage) Slot() primitives.Slot { + return r.m.Data.Slot +} + +// PayloadStatus returns the payload status from the payload attestation message. +func (r *ROMessage) PayloadStatus() primitives.PTCStatus { + return r.m.Data.PayloadStatus +} + +// SigningRoot returns the signing root from the payload attestation message. +func (r *ROMessage) SigningRoot(domain []byte) ([32]byte, error) { + return signing.ComputeSigningRoot(r.m.Data, domain) +} + +// VerifiedROMessage represents a verified read-only payload attestation message. +type VerifiedROMessage struct { + ROMessage +} + +// NewVerifiedROMessage creates a new VerifiedROMessage instance after validating the message. +func NewVerifiedROMessage(r ROMessage) VerifiedROMessage { + return VerifiedROMessage{r} +} diff --git a/consensus-types/epbs/payload-attestation/readonly_message_test.go b/consensus-types/epbs/payload-attestation/readonly_message_test.go new file mode 100644 index 000000000000..5ed817b29356 --- /dev/null +++ b/consensus-types/epbs/payload-attestation/readonly_message_test.go @@ -0,0 +1,132 @@ +package payloadattestation + +import ( + "testing" + + fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/testing/require" +) + +func TestValidatePayload(t *testing.T) { + tests := []struct { + name string + bfunc func(t *testing.T) *ethpb.PayloadAttestationMessage + wanterr error + }{ + { + name: "nil PayloadAttestationMessage", + bfunc: func(t *testing.T) *ethpb.PayloadAttestationMessage { + return nil + }, + wanterr: errNilPayloadAttMessage, + }, + { + name: "nil data", + bfunc: func(t *testing.T) *ethpb.PayloadAttestationMessage { + return ðpb.PayloadAttestationMessage{ + Data: nil, + } + }, + wanterr: errNilPayloadAttData, + }, + { + name: "nil signature", + bfunc: func(t *testing.T) *ethpb.PayloadAttestationMessage { + return ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + }, + Signature: nil, + } + }, + wanterr: errNilPayloadAttSignature, + }, + { + name: "Correct PayloadAttestationMessage", + bfunc: func(t *testing.T) *ethpb.PayloadAttestationMessage { + return ðpb.PayloadAttestationMessage{ + Signature: make([]byte, fieldparams.BLSSignatureLength), + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + }, + } + }, + wanterr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name+" ReadOnly", func(t *testing.T) { + m := tt.bfunc(t) + err := validatePayloadAtt(m) + if tt.wanterr != nil { + require.ErrorIs(t, err, tt.wanterr) + } else { + roMess, err := NewReadOnly(m) + require.NoError(t, err) + require.Equal(t, roMess.m.Data, m.Data) + require.DeepEqual(t, roMess.m.Signature, m.Signature) + } + }) + } +} + +func TestValidatorIndex(t *testing.T) { + valIdx := primitives.ValidatorIndex(1) + m := &ROMessage{ + m: ðpb.PayloadAttestationMessage{ + ValidatorIndex: valIdx, + }, + } + require.Equal(t, valIdx, m.ValidatorIndex()) +} + +func TestSignature(t *testing.T) { + sig := [96]byte{} + m := &ROMessage{ + m: ðpb.PayloadAttestationMessage{ + Signature: sig[:], + }, + } + require.Equal(t, sig, m.Signature()) +} + +func TestBeaconBlockRoot(t *testing.T) { + root := [32]byte{} + m := &ROMessage{ + m: ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: root[:], + }, + }, + } + require.Equal(t, root, m.BeaconBlockRoot()) +} + +func TestSlot(t *testing.T) { + slot := primitives.Slot(1) + m := &ROMessage{ + m: ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + Slot: slot, + }, + }, + } + require.Equal(t, slot, m.Slot()) +} + +func TestPayloadStatus(t *testing.T) { + for status := primitives.PAYLOAD_ABSENT; status < primitives.PAYLOAD_INVALID_STATUS; status++ { + m := &ROMessage{ + m: ðpb.PayloadAttestationMessage{ + Data: ðpb.PayloadAttestationData{ + PayloadStatus: status, + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + }, + } + require.NoError(t, validatePayloadAtt(m.m)) + require.Equal(t, status, m.PayloadStatus()) + } +} diff --git a/testing/validator-mock/validator_client_mock.go b/testing/validator-mock/validator_client_mock.go index f8f2ef89e8f3..16ff6357d86e 100644 --- a/testing/validator-mock/validator_client_mock.go +++ b/testing/validator-mock/validator_client_mock.go @@ -366,7 +366,6 @@ func (mr *MockValidatorClientMockRecorder) SubmitPayloadAttestation(arg0, arg1 a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitPayloadAttestation", reflect.TypeOf((*MockValidatorClient)(nil).SubmitPayloadAttestation), arg0, arg1) } - // SubmitSignedAggregateSelectionProof mocks base method. func (m *MockValidatorClient) SubmitSignedAggregateSelectionProof(arg0 context.Context, arg1 *eth.SignedAggregateSubmitRequest) (*eth.SignedAggregateSubmitResponse, error) { m.ctrl.T.Helper()