diff --git a/beacon-chain/core/helpers/BUILD.bazel b/beacon-chain/core/helpers/BUILD.bazel index 355a367e6e5d..e92db3c48a75 100644 --- a/beacon-chain/core/helpers/BUILD.bazel +++ b/beacon-chain/core/helpers/BUILD.bazel @@ -26,6 +26,7 @@ go_library( "//beacon-chain/state:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", + "//consensus-types/epbs:go_default_library", "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", "//container/slice:go_default_library", @@ -77,6 +78,7 @@ go_test( "//beacon-chain/state/state-native:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", + "//consensus-types/epbs:go_default_library", "//consensus-types/primitives:go_default_library", "//container/slice:go_default_library", "//crypto/hash:go_default_library", diff --git a/beacon-chain/core/helpers/payload_attestation.go b/beacon-chain/core/helpers/payload_attestation.go index 4796223d7d20..2342ac5a2fd6 100644 --- a/beacon-chain/core/helpers/payload_attestation.go +++ b/beacon-chain/core/helpers/payload_attestation.go @@ -2,11 +2,16 @@ package helpers import ( "context" + "slices" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/crypto/bls" "github.com/prysmaticlabs/prysm/v5/math" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/runtime/version" @@ -97,3 +102,150 @@ func PtcAllocation(totalActive uint64) (committeesPerSlot, membersPerCommittee u membersPerCommittee = fieldparams.PTCSize / committeesPerSlot return } + +// GetPayloadAttestingIndices returns the set of attester indices corresponding to the given PayloadAttestation. +// +// Spec pseudocode definition: +// +// def get_payload_attesting_indices(state: BeaconState, slot: Slot, +// payload_attestation: PayloadAttestation) -> Set[ValidatorIndex]: +// """ +// Return the set of attesting indices corresponding to ``payload_attestation``. +// """ +// ptc = get_ptc(state, slot) +// return set(index for i, index in enumerate(ptc) if payload_attestation.aggregation_bits[i]) +func GetPayloadAttestingIndices(ctx context.Context, state state.ReadOnlyBeaconState, slot primitives.Slot, att *eth.PayloadAttestation) (indices []primitives.ValidatorIndex, err error) { + if state.Version() < version.EPBS { + return nil, errPreEPBSState + } + + ptc, err := GetPayloadTimelinessCommittee(ctx, state, slot) + if err != nil { + return nil, err + } + + for i, validatorIndex := range ptc { + if att.AggregationBits.BitAt(uint64(i)) { + indices = append(indices, validatorIndex) + } + } + + return +} + +// GetIndexedPayloadAttestation replaces a PayloadAttestation's AggregationBits with sorted AttestingIndices and returns an IndexedPayloadAttestation. +// +// Spec pseudocode definition: +// +// def get_indexed_payload_attestation(state: BeaconState, slot: Slot, +// payload_attestation: PayloadAttestation) -> IndexedPayloadAttestation: +// """ +// Return the indexed payload attestation corresponding to ``payload_attestation``. +// """ +// attesting_indices = get_payload_attesting_indices(state, slot, payload_attestation) +// +// return IndexedPayloadAttestation( +// attesting_indices=sorted(attesting_indices), +// data=payload_attestation.data, +// signature=payload_attestation.signature, +// ) +func GetIndexedPayloadAttestation(ctx context.Context, state state.ReadOnlyBeaconState, slot primitives.Slot, att *eth.PayloadAttestation) (*epbs.IndexedPayloadAttestation, error) { + if state.Version() < version.EPBS { + return nil, errPreEPBSState + } + + attestingIndices, err := GetPayloadAttestingIndices(ctx, state, slot, att) + if err != nil { + return nil, err + } + + slices.Sort(attestingIndices) + + return &epbs.IndexedPayloadAttestation{ + AttestingIndices: attestingIndices, + Data: att.Data, + Signature: att.Signature, + }, nil +} + +// IsValidIndexedPayloadAttestation validates the given IndexedPayloadAttestation. +// +// Spec pseudocode definition: +// +// def is_valid_indexed_payload_attestation( +// state: BeaconState, +// indexed_payload_attestation: IndexedPayloadAttestation) -> bool: +// """ +// Check if ``indexed_payload_attestation`` is not empty, has sorted and unique indices and has +// a valid aggregate signature. +// """ +// # Verify the data is valid +// if indexed_payload_attestation.data.payload_status >= PAYLOAD_INVALID_STATUS: +// return False +// +// # Verify indices are sorted and unique +// indices = indexed_payload_attestation.attesting_indices +// if len(indices) == 0 or not indices == sorted(set(indices)): +// return False +// +// # Verify aggregate signature +// pubkeys = [state.validators[i].pubkey for i in indices] +// domain = get_domain(state, DOMAIN_PTC_ATTESTER, None) +// signing_root = compute_signing_root(indexed_payload_attestation.data, domain) +// return bls.FastAggregateVerify(pubkeys, signing_root, indexed_payload_attestation.signature) +func IsValidIndexedPayloadAttestation(state state.ReadOnlyBeaconState, att *epbs.IndexedPayloadAttestation) (bool, error) { + if state.Version() < version.EPBS { + return false, errPreEPBSState + } + + // Verify the data is valid. + if att.Data.PayloadStatus >= primitives.PAYLOAD_INVALID_STATUS { + return false, nil + } + + // Verify indices are sorted and unique. + indices := att.AttestingIndices + slices.Sort(indices) + if len(indices) == 0 || !slices.Equal(att.AttestingIndices, indices) { + return false, nil + } + + // Verify aggregate signature. + publicKeys := make([]bls.PublicKey, len(indices)) + for i, index := range indices { + validator, err := state.ValidatorAtIndexReadOnly(index) + if err != nil { + return false, err + } + + publicKeyBytes := validator.PublicKey() + publicKey, err := bls.PublicKeyFromBytes(publicKeyBytes[:]) + if err != nil { + return false, err + } + + publicKeys[i] = publicKey + } + + domain, err := signing.Domain( + state.Fork(), + slots.ToEpoch(state.Slot()), + params.BeaconConfig().DomainPTCAttester, + state.GenesisValidatorsRoot(), + ) + if err != nil { + return false, err + } + + signingRoot, err := signing.ComputeSigningRoot(att.Data, domain) + if err != nil { + return false, err + } + + signature, err := bls.SignatureFromBytes(att.Signature) + if err != nil { + return false, err + } + + return signature.FastAggregateVerify(publicKeys, signingRoot), nil +} diff --git a/beacon-chain/core/helpers/payload_attestation_test.go b/beacon-chain/core/helpers/payload_attestation_test.go index 91213fb82cd2..a613fd136ade 100644 --- a/beacon-chain/core/helpers/payload_attestation_test.go +++ b/beacon-chain/core/helpers/payload_attestation_test.go @@ -2,18 +2,25 @@ package helpers_test import ( "context" + "slices" "strconv" "testing" "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/crypto/bls" + "github.com/prysmaticlabs/prysm/v5/crypto/rand" "github.com/prysmaticlabs/prysm/v5/math" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" 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/testing/util/random" "github.com/prysmaticlabs/prysm/v5/time/slots" ) @@ -115,3 +122,242 @@ func Test_PtcAllocation(t *testing.T) { } } } + +func TestGetPayloadAttestingIndices(t *testing.T) { + helpers.ClearCache() + + // Create 10 committees. Total 40960 validators. + committeeCount := uint64(10) + validatorCount := committeeCount * params.BeaconConfig().TargetCommitteeSize * uint64(params.BeaconConfig().SlotsPerEpoch) + validators := make([]*ethpb.Validator, validatorCount) + + for i := 0; i < len(validators); i++ { + pubkey := make([]byte, 48) + copy(pubkey, strconv.Itoa(i)) + validators[i] = ðpb.Validator{ + PublicKey: pubkey, + WithdrawalCredentials: make([]byte, 32), + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + } + } + + // Create a beacon state. + state, err := state_native.InitializeFromProtoEpbs(ðpb.BeaconStateEPBS{ + Validators: validators, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + }) + require.NoError(t, err) + + // Get PTC. + ptc, err := helpers.GetPayloadTimelinessCommittee(context.Background(), state, state.Slot()) + require.NoError(t, err) + require.Equal(t, fieldparams.PTCSize, len(ptc)) + + // Generate random indices. PTC members at the corresponding indices are considered attested. + randGen := rand.NewDeterministicGenerator() + attesterCount := randGen.Intn(fieldparams.PTCSize) + 1 + indices := randGen.Perm(fieldparams.PTCSize)[:attesterCount] + slices.Sort(indices) + require.Equal(t, attesterCount, len(indices)) + + // Create a PayloadAttestation with AggregationBits set true at the indices. + aggregationBits := bitfield.NewBitvector512() + for _, index := range indices { + aggregationBits.SetBitAt(uint64(index), true) + } + + payloadAttestation := ð.PayloadAttestation{ + AggregationBits: aggregationBits, + Data: ð.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, 32), + }, + Signature: make([]byte, 96), + } + + // Get attesting indices. + attesters, err := helpers.GetPayloadAttestingIndices(context.Background(), state, state.Slot(), payloadAttestation) + require.NoError(t, err) + require.Equal(t, len(indices), len(attesters)) + + // Check if each attester equals to the PTC member at the corresponding index. + for i, index := range indices { + require.Equal(t, attesters[i], ptc[index]) + } +} + +func TestGetIndexedPayloadAttestation(t *testing.T) { + helpers.ClearCache() + + // Create 10 committees. Total 40960 validators. + committeeCount := uint64(10) + validatorCount := committeeCount * params.BeaconConfig().TargetCommitteeSize * uint64(params.BeaconConfig().SlotsPerEpoch) + validators := make([]*ethpb.Validator, validatorCount) + + for i := 0; i < len(validators); i++ { + publicKey := make([]byte, 48) + copy(publicKey, strconv.Itoa(i)) + validators[i] = ðpb.Validator{ + PublicKey: publicKey, + WithdrawalCredentials: make([]byte, 32), + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + } + } + + // Create a beacon state. + state, err := state_native.InitializeFromProtoEpbs(ðpb.BeaconStateEPBS{ + Validators: validators, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + }) + require.NoError(t, err) + + // Get PTC. + ptc, err := helpers.GetPayloadTimelinessCommittee(context.Background(), state, state.Slot()) + require.NoError(t, err) + require.Equal(t, fieldparams.PTCSize, len(ptc)) + + // Generate random indices. PTC members at the corresponding indices are considered attested. + randGen := rand.NewDeterministicGenerator() + attesterCount := randGen.Intn(fieldparams.PTCSize) + 1 + indices := randGen.Perm(fieldparams.PTCSize)[:attesterCount] + slices.Sort(indices) + require.Equal(t, attesterCount, len(indices)) + + // Create a PayloadAttestation with AggregationBits set true at the indices. + aggregationBits := bitfield.NewBitvector512() + for _, index := range indices { + aggregationBits.SetBitAt(uint64(index), true) + } + + payloadAttestation := ð.PayloadAttestation{ + AggregationBits: aggregationBits, + Data: ð.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, 32), + }, + Signature: make([]byte, 96), + } + + // Get attesting indices. + ctx := context.Background() + attesters, err := helpers.GetPayloadAttestingIndices(ctx, state, state.Slot(), payloadAttestation) + require.NoError(t, err) + require.Equal(t, len(indices), len(attesters)) + + // Get an IndexedPayloadAttestation. + indexedPayloadAttestation, err := helpers.GetIndexedPayloadAttestation(ctx, state, state.Slot(), payloadAttestation) + require.NoError(t, err) + require.Equal(t, len(indices), len(indexedPayloadAttestation.AttestingIndices)) + require.DeepEqual(t, payloadAttestation.Data, indexedPayloadAttestation.Data) + require.DeepEqual(t, payloadAttestation.Signature, indexedPayloadAttestation.Signature) + + // Check if the attesting indices are the same. + slices.Sort(attesters) // GetIndexedPayloadAttestation sorts attesting indices. + require.DeepEqual(t, attesters, indexedPayloadAttestation.AttestingIndices) +} + +func TestIsValidIndexedPayloadAttestation(t *testing.T) { + helpers.ClearCache() + + // Create validators. + validatorCount := uint64(350) + validators := make([]*ethpb.Validator, validatorCount) + _, secretKeys, err := util.DeterministicDepositsAndKeys(validatorCount) + require.NoError(t, err) + + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + PublicKey: secretKeys[i].PublicKey().Marshal(), + WithdrawalCredentials: make([]byte, 32), + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + } + } + + // Create a beacon state. + state, err := state_native.InitializeFromProtoEpbs(ðpb.BeaconStateEPBS{ + Validators: validators, + Fork: ðpb.Fork{ + Epoch: 0, + CurrentVersion: params.BeaconConfig().GenesisForkVersion, + PreviousVersion: params.BeaconConfig().GenesisForkVersion, + }, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + }) + require.NoError(t, err) + + // Define test cases. + tests := []struct { + attestation *epbs.IndexedPayloadAttestation + }{ + { + attestation: &epbs.IndexedPayloadAttestation{ + AttestingIndices: []primitives.ValidatorIndex{1}, + Data: ð.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + }, + }, + { + attestation: &epbs.IndexedPayloadAttestation{ + AttestingIndices: []primitives.ValidatorIndex{13, 19}, + Data: ð.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + }, + }, + { + attestation: &epbs.IndexedPayloadAttestation{ + AttestingIndices: []primitives.ValidatorIndex{123, 234, 345}, + Data: ð.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + }, + }, + { + attestation: &epbs.IndexedPayloadAttestation{ + AttestingIndices: []primitives.ValidatorIndex{38, 46, 54, 62, 70, 78, 86, 194}, + Data: ð.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + }, + }, + { + attestation: &epbs.IndexedPayloadAttestation{ + AttestingIndices: []primitives.ValidatorIndex{5}, + Data: ð.PayloadAttestationData{ + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + }, + }, + } + + // Run test cases. + for _, test := range tests { + signatures := make([]bls.Signature, len(test.attestation.AttestingIndices)) + for i, index := range test.attestation.AttestingIndices { + signedBytes, err := signing.ComputeDomainAndSign( + state, + slots.ToEpoch(test.attestation.Data.Slot), + test.attestation.Data, + params.BeaconConfig().DomainPTCAttester, + secretKeys[index], + ) + require.NoError(t, err) + + signature, err := bls.SignatureFromBytes(signedBytes) + require.NoError(t, err) + + signatures[i] = signature + } + + aggregatedSignature := bls.AggregateSignatures(signatures) + test.attestation.Signature = aggregatedSignature.Marshal() + + isValid, err := helpers.IsValidIndexedPayloadAttestation(state, test.attestation) + require.NoError(t, err) + require.Equal(t, true, isValid) + } +} diff --git a/consensus-types/epbs/BUILD.bazel b/consensus-types/epbs/BUILD.bazel new file mode 100644 index 000000000000..8d2f8cb29f6e --- /dev/null +++ b/consensus-types/epbs/BUILD.bazel @@ -0,0 +1,12 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["indexed_payload_attestation.go"], + importpath = "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs", + visibility = ["//visibility:public"], + deps = [ + "//consensus-types/primitives:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + ], +) diff --git a/consensus-types/epbs/indexed_payload_attestation.go b/consensus-types/epbs/indexed_payload_attestation.go new file mode 100644 index 000000000000..31c35fb53c6b --- /dev/null +++ b/consensus-types/epbs/indexed_payload_attestation.go @@ -0,0 +1,33 @@ +package epbs + +import ( + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" +) + +type IndexedPayloadAttestation struct { + AttestingIndices []primitives.ValidatorIndex + Data *eth.PayloadAttestationData + Signature []byte +} + +func (x *IndexedPayloadAttestation) GetAttestingIndices() []primitives.ValidatorIndex { + if x != nil { + return x.AttestingIndices + } + return []primitives.ValidatorIndex(nil) +} + +func (x *IndexedPayloadAttestation) GetData() *eth.PayloadAttestationData { + if x != nil { + return x.Data + } + return nil +} + +func (x *IndexedPayloadAttestation) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +}